#2650·frankenphp

Static builds: the bundled `parallel` extension replaces Go's SIGSEGV handler without SA_ONSTACK, turning recoverable faults into process crashes

Author: austinginderCreated Sep 17, 2026Updated Sep 17, 2026

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:

  1. 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.
  2. 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.