Skip to content

Narrow crash handler to undefined-function calls only#378

Merged
realFlowControl merged 1 commit into
developfrom
florian/narrow-crash-handler-to-undefined-functions
Jun 16, 2026
Merged

Narrow crash handler to undefined-function calls only#378
realFlowControl merged 1 commit into
developfrom
florian/narrow-crash-handler-to-undefined-functions

Conversation

@realFlowControl

@realFlowControl realFlowControl commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #355.

On POSIX, the SIGSEGV handler introduced in #355 caught every fault on a parallel worker thread and converted it into a catchable IllegalInstruction error via zend_bailout(). That was too broad:

  • It masked genuine bugs in worker code (and in other loaded extensions): no backtrace, no core dump
  • zend_bailout() is a longjmp, and longjmp-ing out of a signal handler is not async-signal-safe

This PR restricts the POSIX conversion to the single case the handler was built for: a worker faulting on a ZEND_INIT_FCALL to a function that is not in the worker's function table (i.e. not provided via a bootstrap file). Any other fault now falls through to the previously installed handler, so it crashes properly with a backtrace / core dump (e.g. under ASAN/UBSAN or gdb).

Detection lives in php_parallel_scheduler_recover_missing_function().

Windows is intentionally left unchanged

The Windows VEH is a process-wide, first-chance handler; #355 used it as a catch-all for every access violation on a worker. Narrowing it the same way unmasked a latent shutdown access violation in functional/007 (bootstrap set inside a task) that kills the process. That is a separate, Windows-only issue requiring a Windows repro to fix properly, so the VEH keeps its original catch-all behavior here.

The SIGSEGV/VEH handler added in #355 caught *every* fault on a parallel
worker thread and converted it into a catchable IllegalInstruction error
via zend_bailout(). This masked genuine bugs (no backtrace, no core dump)
and longjmp-ing out of a signal handler is not async-signal-safe.

Restrict the conversion to the single case it was built for: a worker
faulting on a ZEND_INIT_FCALL to a function that is not in the worker's
function table (i.e. not provided via a bootstrap file). Any other fault
now falls through to the previous handler so it crashes properly with a
backtrace / core dump.

Detection is factored into php_parallel_scheduler_recover_missing_function()
shared by both the POSIX and Windows handlers.
@realFlowControl
realFlowControl force-pushed the florian/narrow-crash-handler-to-undefined-functions branch from 285f6e9 to 41b8bfc Compare June 16, 2026 08:48
@realFlowControl
realFlowControl marked this pull request as ready for review June 16, 2026 08:56
@realFlowControl
realFlowControl merged commit f1f7bd4 into develop Jun 16, 2026
73 of 88 checks passed
@realFlowControl
realFlowControl deleted the florian/narrow-crash-handler-to-undefined-functions branch June 16, 2026 09:17
realFlowControl added a commit that referenced this pull request Jun 16, 2026
Apply the same policy the POSIX SIGSEGV handler already has (#378) to the
Windows vectored exception handler: only convert an access violation into a
catchable IllegalInstruction error when a parallel worker faulted on a call
to a function missing from its function table. Every other access violation
is forwarded to the previously installed handler via EXCEPTION_CONTINUE_SEARCH
(and crashes cleanly if nothing handles it) instead of being masked by an
unconditional zend_bailout().

The missing-function detection is shared between both handlers via
php_parallel_scheduler_recover_missing_function().
realFlowControl added a commit that referenced this pull request Jun 16, 2026
Apply the same policy the POSIX SIGSEGV handler already has (#378) to the
Windows vectored exception handler: only convert an access violation into a
catchable IllegalInstruction error when a parallel worker faulted on a call
to a function missing from its function table. Every other access violation
is forwarded to the previously installed handler via EXCEPTION_CONTINUE_SEARCH
(and crashes cleanly if nothing handles it) instead of being masked by an
unconditional zend_bailout().

The missing-function detection is shared between both handlers via
php_parallel_scheduler_recover_missing_function().
realFlowControl added a commit that referenced this pull request Jun 16, 2026
* Fix use-after-free race on future->monitor in error paths

When a task threw (normal exception) or crashed (IllegalInstruction), the
worker signalled the future twice: first PHP_PARALLEL_ERROR (or READY|ERROR),
then an unconditional trailing PHP_PARALLEL_READY at the end of
php_parallel_scheduler_run().

Future::value() waits on READY|FAILURE|ERROR, so the first signal already
releases the consumer. The consumer can then tear the future down (freeing
future->monitor) before the worker runs the trailing
php_parallel_monitor_set(future->monitor, PHP_PARALLEL_READY), which then
dereferences a dangling monitor inside pthread_cond_signal -> access
violation / use-after-free.

This was observed as a hard crash on Windows (0xC0000005) in
tests/functional/007.phpt, masked until now by the catch-all VEH; on Linux
the freed memory happened to stay readable.

Fix: the error paths now signal READY|ERROR exactly once and skip the
trailing READY (via a 'ready' flag). The success and kill paths are
unchanged - they have no early wake and still rely on the trailing READY.
Setting READY in the error path also ensures the future destructor (which
waits for READY) never blocks when value() is not called.

* Narrow the Windows VEH to undefined-function calls only

Apply the same policy the POSIX SIGSEGV handler already has (#378) to the
Windows vectored exception handler: only convert an access violation into a
catchable IllegalInstruction error when a parallel worker faulted on a call
to a function missing from its function table. Every other access violation
is forwarded to the previously installed handler via EXCEPTION_CONTINUE_SEARCH
(and crashes cleanly if nothing handles it) instead of being masked by an
unconditional zend_bailout().

The missing-function detection is shared between both handlers via
php_parallel_scheduler_recover_missing_function().
@realFlowControl realFlowControl added this to the v1.2.14 milestone Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant