Static builds: the bundled `parallel` extension replaces Go's SIGSEGV handler without SA_ONSTACK, turning recoverable faults into process crashes
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 11.3k
- Forks
- 488
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 11
Description
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()(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. - Drop
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read build-static.sh for defaultExtensions, frankenphp.c around php_module_startup(), and src/scheduler.c for parallel's SIGSEGV setup. First determine which proposed FrankenPHP-side fix is accepted, then validate static builds under the reported workload. Done means parallel no longer causes Go's signal-stack protection to be bypassed and the process remains stable.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, go, php
- Domain
- backend, build-system, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100