From ddfc19fa09708e01d7d7d2b1ede4eb528a3d508d Mon Sep 17 00:00:00 2001 From: xhon-pelushi Date: Tue, 18 Aug 2026 22:08:35 -0400 Subject: [PATCH] Fix GH-10134: report internal call arguments to GC A fiber can be suspended below an internal function, leaving that frame on its 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() returned early for internal frames without reporting those arguments, so the cycle collector never saw them. Any cycle running through such an argument was uncollectable and only broken at request shutdown. The reported case is Fiber::suspend() reached through Generator::send($fiber), where $fiber sits in the argument slot of the internal Generator::send frame, but the defect is not specific to generators: the same happens for any internal call a fiber is suspended inside, e.g. array_map(). Entered calls are removed from the caller's EX(call) chain, so these arguments are not also reported by zend_unfinished_calls_gc() and no reference is counted twice. --- .../fibers/gh10134-internal-call-args.phpt | 30 ++++++++++++++ Zend/tests/fibers/gh10134.phpt | 39 +++++++++++++++++++ Zend/zend_execute.c | 11 ++++++ 3 files changed, 80 insertions(+) create mode 100644 Zend/tests/fibers/gh10134-internal-call-args.phpt create mode 100644 Zend/tests/fibers/gh10134.phpt diff --git a/Zend/tests/fibers/gh10134-internal-call-args.phpt b/Zend/tests/fibers/gh10134-internal-call-args.phpt new file mode 100644 index 000000000000..b954ef6a9ddb --- /dev/null +++ b/Zend/tests/fibers/gh10134-internal-call-args.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-10134 (GC sees the arguments of an internal call a fiber is suspended inside) +--FILE-- +start(); + +$fiber = null; +$canary = null; +var_dump(gc_collect_cycles() > 0); + +var_dump('Shutdown'); + +?> +--EXPECT-- +string(11) "Canary dtor" +bool(true) +string(8) "Shutdown" diff --git a/Zend/tests/fibers/gh10134.phpt b/Zend/tests/fibers/gh10134.phpt new file mode 100644 index 000000000000..04a3b092005a --- /dev/null +++ b/Zend/tests/fibers/gh10134.phpt @@ -0,0 +1,39 @@ +--TEST-- +GH-10134 (Non-suspended generators in suspended Fiber do not participate in GC) +--FILE-- +x); + } +} + +$gen = (function() { + $canary = new Canary('Generator dtor'); + $fiber = yield; + Fiber::suspend(); +})(); + +$fiber = new Fiber(function() use ($gen, &$fiber) { + $canary = new Canary('Fiber dtor'); + $gen->send($fiber); +}); +$fiber->start(); + +$gen = null; +$fiber = null; +gc_collect_cycles(); + +var_dump('Shutdown'); + +?> +--EXPECT-- +string(14) "Generator dtor" +string(10) "Fiber dtor" +string(8) "Shutdown" diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 14a340ffee37..5809c65cd8b8 100644 --- a/Zend/zend_execute.c +++ b/Zend/zend_execute.c @@ -5012,6 +5012,17 @@ ZEND_API HashTable *zend_unfinished_execution_gc_ex(zend_execute_data *execute_d if (!ZEND_USER_CODE(EX(func)->common.type)) { ZEND_ASSERT(!(EX_CALL_INFO() & (ZEND_CALL_HAS_SYMBOL_TABLE|ZEND_CALL_FREE_EXTRA_ARGS|ZEND_CALL_HAS_EXTRA_NAMED_PARAMS))); + /* An internal function frame owns the arguments that were pushed for it and releases + * them when it returns. Execution can be suspended inside such a call: a fiber that + * suspends below an internal function leaves that frame on its stack, e.g. + * Fiber::suspend() reached through Generator::send($arg). The arguments are then still + * live, so they have to be reported or a cycle running through one of them is never + * collected. Entered calls are removed from the caller's EX(call) chain, so these are + * not also reported by zend_unfinished_calls_gc(). */ + uint32_t num_args = ZEND_CALL_NUM_ARGS(execute_data); + for (uint32_t i = 0; i < num_args; i++) { + zend_get_gc_buffer_add_zval(gc_buffer, ZEND_CALL_VAR_NUM(execute_data, i)); + } return NULL; }