Description
On Linux when a CommandExt::pre_exec
hook is in use, std::process::Command::spawn
forks, calls do_exec
and then runs each of the hooks in order. If do_exec
fails (if a pre_exec hook or exec itself fails for example), the Err
bubbles up and the child process writes the errno to the pipe of the parent before exiting.
After the parent reads the error, it calls Command::wait
which internally calls waitpid(2)
. When a SIGCHLD
handler is installed - in this case, SIG_IGN
- there is no child to wait for, so waitpid(2)
returns ECHILD
. This leads to spawn
panicking:
thread 'main' panicked at 'wait() should either return Ok or panic', library/std/src/sys/unix/process/process_unix.rs:129:21
I expected to be able to return an error from pre_exec
hooks without causing a panic, even if SIGCHLD
is handled.
The pre_exec
hook is a prerequisite in order to disable the posix_spawn
path. Note that there are other ways to disable posix_spawn
, which would reproduce the issue as well.
Reproduction
The issue can be reproduced as follows:
// Built with libc 0.2.141
fn main() {
use std::os::unix::process::CommandExt;
let mut cmd = std::process::Command::new("hopefully invalid path");
unsafe {
libc::signal(libc::SIGCHLD, libc::SIG_IGN);
cmd.pre_exec(|| Ok(()));
}
cmd.spawn().unwrap();
}
Which results in
thread 'main' panicked at 'wait() should either return Ok or panic', library/std/src/sys/unix/process/process_unix.rs:129:21
stack backtrace:
0: rust_begin_unwind
at /rustc/5e1d3299a290026b85787bc9c7e72bcc53ac283f/library/std/src/panicking.rs:577:5
1: core::panicking::panic_fmt
at /rustc/5e1d3299a290026b85787bc9c7e72bcc53ac283f/library/core/src/panicking.rs:67:14
2: std::sys::unix::process::process_inner::<impl std::sys::unix::process::process_common::Command>::spawn
3: std::process::Command::spawn
at /rustc/5e1d3299a290026b85787bc9c7e72bcc53ac283f/library/std/src/process.rs:893:9
4: rust_test::main
at ./src/main.rs:10:5
5: core::ops::function::FnOnce::call_once
at /rustc/5e1d3299a290026b85787bc9c7e72bcc53ac283f/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
The waitpid behavior can also be observed in plain C with:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
int main() {
signal(SIGCHLD, SIG_IGN);
pid_t pid = fork();
if (pid == -1) {
fprintf(stderr, "fork failed: %d, %s\n", errno, strerror(errno));
return 1;
}
if (pid == 0) {
fprintf(stderr, "child!\n");
return 0;
}
int status = 0;
int res = waitpid(pid, &status, 0);
fprintf(stderr, "res: %d, status: %d, errno: %d, %s\n", res, status, errno, strerror(errno));
return 0;
}
Which outputs:
child!
res: -1, errno: 10, No child processes⏎
If the signal handler is commented out, waitpid
works as the Command::spawn
expects.
Meta
rustc --version --verbose
:
rustc 1.70.0-nightly (5e1d3299a 2023-03-31)
binary: rustc
commit-hash: 5e1d3299a290026b85787bc9c7e72bcc53ac283f
commit-date: 2023-03-31
host: x86_64-unknown-linux-gnu
release: 1.70.0-nightly
LLVM version: 16.0.0
- Kernel: Linux 6.2.10-arch1-1
- glibc: 2.37-2