[php-src] PR #23362: Fix GH-10134: report internal call arguments to GC

From: Date: Wed, 19 Aug 2026 03:01:56 +0000
Subject: [php-src] PR #23362: Fix GH-10134: report internal call arguments to GC
Groups: php.git-pulls 
Request: Send a blank email to git-pulls+get-37990@lists.php.net to get a copy of this message
Pull Request: https://github.com/php/php-src/pull/23362 Author: xhon-pelushi Fixes GH-10134. ## The bug A fiber can be suspended *below* an internal function. That leaves the internal frame parked on the fiber's stack with the arguments that were pushed for it still live — the frame owns them and only releases them when it returns. zend_unfinished_execution_gc_ex() returns early for internal frames without reporting those arguments: ```c if (!ZEND_USER_CODE(EX(func)->common.type)) { ZEND_ASSERT(...); return NULL; /* arguments never reported */ } ``` So the cycle collector never sees them, and any cycle running through such an argument is uncollectable — it is only broken at request shutdown. ## Not specific to generators GH-10134 reports this as a generator problem, because the reporter hit it through Fiber::suspend() inside a generator resumed by Generator::send($fiber) — $fiber lands in the argument slot of the internal Generator::send frame. But nothing about it is generator-specific. With no generator anywhere, a fiber suspended inside array_map() whose array argument holds the fiber behaves identically (second test added here). ## How it was diagnosed zend_fiber_object_gc() is not at fault — it correctly walks the suspended fiber's frames, finds the generator frame, and calls zend_generator_frame_gc() on it. Instrumenting the collector's root processing showed the actual failure: ``` [root-before] Fiber rc=2 PURPLE reported obj Fiber rc=2 <- reported once [root-after-mark] Fiber rc=1 GREY [root-after-mark] Generator rc=3 <- restored from 0 ``` The Fiber's refcount is 2 but the mark phase discovers only one of those references, so it lands at 1 instead of 0, gc_scan concludes it is externally referenced, and gc_scan_black restores every refcount it had decremented. Dumping the frames shows where the second reference lives: ``` frame ... INTERNAL func=send nargs=1 ARG[0] type=8 Fiber 0x... <- never reported ``` ## On double counting This was the main risk. It does not happen: on ZEND_DO_ICALL / ZEND_DO_FCALL the VM does EX(call) = call->prev_execute_data, so an *entered* frame is no longer in its caller's EX(call) chain and is therefore not also scanned by zend_unfinished_calls_gc(), which only walks calls that are still being built. Those pending frames are siblings rather than ancestors, so they never show up in the prev_execute_data walk either. zend_unfinished_execution_gc_ex() has two callers. zend_generator_frame_gc() always passes generator->execute_data, which is user code, so only the fiber path is affected in practice. ## Testing - Both new tests **fail on unmodified master and pass with this change** — I ran them against a pristine build to confirm they are real regression guards. - The issue's verbatim script now prints the expected order (Generator dtor, Fiber dtor, Shutdown) instead of Shutdown first, and gc_collect_cycles() returns 1 instead of 0. - **No premature frees**, which is the thing to worry about when handing GC additional edges: an object passed as an internal-frame argument that is *also* still referenced from live scope survives gc_collect_cycles() and remains usable. Checked explicitly. - Full test suite on this build (--disable-all --enable-debug): **13386 passed, 0 failed, 0 warned** (7 expected-fail, the rest skipped for disabled extensions). - Zend/tests/fibers and Zend/tests/gc under valgrind memcheck: clean. Built and tested on Linux x86_64, --disable-all --enable-debug, so the extension-heavy parts of the suite were skipped; a normal CI run would cover those.

« previous php.git-pulls (#37990) next »