diff --git a/library/std/src/os/unix/process.rs b/library/std/src/os/unix/process.rs index 9fa731de82691..33d3a601f6828 100644 --- a/library/std/src/os/unix/process.rs +++ b/library/std/src/os/unix/process.rs @@ -218,6 +218,89 @@ pub impl(self) trait CommandExt { #[unstable(feature = "process_setsid", issue = "105376")] fn setsid(&mut self, setsid: bool) -> &mut process::Command; + + /// Pass a file descriptor to a child process. + /// + /// `old_fd` is an open file descriptor in the parent process. This descriptor will be + /// duplicated in the child process and associated with the file descriptor number `new_fd`. + /// + /// Getting this right is tricky. It is recommended to provide further information to the child + /// process by some other mechanism. This could be an argument confirming file descriptors that + /// the child can use, device/inode numbers to allow for sanity checks, or something similar. + /// + /// If `old_fd` is an open file descriptor in the child process (e.g. if multiple parent fds are being + /// mapped to the same child one) and closing it would produce one or more errors, + /// those errors will be lost when this function is called. See + /// [`man 2 dup`] for more information. + /// + /// [`man 2 dup`]: https://pubs.opengroup.org/onlinepubs/9799919799/functions/dup.html + /// ``` + /// #![feature(command_pass_fds)] + /// + /// use std::process::{Command, Stdio}; + /// use std::os::unix::process::CommandExt; + /// use std::io::{self, Write}; + /// + /// # fn main() -> io::Result<()> { + /// let (pipe_reader, mut pipe_writer) = io::pipe()?; + /// + /// let fd_num = 123; + /// + /// let mut cmd = Command::new("cat"); + /// cmd.arg(format!("/dev/fd/{fd_num}")).stdout(Stdio::piped()).fd(fd_num, pipe_reader); + /// + /// let mut child = cmd.spawn()?; + /// let mut stdout = child.stdout.take().unwrap(); + /// + /// pipe_writer.write_all(b"Hello, world!")?; + /// drop(pipe_writer); + /// + /// child.wait()?; + /// assert_eq!(io::read_to_string(&mut stdout)?, "Hello, world!"); + /// + /// # Ok(()) + /// # } + /// ``` + /// + /// If this method is called multiple times with the same `new_fd`, all but one file descriptor + /// will be lost. + /// + /// ``` + /// #![feature(command_pass_fds)] + /// + /// use std::process::{Command, Stdio}; + /// use std::os::unix::process::CommandExt; + /// use std::io::{self, Write}; + /// + /// # fn main() -> io::Result<()> { + /// let (pipe_reader1, mut pipe_writer1) = io::pipe()?; + /// let (pipe_reader2, mut pipe_writer2) = io::pipe()?; + /// + /// let fd_num = 123; + /// + /// let mut cmd = Command::new("cat"); + /// cmd.arg(format!("/dev/fd/{fd_num}")) + /// .stdout(Stdio::piped()) + /// .fd(fd_num, pipe_reader1) + /// .fd(fd_num, pipe_reader2); + /// + /// pipe_writer1.write_all(b"Hello from pipe 1!")?; + /// drop(pipe_writer1); + /// + /// pipe_writer2.write_all(b"Hello from pipe 2!")?; + /// drop(pipe_writer2); + /// + /// let mut child = cmd.spawn()?; + /// let mut stdout = child.stdout.take().unwrap(); + /// + /// child.wait()?; + /// assert_eq!(io::read_to_string(&mut stdout)?, "Hello from pipe 2!"); + /// + /// # Ok(()) + /// # } + /// ``` + #[unstable(feature = "command_pass_fds", issue = "144989")] + fn fd(&mut self, new_fd: RawFd, old_fd: impl Into) -> &mut Self; } #[stable(feature = "rust1", since = "1.0.0")] @@ -273,6 +356,11 @@ impl CommandExt for process::Command { self.as_inner_mut().setsid(setsid); self } + + fn fd(&mut self, new_fd: RawFd, old_fd: impl Into) -> &mut Self { + self.as_inner_mut().fd(old_fd.into(), new_fd); + self + } } /// Unix-specific extensions to [`ExitStatus`] and [`ExitStatusError`]. diff --git a/library/std/src/sys/process/unix/common.rs b/library/std/src/sys/process/unix/common.rs index 4f281a4d62751..f2b43beca9d18 100644 --- a/library/std/src/sys/process/unix/common.rs +++ b/library/std/src/sys/process/unix/common.rs @@ -107,6 +107,12 @@ pub struct Command { create_pidfd: bool, pgroup: Option, setsid: bool, + /// A map of parent FDs to child FDs to be inherited during spawn. + fds: Vec<(OwnedFd, RawFd)>, + /// For testing purposes: store `Some(true)` if the last spawn used `posix_spawn`, `Some(false)` + /// if it used `exec`, and `None` if it hasn't been spawned yet. + #[cfg(test)] + last_spawn_was_posix_spawn: Option, } // passed to do_exec() with configuration of what the child stdio should look @@ -187,6 +193,9 @@ impl Command { create_pidfd: false, pgroup: None, setsid: false, + fds: Vec::new(), + #[cfg(test)] + last_spawn_was_posix_spawn: None, } } @@ -372,6 +381,33 @@ impl Command { let theirs = ChildPipes { stdin: their_stdin, stdout: their_stdout, stderr: their_stderr }; Ok((ours, theirs)) } + + pub fn fd(&mut self, old_fd: OwnedFd, new_fd: RawFd) { + self.fds.push((old_fd, new_fd)); + } + + pub fn get_fds(&self) -> &[(OwnedFd, RawFd)] { + &self.fds + } + + /// Clear the fd vector, closing all descriptors owned by this `Command`. + pub fn close_owned_fds(&mut self) { + self.fds.clear(); + } + + #[cfg_attr(not(test), allow(unused_variables))] + pub(crate) fn last_spawn_was_posix_spawn(&mut self, val: bool) { + #[cfg(test)] + { + self.last_spawn_was_posix_spawn = Some(val); + } + } + + // we only use this for testing + #[cfg(test)] + pub(crate) fn get_last_spawn_was_posix_spawn(&self) -> Option { + self.last_spawn_was_posix_spawn + } } fn os2c(s: &OsStr, saw_nul: &mut bool) -> CString { diff --git a/library/std/src/sys/process/unix/unix.rs b/library/std/src/sys/process/unix/unix.rs index aa47fcb3360c1..2054fa33f3e2b 100644 --- a/library/std/src/sys/process/unix/unix.rs +++ b/library/std/src/sys/process/unix/unix.rs @@ -13,10 +13,13 @@ use libc::{gid_t, uid_t}; use super::common::*; use crate::io::{self, Error, ErrorKind}; use crate::num::NonZero; +use crate::os::fd::AsRawFd; +#[cfg(not(any(target_os = "tvos", target_os = "watchos")))] +use crate::os::fd::OwnedFd; use crate::process::StdioPipes; -use crate::sys::cvt; #[cfg(target_os = "linux")] use crate::sys::process::PidFd; +use crate::sys::{FromInner, IntoInner, cvt}; use crate::{fmt, mem, sys}; cfg_select! { @@ -71,8 +74,12 @@ impl Command { let (ours, theirs) = self.setup_io(default, needs_stdin)?; if let Some(ret) = self.posix_spawn(&theirs, envp.as_ref())? { + self.last_spawn_was_posix_spawn(true); + // Close fds in the parent that have been duplicated in the child + self.close_owned_fds(); return Ok((ret, ours)); } + self.last_spawn_was_posix_spawn(false); #[cfg(target_os = "linux")] let (input, output) = sys::net::Socket::new_pair(libc::AF_UNIX, libc::SOCK_SEQPACKET)?; @@ -101,7 +108,15 @@ impl Command { if self.get_create_pidfd() { self.send_pidfd(&output); } - let Err(err) = unsafe { self.do_exec(theirs, envp.as_ref()) }; + #[cfg(target_os = "linux")] + let mut output_fd = output.into_inner().into_inner(); + #[cfg(not(target_os = "linux"))] + let mut output_fd = output.into_inner(); + let Err(err) = unsafe { self.do_exec(theirs, envp.as_ref(), Some(&mut output_fd)) }; + #[cfg(target_os = "linux")] + let output = sys::net::Socket::from_inner(sys::fd::FileDesc::from_inner(output_fd)); + #[cfg(not(target_os = "linux"))] + let output = sys::fd::FileDesc::from_inner(output_fd); let errno = err.raw_os_error().unwrap_or(libc::EINVAL) as u32; let errno = errno.to_be_bytes(); let bytes = [ @@ -124,6 +139,9 @@ impl Command { drop(env_lock); drop(output); + // Close fds in the parent that have been duplicated in the child + self.close_owned_fds(); + #[cfg(target_os = "linux")] let pidfd = if self.get_create_pidfd() { self.recv_pidfd(&input) } else { -1 }; @@ -241,7 +259,7 @@ impl Command { // environment lock before we try to exec. let _lock = sys::env::env_read_lock(); - let Err(e) = self.do_exec(theirs, envp.as_ref()); + let Err(e) = self.do_exec(theirs, envp.as_ref(), None); e } } @@ -284,6 +302,7 @@ impl Command { &mut self, stdio: ChildPipes, maybe_envp: Option<&CStringArray>, + mut preserve_fd: Option<&mut OwnedFd>, ) -> Result { use crate::sys::{self, cvt_r}; @@ -297,6 +316,22 @@ impl Command { cvt_r(|| libc::dup2(fd, libc::STDERR_FILENO))?; } + for &(ref old_fd, new_fd) in self.get_fds() { + if let Some(ref mut preserve_fd) = preserve_fd + && new_fd == preserve_fd.as_raw_fd() + { + let mut copy = preserve_fd.try_clone()?; + mem::swap(&mut copy, preserve_fd); + // after the swap, copy holds the same file descriptor as new_fd, so we can let dup2 + // close it instead + mem::forget(copy); + } + cvt_r(|| libc::dup2(old_fd.as_raw_fd(), new_fd))?; + if old_fd.as_raw_fd() != new_fd { + cvt_r(|| libc::close(old_fd.as_raw_fd()))?; + } + } + #[cfg(not(target_os = "l4re"))] { if let Some(_g) = self.get_groups() { @@ -462,6 +497,7 @@ impl Command { use core::sync::atomic::{Atomic, AtomicU8, Ordering}; use crate::mem::MaybeUninit; + use crate::os::fd::AsRawFd; use crate::pin::{Pin, pin}; use crate::sys::helpers::COpaque; use crate::sys::{self, cvt_nz, on_broken_pipe_used}; @@ -734,6 +770,19 @@ impl Command { libc::STDERR_FILENO, ))?; } + for &(ref old_fd, new_fd) in self.get_fds() { + cvt_nz(libc::posix_spawn_file_actions_adddup2( + file_actions.0.get(), + old_fd.as_raw_fd(), + new_fd, + ))?; + if old_fd.as_raw_fd() != new_fd { + cvt_nz(libc::posix_spawn_file_actions_addclose( + file_actions.0.get(), + old_fd.as_raw_fd(), + ))?; + } + } if let Some((f, cwd)) = addchdir { cvt_nz(f(file_actions.0.get(), cwd.as_ptr()))?; } diff --git a/library/std/src/sys/process/unix/unix/tests.rs b/library/std/src/sys/process/unix/unix/tests.rs index 9a029f16a3a20..5f4ba41b3461a 100644 --- a/library/std/src/sys/process/unix/unix/tests.rs +++ b/library/std/src/sys/process/unix/unix/tests.rs @@ -1,10 +1,20 @@ +use crate::collections::HashSet; +use crate::os::fd::AsRawFd; +use crate::os::unix::fs::MetadataExt; use crate::os::unix::process::{CommandExt, ExitStatusExt}; use crate::panic::catch_unwind; use crate::process::Command; +use crate::sys::AsInner; +use crate::{fs, io}; // Many of the other aspects of this situation, including heap alloc concurrency // safety etc., are tested in tests/ui/process/process-panic-after-fork.rs +/// Use dev + ino to uniquely identify a file +fn md_file_id(md: &fs::Metadata) -> (u64, u64) { + (md.dev(), md.ino()) +} + #[test] fn exitstatus_display_tests() { // In practice this is the same on every Unix. @@ -77,3 +87,191 @@ fn test_command_fork_no_unwind() { || signal == libc::SIGSEGV ); } + +/// For `Command`'s fd-related tests, we want to be sure they work both with exec +/// and with `posix_spawn`. We test both the default which should use `posix_spawn` +/// on supported platforms, and using `pre_exec` to force spawn using `exec`. +mod fd_impls { + use super::{assert_spawn_method, md_file_id}; + use crate::fs; + use crate::io::{self, Write}; + use crate::os::fd::AsRawFd; + use crate::os::unix::process::CommandExt; + use crate::process::{Command, Stdio}; + + /// Check setting the child's stdin via `.fd`. + pub fn test_stdin(use_exec: bool) { + let (pipe_reader, mut pipe_writer) = io::pipe().unwrap(); + + let fd_num = libc::STDIN_FILENO; + + let mut cmd = Command::new("cat"); + cmd.stdout(Stdio::piped()).fd(fd_num, pipe_reader); + + if use_exec { + unsafe { + cmd.pre_exec(|| Ok(())); + } + } + + let mut child = cmd.spawn().unwrap(); + let mut stdout = child.stdout.take().unwrap(); + + assert_spawn_method(&cmd, use_exec); + + pipe_writer.write_all(b"Hello, world!").unwrap(); + drop(pipe_writer); + + child.wait().unwrap().exit_ok().unwrap(); + assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello, world!"); + } + + /// Check that the last `.fd` mapping is preserved when there are conflicts. + pub fn test_swap(use_exec: bool) { + let (pipe_reader1, mut pipe_writer1) = io::pipe().unwrap(); + let (pipe_reader2, mut pipe_writer2) = io::pipe().unwrap(); + + let num1 = pipe_reader1.as_raw_fd(); + let num2 = pipe_reader2.as_raw_fd(); + + let mut cmd = Command::new("cat"); + cmd.arg(format!("/dev/fd/{num1}")) + .arg(format!("/dev/fd/{num2}")) + .stdout(Stdio::piped()) + .fd(num2, pipe_reader1) + .fd(num1, pipe_reader2); + + if use_exec { + unsafe { + cmd.pre_exec(|| Ok(())); + } + } + + pipe_writer1.write_all(b"Hello from pipe 1!").unwrap(); + drop(pipe_writer1); + + pipe_writer2.write_all(b"Hello from pipe 2!").unwrap(); + drop(pipe_writer2); + + let mut child = cmd.spawn().unwrap(); + let mut stdout = child.stdout.take().unwrap(); + + assert_spawn_method(&cmd, use_exec); + + child.wait().unwrap().exit_ok().unwrap(); + // the second pipe's output is clobbered; this is expected. + assert_eq!(io::read_to_string(&mut stdout).unwrap(), "Hello from pipe 1!"); + } + + // ensure that the fd is properly closed in the parent, but only after the child is spawned. + pub fn test_close_time(use_exec: bool) { + let (_pipe_reader, pipe_writer) = io::pipe().unwrap(); + + let fd = pipe_writer.as_raw_fd(); + let fd_path = format!("/dev/fd/{fd}"); + + let mut cmd = Command::new("true"); + cmd.fd(123, pipe_writer); + + if use_exec { + unsafe { + cmd.pre_exec(|| Ok(())); + } + } + + // Get the identifier of the fd (metadata follows symlinks) + let fd_id = md_file_id(&fs::metadata(&fd_path).expect("fd should be open")); + + cmd.spawn().unwrap().wait().unwrap().exit_ok().unwrap(); + + assert_spawn_method(&cmd, use_exec); + + // After the child is spawned, our fd should be closed + match fs::metadata(&fd_path) { + // Ok; fd exists but points to a different file + Ok(md) => assert_ne!(md_file_id(&md), fd_id), + // Ok; fd does not exist + Err(_) => (), + } + } +} + +#[test] +fn fd_test_stdin() { + fd_impls::test_stdin(false); + fd_impls::test_stdin(true); +} + +// FIXME: fails on android +#[cfg_attr(not(target_os = "android"), should_panic)] +#[test] +fn fd_test_swap() { + fd_impls::test_swap(false); + fd_impls::test_swap(true); +} + +#[test] +fn fd_test_close_time() { + fd_impls::test_close_time(false); + fd_impls::test_close_time(true); +} + +#[test] +fn test_fd_child_status_conflict() { + // Test based on + // https://github.com/rust-lang/rust/pull/145687/changes/cda572f6ba69#r2896721202 + + // Open a bunch of FDs that we can pass to the child + let (pipe_reader, _pipe_writer) = io::pipe().unwrap(); + let mut pipes = Vec::new(); + let mut pipe_fds = HashSet::new(); + for _ in 0..100 { + let pipe = pipe_reader.try_clone().unwrap(); + pipe_fds.insert(pipe.as_raw_fd()); + pipes.push(pipe); + } + pipe_fds.insert(pipe_reader.as_raw_fd()); + pipes.push(pipe_reader); + + // Prepare a command that will never succeed, and force fork+exec + let mut cmd = Command::new("I don't exist!"); + unsafe { cmd.pre_exec(|| Ok(())) }; + + // Pass the child our dummy FDs, using all FD numbers that may be available as + // the destination. + let mut next_dst_fd = 0; + for pipe in pipes { + while pipe_fds.contains(&next_dst_fd) { + next_dst_fd += 1; + } + cmd.fd(next_dst_fd, pipe); + next_dst_fd += 1; + } + + // The hope is that at this point, one of our `.fd()`s has a `new_fd` happens to + // overlap with what gets picked for our pipe/socket/pidfd to communicate child + // status back. If so and we don't account for this, the child's pipe to the parent + // will get clobbered and the failure status won't be reported back. + + assert!(cmd.spawn().is_err()); +} + +#[track_caller] +fn assert_spawn_method(cmd: &Command, use_exec: bool) { + let used_posix_spawn = cmd.as_inner().get_last_spawn_was_posix_spawn().unwrap(); + if use_exec { + assert!(!used_posix_spawn, "posix_spawn used but exec was expected"); + } else if cfg!(any( + target_os = "freebsd", + target_os = "illumos", + all(target_os = "linux", target_env = "gnu"), + all(target_os = "linux", target_env = "musl"), + target_os = "nto", + target_vendor = "apple", + target_os = "cygwin", + )) { + assert!(used_posix_spawn, "platform supports posix_spawn but it wasn't used"); + } else { + assert!(!used_posix_spawn); + } +}