fix: consume the async exit exception left in EG at request shutdown#15
Closed
EdmondDantes wants to merge 1 commit into
Closed
fix: consume the async exit exception left in EG at request shutdown#15EdmondDantes wants to merge 1 commit into
EdmondDantes wants to merge 1 commit into
Conversation
After a deadlock the scheduler raises a terminal DeadlockError as the exit exception. When
the entrypoint drives the main coroutine to completion itself (php_execute_script, the
normal file path), that function's own post-run check reports and frees it. But `php -r`
returns from zend_eval_string with EG(exception) clean; the main coroutine is only
finalized later, during php_request_shutdown's final scheduler pass, which rethrows the
DeadlockError into EG(exception) with no caller left to handle it. In a debug build the
Zend MM shutdown then reports it as a leaked object ("Freeing ... 152 bytes").
Consume it right where it is raised: after ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN, if an
exception is pending, report and release it exactly as php_execute_script does. The `php -r`
deadlock now prints the "Uncaught DeadlockError" fatal like the file path, and the object is
freed. The guard is a no-op for every request that reaches shutdown with EG clean.
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.
The bug
After a deadlock, the scheduler raises a terminal
DeadlockErroras the exit exception. A debug build then reports it as a leaked object at Zend MM shutdown:It reproduces only via
php -r, never through file execution, and only after an async deadlock (channel or mutualawait, caught or uncaught).valgrind --leak-check=fullreports0 bytes lost— the object is freed at MM shutdown, so it is not a growing leak, but it is a genuine defect: plainphp -r 'throw new Exception'does not leak, only the async path does.Root cause — the entrypoint asymmetry
php_execute_script()(the file path) finalizes the main coroutine and consumes its exception itself:The
-rendpoint iszend_eval_string_ex(), which does not run the scheduler-after-main.zend_eval_string_exreturns withEG(exception)clean; the main coroutine is only finalized later, duringphp_request_shutdown()'s ownZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN(main.c:1990), which rethrows theDeadlockErrorintoEG(exception)— with no caller left to handle it.Confirmed by a backtrace of where
EG(exception)is set in-rmode:The fix
Consume the exception right where it is raised — after
ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAINinphp_request_shutdown, mirroring exactly whatphp_execute_scriptalready does at its own scheduler-after-main:For the file path this is a no-op:
php_execute_scriptalready finalized the coroutine and consumed the exception, soEG(exception)is clean by the time shutdown's scheduler pass runs. The guard only fires on the deferred (-r/eval/stdin) path.Verified
-rdeadlock — leak-rdeadlock — printsUncaught DeadlockError-rdeadlock — exit codeAll four deadlock shapes (channel/mutual-await × caught/uncaught) via
-r: 0 leaks. The-rpath now behaves identically to the file path — it prints theUncaught DeadlockErrorfatal and frees the object.ext/async/testsandext/async/fuzzy-tests/_generatedshow no new failures;Zend/testsunaffected (the one failure there,arginfo_zpp_mismatch, crashes identically on baseline without this change).