Skip to content

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

Description

@austinginder

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions