What happens
On the static builds, FrankenPHP dies a few times an hour under load (heavy WordPress admin requests, plugin installs/updates through WP_Upgrader) with:
signal 11 received but handler not on signal stack
fatal error: non-Go code set up signal handler without SA_ONSTACK flag
runtime stack:
runtime.throw(...)
runtime.sigNotOnStack(0xb, ...)
runtime.adjustSignalStack2(0xb, ...)
runtime.sigtrampgo(0xb, ...)
...
goroutine N [syscall, locked to thread]: (a PHP thread)
Each one takes the whole process down (~2 s restart under a supervisor). Before this was pinned down it looked like heap corruption in the upgrader, because that is the heaviest request most sites run.
Root cause
The default static extension set (defaultExtensions in build-static.sh) includes parallel. Its PHP_MINIT_FUNCTION(PARALLEL_SCHEDULER) (src/scheduler.c) installs a SIGSEGV handler with sa_flags = SA_SIGINFO and no SA_ONSTACK, replacing the handler the Go runtime installed. Its handler chains into Go's saved one from the ordinary thread stack, which the Go runtime rejects (sigNotOnStack). So any SIGSEGV in a PHP thread that Go would normally convert into a recoverable per-request panic becomes a process-wide fatal.
Read from a live process (v1.12.7, PHP 8.5.9 ZTS, parallel 1.2.15, macOS arm64, sigaction(sig, NULL, &old) via lldb):
| Signal |
Handler |
sa_flags |
| SIGBUS / SIGABRT / SIGFPE |
runtime.cgoSigtramp |
SA_ONSTACK | SA_RESTART | SA_SIGINFO, mask all |
| SIGSEGV |
php_parallel_sigsegv_handler |
SA_SIGINFO only |
Proof
Restoring Go's handler in the running process, sigaction(SIGSEGV, &php_parallel_old_sigsegv_action, NULL) (parallel keeps the previous action in that global; it is what its MSHUTDOWN does), and then repeating the same load: 10 heavy runs (four real Plugin_Upgrader::bulk_upgrade batches, several plugin-heavy admin sweeps) produced zero fatals on one unchanged pid. The identical load earlier the same day had produced nine.
Filed upstream as krakjoe/parallel#406 (add SA_ONSTACK to its flags).
Possible fixes on the FrankenPHP side
Any of these would close it independently of parallel's release cadence:
- After
php_module_startup() (in frankenphp.c), re-assert SA_ONSTACK on whatever handler is installed for SIGSEGV/SIGBUS: sigaction(sig, NULL, &sa); if (!(sa.sa_flags & SA_ONSTACK)) { sa.sa_flags |= SA_ONSTACK; sigaction(sig, &sa, NULL); }. This is the shape Go's cgo documentation asks of foreign handlers and protects against any other extension doing the same.
- Drop
parallel from defaultExtensions in build-static.sh (its worker threads are its own thread model, which is an unusual thing to want inside FrankenPHP's threads anyway).
Happy to send a PR for (1) if that is welcome.
What happens
On the static builds, FrankenPHP dies a few times an hour under load (heavy WordPress admin requests, plugin installs/updates through
WP_Upgrader) with:Each one takes the whole process down (~2 s restart under a supervisor). Before this was pinned down it looked like heap corruption in the upgrader, because that is the heaviest request most sites run.
Root cause
The default static extension set (
defaultExtensionsinbuild-static.sh) includesparallel. ItsPHP_MINIT_FUNCTION(PARALLEL_SCHEDULER)(src/scheduler.c) installs a SIGSEGV handler withsa_flags = SA_SIGINFOand noSA_ONSTACK, replacing the handler the Go runtime installed. Its handler chains into Go's saved one from the ordinary thread stack, which the Go runtime rejects (sigNotOnStack). So any SIGSEGV in a PHP thread that Go would normally convert into a recoverable per-request panic becomes a process-wide fatal.Read from a live process (v1.12.7, PHP 8.5.9 ZTS, parallel 1.2.15, macOS arm64,
sigaction(sig, NULL, &old)via lldb):sa_flagsruntime.cgoSigtrampSA_ONSTACK | SA_RESTART | SA_SIGINFO, mask allphp_parallel_sigsegv_handlerSA_SIGINFOonlyProof
Restoring Go's handler in the running process,
sigaction(SIGSEGV, &php_parallel_old_sigsegv_action, NULL)(parallel keeps the previous action in that global; it is what its MSHUTDOWN does), and then repeating the same load: 10 heavy runs (four realPlugin_Upgrader::bulk_upgradebatches, several plugin-heavy admin sweeps) produced zero fatals on one unchanged pid. The identical load earlier the same day had produced nine.Filed upstream as krakjoe/parallel#406 (add
SA_ONSTACKto its flags).Possible fixes on the FrankenPHP side
Any of these would close it independently of parallel's release cadence:
php_module_startup()(infrankenphp.c), re-assertSA_ONSTACKon whatever handler is installed for SIGSEGV/SIGBUS:sigaction(sig, NULL, &sa); if (!(sa.sa_flags & SA_ONSTACK)) { sa.sa_flags |= SA_ONSTACK; sigaction(sig, &sa, NULL); }. This is the shape Go's cgo documentation asks of foreign handlers and protects against any other extension doing the same.parallelfromdefaultExtensionsinbuild-static.sh(its worker threads are its own thread model, which is an unusual thing to want inside FrankenPHP's threads anyway).Happy to send a PR for (1) if that is welcome.