diff --git a/library/std/src/os/unix/process.rs b/library/std/src/os/unix/process.rs index fab1b20b8c0e9..b96469d799ed1 100644 --- a/library/std/src/os/unix/process.rs +++ b/library/std/src/os/unix/process.rs @@ -219,6 +219,35 @@ pub trait CommandExt: Sealed { #[unstable(feature = "process_setsid", issue = "105376")] fn setsid(&mut self, setsid: bool) -> &mut process::Command; + + /// Registers a `dup2` file action to be performed when spawning the child + /// process. + /// + /// This adds a `posix_spawn_file_actions_adddup2(oldfd, newfd)` action when + /// using the `posix_spawn` path, or a `dup2(oldfd, newfd)` call in the + /// `fork`+`exec` fallback path. + /// + /// A common use case is passing `dup2(fd, fd)` which, on glibc >= 2.29, + /// clears the `CLOEXEC` flag on `fd` — allowing the child to inherit file + /// descriptors (such as jobserver pipes) without requiring a [`pre_exec`] + /// closure. This is important because `pre_exec` closures prevent the use of + /// the fast `posix_spawn` path. + /// + /// The actions are executed in order, after stdio setup but before any + /// [`pre_exec`] closures. + /// + /// # Safety + /// + /// The caller must ensure that `oldfd` is a valid, open file descriptor at + /// the time the child process is spawned. If `oldfd != newfd`, any + /// previously open `newfd` in the child will be silently closed and + /// replaced. The caller is responsible for ensuring this does not + /// conflict with stdio file descriptors or other file action + /// registrations. + /// + /// [`pre_exec`]: CommandExt::pre_exec + #[unstable(feature = "process_file_actions", issue = "none")] + unsafe fn dup2_file_action(&mut self, oldfd: RawFd, newfd: RawFd) -> &mut process::Command; } #[stable(feature = "rust1", since = "1.0.0")] @@ -274,6 +303,11 @@ impl CommandExt for process::Command { self.as_inner_mut().setsid(setsid); self } + + unsafe fn dup2_file_action(&mut self, oldfd: RawFd, newfd: RawFd) -> &mut process::Command { + self.as_inner_mut().add_dup2_file_action(oldfd, newfd); + self + } } /// Unix-specific extensions to [`process::ExitStatus`] and diff --git a/library/std/src/sys/process/unix/common.rs b/library/std/src/sys/process/unix/common.rs index f6bbfed61ef31..9383d7fbd4a63 100644 --- a/library/std/src/sys/process/unix/common.rs +++ b/library/std/src/sys/process/unix/common.rs @@ -95,6 +95,7 @@ pub struct Command { gid: Option, saw_nul: bool, closures: Vec io::Result<()> + Send + Sync>>, + dup2_file_actions: Vec<(c_int, c_int)>, groups: Option>, stdin: Option, stdout: Option, @@ -175,6 +176,7 @@ impl Command { gid: None, saw_nul, closures: Vec::new(), + dup2_file_actions: Vec::new(), groups: None, stdin: None, stdout: None, @@ -316,6 +318,14 @@ impl Command { self.closures.push(f); } + pub fn add_dup2_file_action(&mut self, oldfd: c_int, newfd: c_int) { + self.dup2_file_actions.push((oldfd, newfd)); + } + + pub fn get_dup2_file_actions(&self) -> &[(c_int, c_int)] { + &self.dup2_file_actions + } + pub fn stdin(&mut self, stdin: Stdio) { self.stdin = Some(stdin); } diff --git a/library/std/src/sys/process/unix/unix.rs b/library/std/src/sys/process/unix/unix.rs index 82ff94fb1e030..a0b9bdf989470 100644 --- a/library/std/src/sys/process/unix/unix.rs +++ b/library/std/src/sys/process/unix/unix.rs @@ -380,6 +380,13 @@ impl Command { } } + // Execute any registered dup2 file actions. These are used to manipulate + // file descriptors in the child (e.g., clearing CLOEXEC via dup2(fd, fd)) + // without requiring a pre_exec closure, which would prevent posix_spawn. + for &(oldfd, newfd) in self.get_dup2_file_actions() { + cvt_r(|| libc::dup2(oldfd, newfd))?; + } + for callback in self.get_closures().iter_mut() { callback()?; } @@ -719,6 +726,16 @@ impl Command { cvt_nz(f(file_actions.0.as_mut_ptr(), cwd.as_ptr()))?; } + // Add any registered dup2 file actions (e.g., for clearing CLOEXEC + // on inherited fds like jobserver pipes). + for &(oldfd, newfd) in self.get_dup2_file_actions() { + cvt_nz(libc::posix_spawn_file_actions_adddup2( + file_actions.0.as_mut_ptr(), + oldfd, + newfd, + ))?; + } + if let Some(pgroup) = pgroup { flags |= libc::POSIX_SPAWN_SETPGROUP; cvt_nz(libc::posix_spawnattr_setpgroup(attrs.0.as_mut_ptr(), pgroup))?; diff --git a/tests/ui/command/command-dup2-file-action.rs b/tests/ui/command/command-dup2-file-action.rs new file mode 100644 index 0000000000000..81c88f019040f --- /dev/null +++ b/tests/ui/command/command-dup2-file-action.rs @@ -0,0 +1,87 @@ +//@ run-pass +//@ only-unix (this is a unix-specific test) +//@ needs-subprocess +//@ ignore-fuchsia no execvp syscall +//@ ignore-tvos execvp is prohibited +//@ ignore-watchos execvp is prohibited + +// Test for CommandExt::dup2_file_action: verifies that a CLOEXEC pipe fd +// can be inherited by a child process when dup2_file_action(fd, fd) is used +// to clear the CLOEXEC flag via posix_spawn_file_actions_adddup2. + +#![feature(rustc_private, process_file_actions)] + +extern crate libc; + +use std::env; +use std::os::unix::process::CommandExt; +use std::process::Command; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() >= 3 && args[1] == "child" { + // Child mode: read from the inherited fd and print what we got + let fd: libc::c_int = args[2].parse().unwrap(); + let mut buf = [0u8; 64]; + let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) }; + assert!(n > 0, "expected to read from inherited fd {}, got {}", fd, n); + let msg = std::str::from_utf8(&buf[..n as usize]).unwrap(); + assert_eq!(msg, "hello from parent"); + return; + } + + // Parent mode: create a pipe (fds are CLOEXEC by default via pipe2), + // write to the write end, then spawn a child with dup2_file_action + // to clear CLOEXEC on the read end so the child can read it. + + let mut pipe_fds = [0 as libc::c_int; 2]; + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + { + let ret = unsafe { libc::pipe2(pipe_fds.as_mut_ptr(), libc::O_CLOEXEC) }; + assert_eq!(ret, 0, "pipe2 failed"); + } + #[cfg(any(target_os = "macos", target_os = "ios"))] + { + // macOS doesn't have pipe2, use pipe + fcntl + let ret = unsafe { libc::pipe(pipe_fds.as_mut_ptr()) }; + assert_eq!(ret, 0, "pipe failed"); + unsafe { + libc::fcntl(pipe_fds[0], libc::F_SETFD, libc::FD_CLOEXEC); + libc::fcntl(pipe_fds[1], libc::F_SETFD, libc::FD_CLOEXEC); + } + } + + let read_fd = pipe_fds[0]; + let write_fd = pipe_fds[1]; + + // Verify the read end has CLOEXEC set + let flags = unsafe { libc::fcntl(read_fd, libc::F_GETFD) }; + assert!(flags & libc::FD_CLOEXEC != 0, "expected CLOEXEC on read fd"); + + // Write data to the pipe + let msg = b"hello from parent"; + let written = unsafe { libc::write(write_fd, msg.as_ptr() as *const _, msg.len()) }; + assert_eq!(written, msg.len() as isize); + unsafe { libc::close(write_fd) }; + + // Spawn child with dup2_file_action(read_fd, read_fd) to clear CLOEXEC + let me = env::current_exe().unwrap(); + let output = unsafe { + Command::new(&me) + .arg("child") + .arg(read_fd.to_string()) + .dup2_file_action(read_fd, read_fd) + .output() + .unwrap() + }; + + unsafe { libc::close(read_fd) }; + + assert!( + output.status.success(), + "child failed: status={}, stderr={}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); +}