From 0e30e5cd3bd3f6114967fbbc72c6c4685bac0110 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Fri, 14 Aug 2026 09:24:31 +0000 Subject: [PATCH] Add ZEND_OBJECT_TRANSFER_RELEASE so a transit shell can free what it owns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit transfer_obj had TRANSFER and LOAD only. A handler that puts C state into the transit shell — a refcounted pointer, a persistent string — has no way to get it back: free_obj never runs for a shell, so the state is unreachable once the shell is released. Handlers work around this by hand where they can (the async extension frees a closure snapshot from its release walk; the TrueAsync server frees its worker shells from the object that owns them), and leak where they cannot. The third kind closes that. Its contract: the handler frees its own C state and returns NULL, must not call default_fn, and must tolerate a NULL ctx, since a release runs outside a request. WeakReference and WeakMap are the two handlers in Zend; both branch on TRANSFER and treat everything else as LOAD, so they now refuse the new kind explicitly. Their transit wrappers own nothing of their own — the referent slots and the class-name copy are freed by the generic release walk. --- Zend/zend_object_handlers.h | 6 ++++++ Zend/zend_weakrefs.c | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Zend/zend_object_handlers.h b/Zend/zend_object_handlers.h index d6a456db395b..191213f86e38 100644 --- a/Zend/zend_object_handlers.h +++ b/Zend/zend_object_handlers.h @@ -185,6 +185,12 @@ typedef zend_object* (*zend_object_clone_obj_with_t)(zend_object *object, const typedef enum { ZEND_OBJECT_TRANSFER, /* emalloc → pemalloc (send to another thread) */ ZEND_OBJECT_LOAD, /* pemalloc → emalloc (receive in target thread) */ + /* The transit shell is being freed: release what TRANSFER acquired into it. + * The handler frees its own C state and returns NULL; it must not call + * default_fn, and `ctx` may be NULL because a release runs outside a + * request. Without this kind, anything a handler stores in the shell is + * unreachable — free_obj never runs for a shell. */ + ZEND_OBJECT_TRANSFER_RELEASE, } zend_object_transfer_kind_t; typedef zend_object* (*zend_object_transfer_default_fn)( diff --git a/Zend/zend_weakrefs.c b/Zend/zend_weakrefs.c index 36453c5128fb..3bf961586c2f 100644 --- a/Zend/zend_weakrefs.c +++ b/Zend/zend_weakrefs.c @@ -818,6 +818,12 @@ static zend_object *zend_weakref_transfer_obj( { (void) default_fn; + if (kind == ZEND_OBJECT_TRANSFER_RELEASE) { + /* The transit wrapper owns nothing of its own: the referent slot and the + * class name are freed by the generic release walk. */ + return NULL; + } + if (kind == ZEND_OBJECT_TRANSFER) { zend_weakref *wr = zend_weakref_from(object); zend_object *referent = wr->referent; @@ -875,6 +881,12 @@ static zend_object *zend_weakmap_transfer_obj( { (void) default_fn; + if (kind == ZEND_OBJECT_TRANSFER_RELEASE) { + /* The transit wrapper owns nothing of its own: the key/value slots and + * the class name are freed by the generic release walk. */ + return NULL; + } + if (kind == ZEND_OBJECT_TRANSFER) { zend_weakmap *wm = zend_weakmap_from(object); const uint32_t num = zend_hash_num_elements(&wm->ht);