From b5ea591a2332ec64f4f8a296681f77260c676114 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+edmonddantes@users.noreply.github.com> Date: Tue, 28 Jul 2026 21:06:49 +0000 Subject: [PATCH] fix(thread): release multiple transferred roots under one visited set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A transfer deep-copies an object graph under one ctx whose xlat table dedups shared objects — a closure that captures an object transferred alongside it points at the same copy. Release dedups only WITHIN one async_thread_release_transferred_zval call (a fresh visited set each), so an embedder that stores several roots of one transfer and releases them separately freed a shared copy twice. Add async_thread_release_transferred_zvals(roots, n) — one visited set across all roots, freeing a shared object exactly once — and register it on the reactor's thread-pool API. --- libuv_reactor.c | 4 ++++ thread.c | 14 ++++++++++++++ thread.h | 1 + 3 files changed, 19 insertions(+) diff --git a/libuv_reactor.c b/libuv_reactor.c index 855424f..774ee4c 100644 --- a/libuv_reactor.c +++ b/libuv_reactor.c @@ -7161,4 +7161,8 @@ void async_libuv_reactor_register(void) async_thread_transfer_zval, async_thread_load_zval, async_thread_release_transferred_zval, async_thread_xlat_put_ctx, async_thread_defer_release_ctx); + + /* Batched multi-root release (one visited set). Additive; set directly rather + * than widening zend_async_thread_pool_register's signature. */ + zend_async_thread_release_transferred_zvals_fn = async_thread_release_transferred_zvals; } diff --git a/thread.c b/thread.c index 569c631..379a6fa 100644 --- a/thread.c +++ b/thread.c @@ -1401,6 +1401,20 @@ void async_thread_release_transferred_zval(zval *z) ZVAL_UNDEF(z); } +/* One release ctx (one visited set) across all roots — unlike the single-zval call, + * which builds a fresh set each time — so an object xlat-deduped into more than one + * root is freed once. Contract and rationale: the typedef in zend_async_API.h. */ +void async_thread_release_transferred_zvals(zval **roots, size_t count) +{ + thread_release_ctx_t ctx; + thread_release_ctx_init(&ctx); + for (size_t i = 0; i < count; i++) { + thread_release_transferred_zval(&ctx, roots[i]); + ZVAL_UNDEF(roots[i]); + } + thread_release_ctx_destroy(&ctx); +} + /* Partial-release for build-error paths: refcount-based so xlat-shared * sub-pieces still owned by the half-built snapshot survive; the snapshot's * full release frees them shortly afterwards. */ diff --git a/thread.h b/thread.h index 1ea4257..c9e995d 100644 --- a/thread.h +++ b/thread.h @@ -176,6 +176,7 @@ void async_thread_load_zval(zval *dst, const zval *src); * Free a persistent zval created by async_thread_transfer_zval(). */ void async_thread_release_transferred_zval(zval *z); +void async_thread_release_transferred_zvals(zval **roots, size_t count); /* Recursion helpers passed to zend_async_thread_pool_register() — expose * thread_transfer_zval_inner / thread_load_zval_inner / xlat_put / a lazy