Uh oh!
There was an error while loading. Please reload this page.
fix(reactor): no PHP object after the object store is gone (#264) - #265
Merged
Conversation
EdmondDantesforce-pushed
the
264-exception-after-object-store
branch
from
August 23, 2026 11:20
3edef8b to
8f2fb85CompareCodecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
zend_deactivate runs shutdown_executor() before ZEND_ASYNC_ENGINE_SHUTDOWN(), so the object store is freed and EG(active) is 0 by the time libuv_reactor_shutdown turns the loop to finish cancelling work. A write libuv cancels there reaches io_pipe_writev_cb, which branched on status == 0 alone and sent every other status to async_new_exception, where object_init_ex wrote into a NULL bucket table. async_new_exception answers NULL while the executor is inactive, and the four call sites that released its result unconditionally take that NULL. The shape has no caller inside php-src, so a build configured with --enable-async-fuzz queues one such write at reactor shutdown when ASYNC_FUZZ_CANCELLED_WRITE=1 is set. Without the guard the new test prints its expected output and then segfaults, which run-tests reports as Termsig=0; with it, 5 of 5 runs pass. true-async/server's h2/060-h2-write-deadline-reclaims, the intermittent symptom this started from, failed 4 of 30 standalone runs before and 0 of 30 after.
EdmondDantesforce-pushed
the
264-exception-after-object-store
branch
from
August 23, 2026 11:56
8f2fb85 to
c9669d3Compare… tests createTestTable, cleanupTestTable and runAsyncTest defaulted to `async_test` in both MySQL helpers, and every caller took the default. Under parallel workers the DROP TABLE IF EXISTS of whichever test started second removed the table the first had just created, and the first reported `Table 'test.async_test' doesn't exist`. The default is now `async_test_<pid>`: run-tests gives each test a process of its own. Over 002, 004, 011 and 012 at -j4 — 10 failing runs out of 10 before, 0 out of 10 after; both MySQL groups, 39 tests, green. The collision predates this branch: it reproduces the same way on the PHP built on 14 August. It surfaced now because CI reaches MySQL only intermittently, and a run that cannot connect skips these tests instead of racing them.
Uh oh!
There was an error while loading. Please reload this page.
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 freeto 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.
Closes#264.
async_new_exceptionreturns NULL whileEG(active)is 0, and the four callsites in
libuv_reactor.cthat released its result unconditionally take thatNULL:
:2204,:2870,:3126,:6286. The other 23 in that file alreadycarry NULL onward — assigned to
req->base.exceptionand passed to NOTIFY, orreleased under a check — and
async_rethrow_exception(NULL)is a no-op on bothof its branches.
The window this guards
zend_deactivaterunsshutdown_executor()and only thenZEND_ASYNC_ENGINE_SHUTDOWN(). By that pointzend_shutdown_executor_valueshas cleared
EG(active)("No PHP callback functions should be called after thispoint") and
zend_objects_store_destroyhas freed the bucket table.libuv_reactor_shutdownstill turns the loop, so a write libuv cancels therereaches
io_pipe_writev_cbwithstatus != 0and the old code built anexception object with no store to put it in.
Nothing else runs in that window: every uv callback in this extension is defined
in
libuv_reactor.c(scheduler.cmentions libuv only in comments), and thelisteners a NOTIFY reaches consume an exception rather than build one. Callers
outside that file run from PHP-visible code, where
EG(active)is 1.The test
tests/cleanup/005-write_cancelled_at_reactor_shutdown.phptneeds the shapebuilt on purpose: a queued write on a stream handle comes only from
ZEND_ASYNC_IO_WRITEV_EX, whose one consumer is true-async/server, andphp-src's single caller of the write API —
main/streams/plain_wrapper.c:553—writes files, whose queued work the drain waits out instead of cancelling. So a
build configured with
--enable-async-fuzzqueues one such write at reactorshutdown when
ASYNC_FUZZ_CANCELLED_WRITE=1is set, and the test skips on everyother build.
run-testshands a test's ENV section to its SKIPIF process aswell, so the hook declines to fire when
TEST_PHP_EVALUATING_SKIPIFis set;a
putenv()in the SKIPIF cannot do it, because PHP restores the originalenvironment at request shutdown, before the reactor stops.
Without the guard the test prints its expected output and then segfaults, which
run-testsreports asTermsig=0; with it, 5 of 5 runs pass. The full suite onthis build: 1293 tests, 1192 passed, 98 skipped, 3 failed. All three failures
are
include __DIR__ . '/../../../../ext/...'resolving outside php-src,because this checkout is reached through a symlink; nothing in this change can
reach them.
Debug builds crash. Release builds take the fast-shutdown path
(
shutdown_executor,#if ZEND_DEBUG→fast_shutdown = 0), where the buckettable outlives the teardown and the exception was built into a store nothing
reads again.
Where the symptom came from
true-async/server's
h2/060-h2-write-deadline-reclaimsfailed 4 of 30standalone runs and 2 of 17 full
-j4sweeps, its own assertions passing everytime. Against a PHP carrying this change: 0 of 30, and the server's phpt suite
is 349 passed, 20 skipped, 0 failed.
A second commit, test-only
pdo_mysql/011failed on this branch's first CI run, and it is not this change:four MySQL tests took the default fixture name
async_testin one database, sounder parallel workers the
DROP TABLE IF EXISTSof whichever started secondremoved the table the first had just created. It reproduces on the PHP built on
14 August — 10 failing runs out of 10 over those four tests at
-j4— and 0 outof 10 once the default becomes
async_test_<pid>. It rides here because CIcannot go green without it; the suite reaches MySQL only intermittently, which
is why the collision surfaced on this branch rather than earlier.