Skip to content

Detect crash signals in restarted process exit code#162

Closed
alies-dev wants to merge 3 commits intocomposer:mainfrom
alies-dev:detect-crash-signals
Closed

Detect crash signals in restarted process exit code#162
alies-dev wants to merge 3 commits intocomposer:mainfrom
alies-dev:detect-crash-signals

Conversation

@alies-dev
Copy link

@alies-dev alies-dev commented Feb 25, 2026

Summary

On non-Windows systems, proc_close() returns the raw signal number when the child process is killed by a signal (e.g. 11 for SIGSEGV). This is indistinguishable from a normal exit code, causing consumers to silently exit with a misleading code and no diagnostic output.

This PR uses proc_get_status() — which provides definitive signaled and termsig fields — to reliably detect when the child process was killed by a signal, then normalizes the exit code to the Unix convention of 128 + signal.

Why proc_get_status() instead of guessing

The previous approach hardcoded known signal numbers (4, 6, 8, 11) and assumed those exit codes meant a signal death. But those are also valid normal exit codes — exit(11) would be misidentified as SIGSEGV.

proc_get_status() returns separate signaled (bool) and termsig (int) fields that definitively distinguish signal deaths from normal exits. This works on all PHP versions and handles any signal.

Also filed php/php-src#21293 to fix proc_close() itself, but that would only help on future PHP versions.

Context

Discovered while investigating vimeo/psalm#11679, where Psalm's restarted process crashes with SIGSEGV due to a PHP JIT bug, but proc_close() returns 11 — silently producing a misleading exit code.

Before

$ psalm --no-cache
No errors found!
$ echo $?
11
# No indication of what went wrong

After

$ XDEBUG_HANDLER_DEBUG=1 psalm --no-cache
No errors found!
xdebug-handler[1234] Restarted process was killed by signal 11
$ echo $?
139

On non-Windows systems, proc_close() returns the raw signal number
when the child process is killed by a signal (e.g. 11 for SIGSEGV).
This is indistinguishable from a normal exit code, causing consumers
to silently exit with a misleading code and no error output.

Detect common crash signals (SIGILL, SIGABRT, SIGFPE, SIGSEGV) and:
- Log a warning via the existing Status notification system
- Normalize the exit code to the Unix convention of 128 + signal
@alies-dev
Copy link
Author

For reference, I've also reported the underlying PHP issue: php/php-src#21292

proc_close() returns the raw signal number instead of 128 + signal, making it impossible to distinguish signals from normal exit codes in userland.

@alies-dev
Copy link
Author

FYI — proc_get_status() already correctly distinguishes signal deaths from normal exits on all PHP versions. It returns signaled (bool) and termsig (int) fields, so this could be used as a reliable workaround today without waiting for a php-src fix:

$status = proc_get_status($process);
while ($status['running']) {
    usleep(1000);
    $status = proc_get_status($process);
}

if ($status['signaled']) {
    $exitCode = 128 + $status['termsig'];
} else {
    $exitCode = $status['exitcode'];
}

proc_close($process);

I also opened a fix on the php-src side (php/php-src#21293) to make proc_close() return 128 + signal directly, but that would only help on future PHP versions.

Instead of guessing based on a hardcoded list of signal numbers
(which are also valid normal exit codes), use proc_get_status()
which provides definitive signaled/termsig fields.

This works on all PHP versions and detects any signal, not just
the four previously hardcoded ones.
@alies-dev alies-dev force-pushed the detect-crash-signals branch from 5622113 to ec41d2b Compare February 25, 2026 10:19
The previous approach called proc_get_status() once before
proc_close(), but the process could still be running at that point,
causing the signal detection to be skipped entirely.

Now polls proc_get_status() until the process exits, then reads the
definitive signaled/termsig fields. proc_close() is called only to
free the resource.
$process = proc_open($cmd, [], $pipes);
if (is_resource($process)) {
$exitCode = proc_close($process);
// Poll proc_get_status until the process exits, so we can
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would love to see a test failing without this. otherwise, it will be fragile and may break again any time in future

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate your review here: #163

@johnstevenson
Copy link
Member

Thanks but I think it is better to let PHP sort this out.

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.

3 participants