fix: refuse to start on a ZTS build with Zend signals - #2639
Merged
Conversation
nicolas-grekas
force-pushed
the
zend-signals-guard
branch
from
September 7, 2026 12:17
b1ec9e9 to
3f5a6e8
Compare
nicolas-grekas
force-pushed
the
zend-signals-guard
branch
from
September 7, 2026 13:18
3f5a6e8 to
d563e2b
Compare
henderkes
approved these changes
Sep 7, 2026
alexandre-daubois
approved these changes
Sep 7, 2026
dunglas
requested changes
Sep 7, 2026
FrankenPHP has its own SAPI and never calls zend_signal_startup(), which the cli and embed SAPIs of php-src do, so zend_signal_globals_id stays 0. Registering the ini entries of the signal globals then resolves their address through ts_resource(0), which in ZTS returns the tsrm_tls_entry of the calling thread itself, so the write lands on the thread id stored there. TSRM stops recognizing the thread from then on: every lookup appends a new set of globals and tail-recurses, copying the constant, function and class tables each time. The server never finishes booting and its memory grows by hundreds of megabytes per second until it is killed, with no error and no log line. docs/compile.md and the Dockerfiles pass --disable-zend-signals, but PHP enables Zend signals by default, so building PHP by hand without that flag is enough to hit this. Check the flag PHP already reports through frankenphp_get_config() and fail with an actionable error instead. Non-ZTS builds are unaffected: their ini entries address the globals directly, TSRM is not involved.
nicolas-grekas
force-pushed
the
zend-signals-guard
branch
from
September 7, 2026 13:54
c52e28f to
5691558
Compare
dunglas
approved these changes
Sep 7, 2026
| return ErrInvalidPHPVersion | ||
| } | ||
|
|
||
| // FrankenPHP never calls zend_signal_startup(), so in ZTS the ini entries |
Member
There was a problem hiding this comment.
I wonder if calling it conditionally
would fix the issue.
Member
|
Thanks! |
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.
PHP enables Zend signals by default,
docs/compile.mdand the Dockerfiles pass--disable-zend-signals, and nothing checks it. Building PHP by hand without that flag gives a server that never finishes booting, with memory growing by hundreds of megabytes per second until the process is killed. No error, no log line, and the stacks only show PHP copying hash tables, so it reads like a slow machine.What happens: FrankenPHP has its own SAPI and never calls
zend_signal_startup(), which the cli and embed SAPIs of php-src do, sozend_signal_globals_idstays 0. Whenzend_register_standard_ini_entries()reacheszend.signal_check,OnUpdateBoolresolves its address asts_resource(*(int *)mh_arg2) + mh_arg1;ts_resource(0)returns thetsrm_tls_entryof the calling thread itself, so the bool write lands 16 bytes into it, on thethread_idfield, and zeroes its low byte. TSRM never matches that thread again:ts_resource_ex()appends a new resource set and tail-recurses on every call, each level runningexecutor_globals_ctor()and copying the constant, function and class tables. Watchpoint on the field, and the chain measured at 4013 entries before the process was killed.This checks the flag PHP already reports through
frankenphp_get_config()and failsInit()with an actionable error. Against a PHP built without the flag the tests now stop in 0.00s with that message instead of hanging; against a correctly built PHP the suite is unchanged.Non-ZTS builds keep working: their ini entries address the globals directly, TSRM is not involved, so the check is limited to ZTS.
The alternative would be to call
zend_signal_startup()like the embed SAPI does. That is not enough: with Zend signals actually running, PHP's handlers and the Go runtime's fight over the same signals and the test suite crashes, which is presumably why the flag is documented in the first place.