Fix GH-10134: report internal call arguments to GC - #23362
Open
xhon-pelushi wants to merge 1 commit into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: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 byGenerator::send($fiber)—$fiberlands in the argument slot of the internalGenerator::sendframe. But nothing about it is generator-specific. With no generator anywhere, a fiber suspended insidearray_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 callszend_generator_frame_gc()on it. Instrumenting the collector's root processing showed the actual failure: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_scanconcludes it is externally referenced, andgc_scan_blackrestores every refcount it had decremented. Dumping the frames shows where the second reference lives:On double counting
This was the main risk. It does not happen: on
ZEND_DO_ICALL/ZEND_DO_FCALLthe VM doesEX(call) = call->prev_execute_data, so an entered frame is no longer in its caller'sEX(call)chain and is therefore not also scanned byzend_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 theprev_execute_datawalk either.zend_unfinished_execution_gc_ex()has two callers.zend_generator_frame_gc()always passesgenerator->execute_data, which is user code, so only the fiber path is affected in practice.Testing
Generator dtor,Fiber dtor,Shutdown) instead ofShutdownfirst, andgc_collect_cycles()returns 1 instead of 0.gc_collect_cycles()and remains usable. Checked explicitly.--disable-all --enable-debug): 13386 passed, 0 failed, 0 warned (7 expected-fail, the rest skipped for disabled extensions).Zend/tests/fibersandZend/tests/gcunder 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.