From 039ade6f551c6f74d0d2cda3ca645275746c9fd2 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:25:08 +0000 Subject: [PATCH] Release a transit shell's C state through the new transfer kind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit thread_release_transferred_object walked the shell's properties, dropped the class name and pefreed the allocation. Anything a transfer_obj handler had put in the shell's C prefix was never reached, because free_obj does not run for a shell — the TrueAsync server leaked a topic-hub reference per transferred room this way, 21,608 bytes of hub plus the persistent topic string. The walk now resolves the shell's class by name and calls its handler with ZEND_OBJECT_TRANSFER_RELEASE before the generic cleanup. No autoload: a release runs outside a request, and a class that is not loaded owns nothing here. The three handlers in this extension refuse the new kind explicitly, because each branches on TRANSFER and treats everything else as LOAD — reaching that branch with a NULL default_fn would crash. Closure's snapshot is already freed by the walk itself; the channel reference and the future's shared state move under protocols of their own, and reworking those is a separate change. --- future.c | 6 ++++++ thread.c | 21 +++++++++++++++++++++ thread_channel.c | 6 ++++++ 3 files changed, 33 insertions(+) diff --git a/future.c b/future.c index a4b45af..c772f8a 100644 --- a/future.c +++ b/future.c @@ -765,6 +765,12 @@ static zend_object *async_future_state_transfer_obj( zend_object *object, zend_async_thread_transfer_ctx_t *ctx, zend_object_transfer_kind_t kind, zend_object_transfer_default_fn default_fn) { + if (kind == ZEND_OBJECT_TRANSFER_RELEASE) { + /* The shell's shared_state ownership moves to the destination under the + * future protocol; reworking that is a change of its own. */ + return NULL; + } + if (kind == ZEND_OBJECT_TRANSFER) { /* Source thread: create shared_state, bind to original future */ async_future_state_t *src = FUTURE_STATE_FROM_OBJ(object); diff --git a/thread.c b/thread.c index 4887dd8..c320987 100644 --- a/thread.c +++ b/thread.c @@ -1381,6 +1381,21 @@ static void thread_release_transferred_object(thread_release_ctx_t *ctx, zend_ob obj->properties = NULL; } + /* Whatever TRANSFER put in the C prefix of this shell is reachable only + * through the class: free_obj never runs for a shell. Resolved by name + * without autoload — a release runs outside a request. */ + zend_string *lookup_name = zend_string_init( + ZSTR_VAL(class_name), ZSTR_LEN(class_name), 0); + zend_class_entry *ce = zend_lookup_class_ex( + lookup_name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + zend_string_release(lookup_name); + + if (ce != NULL && ce->default_object_handlers != NULL + && ce->default_object_handlers->transfer_obj != NULL) { + ce->default_object_handlers->transfer_obj( + obj, NULL, ZEND_OBJECT_TRANSFER_RELEASE, NULL); + } + for (uint32_t i = 0; i < prop_count; i++) { thread_release_transferred_zval(ctx, &obj->properties_table[i]); } @@ -3279,6 +3294,12 @@ static zend_object *closure_transfer_obj( zend_object *object, zend_async_thread_transfer_ctx_t *ctx, zend_object_transfer_kind_t kind, zend_object_transfer_default_fn default_fn) { + if (kind == ZEND_OBJECT_TRANSFER_RELEASE) { + /* The snapshot is freed by thread_release_transferred_object, which + * recognises a closure shell by its non-NULL `properties`. */ + return NULL; + } + if (kind == ZEND_OBJECT_TRANSFER) { /* Source thread → persistent: deep-copy closure via snapshot */ const zend_function *func = zend_get_closure_method_def(object); diff --git a/thread_channel.c b/thread_channel.c index 1577efa..e5def52 100644 --- a/thread_channel.c +++ b/thread_channel.c @@ -492,6 +492,12 @@ static zend_object *async_thread_channel_transfer_obj( zend_object *object, zend_async_thread_transfer_ctx_t *ctx, zend_object_transfer_kind_t kind, zend_object_transfer_default_fn default_fn) { + if (kind == ZEND_OBJECT_TRANSFER_RELEASE) { + /* The shell's channel reference outlives it; dropping one needs the + * channel's event dispose protocol, which is a change of its own. */ + return NULL; + } + if (kind == ZEND_OBJECT_TRANSFER) { /* Transfer: pemalloc wrapper via default, then copy channel pointer */ zend_object *dst = default_fn(object, ctx, sizeof(thread_channel_object_t));