Skip to content

proc_get_status() loses exit code when SIGCHLD interrupts waitpid() #21350

@NinaPeng-developer

Description

@NinaPeng-developer

Description

The following code:

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 from our 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().

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
Same with php -n (no extensions) exitcode: 42 [Still broken — in PHP core](exitcode: -1)
With pcntl_signal(SIGCHLD, SIG_DFL) first exitcode: 42 exitcode: 42

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 Version

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)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions