[php-src] Issue #21350: proc_get_status() loses exit code when SIGCHLD interrupts waitpid() (EINTR not handled)
[email protected] (NinaPeng-developer)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/21350
Author: NinaPeng-developer
### Description
The following code:
```php
while ($await_processes) {
$await_processes_copy = $await_processes;
foreach ($await_processes_copy as $worker_id => $process) {
$proc_status = proc_get_status($process['process']);
if ($proc_status === false) {
throw new MultiProcessHydraException("Failed to get proc status of worker $worker_id");
}
if ($proc_status['running'] === false) {
unset($await_processes[$worker_id]);
$stdout = stream_get_contents(fopen($process['files']['stdout'], 'r'));
$stderr = stream_get_contents(fopen($process['files']['stderr'], 'r'));
$exit_code = $proc_status['exitcode'];
$log_message = [
'message' => "Awaited process",
'job_class' => $this->worker_class,
'stdout' => $stdout,
'stderr' => $stderr,
'worker_id' => $worker_id,
'exit_code' => $exit_code,
];
$logger->log($log_message);
// If any child-process exits with a non-zero code (such as a fatal, uncatchable error)
// process this individual worker as a failure, but continue to process the responses from
// each other process.
if ($exit_code !== 0) {
$worker_responses[] = [[
'error' => "Worker process $worker_id exited with non-zero exit code: $exit_code. "
. "Stderr: $stderr",
]];
} else {
$worker_responses[] = $cache->getCacheValue($process['files']['cache_file'])->asArray();
}
unset($process['files']['cache_file']);
foreach ($process['files'] as $_ => $file) {
unlink($file);
}
}
}
}
<?php
```
Resulted in this output:
```
Uncaught Exception: [MultiProcessHydraException] Worker process mp-hydra-6b0a280c-2428-41f5-b393-11e4477b4e9d-7 exited with non-zero exit code: -1.
...(multiple process exited with -1)
```
But I expected this output instead:
```
The parent process checked the exitcode of child process to be 0 and finish the job, because the log, all the subtasks were successfully finished.
```
### Investigation
PHP 8.3 installs an internal SIGCHLD signal handler (not present in PHP 8.1) as part of the pcntl extension. When a child process exits, this handler intercepts the SIGCHLD signal and
interrupts the waitpid() syscall inside proc_get_status() with EINTR. Since waitpid_cached() in PHP 8.3's proc_open.c doesn't retry on EINTR, the exit code is lost and -1 is returned.
Verified by:
| Test | PHP 8.1 | PHP 8.3 |
|------|---------|---------|
| SIGCHLD disposition | SIG_DFL | CUSTOM_HANDLER |
| proc_open("exit(42)") + proc_get_status() | exitcode: 42 | exitcode: -1, cached: false |
| Same with php -n (no extensions) | N/A | Still broken — in PHP core |
| With pcntl_signal(SIGCHLD, SIG_DFL) first | N/A | exitcode: 42, cached: true ✅ |
#### Temporary Fix
Added pcntl_signal(SIGCHLD, SIG_DFL) before spawning workers in MultiProcessHydra::spawn(), resetting SIGCHLD to default behavior so proc_get_status() can reap children without
interruption.
References:
- [php.net/proc_get_status](https://www.php.net/proc_get_status) — PHP 8.3 cached field documentation
- [php/php-src#10250](https://github.com/php/php-src/pull/10250) — The PR that added waitpid_cached() (missing EINTR retry)
- [php/php-src#10239](https://github.com/php/php-src/issues/10239) — Original bug report
### PHP Version
```plain
PHP 8.3.25 (cli) (built: Jan 5 2026 18:10:06) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.3.25, Copyright (c) Zend Technologies
with Zend OPcache v8.3.25, Copyright (c), by Zend Technologies
```
### Operating System
Amazon Linux 2 (AL2)