[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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.