Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions library/std/src/os/unix/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this accept an OwnedFd or BorrowedFd as input fd to make this function safe?

}

#[stable(feature = "rust1", since = "1.0.0")]
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions library/std/src/sys/process/unix/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ pub struct Command {
gid: Option<gid_t>,
saw_nul: bool,
closures: Vec<Box<dyn FnMut() -> io::Result<()> + Send + Sync>>,
dup2_file_actions: Vec<(c_int, c_int)>,
groups: Option<Box<[gid_t]>>,
stdin: Option<Stdio>,
stdout: Option<Stdio>,
Expand Down Expand Up @@ -175,6 +176,7 @@ impl Command {
gid: None,
saw_nul,
closures: Vec::new(),
dup2_file_actions: Vec::new(),
groups: None,
stdin: None,
stdout: None,
Expand Down Expand Up @@ -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);
}
Expand Down
17 changes: 17 additions & 0 deletions library/std/src/sys/process/unix/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?;
}
Expand Down Expand Up @@ -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))?;
Expand Down
87 changes: 87 additions & 0 deletions tests/ui/command/command-dup2-file-action.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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)
);
}
Loading