cap_std_ext/
cmdext.rs

1//! Extensions for [`std::process::Command`] that operate on concepts from cap-std.
2//!
3//! The key APIs here are:
4//!
5//! - File descriptor passing
6//! - Changing to a file-descriptor relative directory
7
8use cap_std::fs::Dir;
9use cap_std::io_lifetimes;
10use cap_tempfile::cap_std;
11use io_lifetimes::OwnedFd;
12use rustix::fd::{AsFd, FromRawFd, IntoRawFd};
13use rustix::io::FdFlags;
14use std::os::fd::AsRawFd;
15use std::os::unix::process::CommandExt;
16use std::sync::Arc;
17
18/// Extension trait for [`std::process::Command`].
19///
20/// [`cap_std::fs::Dir`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.Dir.html
21pub trait CapStdExtCommandExt {
22    /// Pass a file descriptor into the target process.
23    fn take_fd_n(&mut self, fd: Arc<OwnedFd>, target: i32) -> &mut Self;
24
25    /// Use the given directory as the current working directory for the process.
26    fn cwd_dir(&mut self, dir: Dir) -> &mut Self;
27
28    /// On Linux, arrange for [`SIGTERM`] to be delivered to the child if the
29    /// parent *thread* exits. This helps avoid leaking child processes if
30    /// the parent crashes for example.
31    ///
32    /// # IMPORTANT
33    ///
34    /// Due to the semantics of <https://man7.org/linux/man-pages/man2/prctl.2.html> this
35    /// will cause the child to exit when the parent *thread* (not process) exits. In
36    /// particular this can become problematic when used with e.g. a threadpool such
37    /// as Tokio's <https://kobzol.github.io/rust/2025/02/23/tokio-plus-prctl-equals-nasty-bug.html>.
38    #[cfg(any(target_os = "linux", target_os = "android"))]
39    fn lifecycle_bind_to_parent_thread(&mut self) -> &mut Self;
40}
41
42#[allow(unsafe_code)]
43impl CapStdExtCommandExt for std::process::Command {
44    fn take_fd_n(&mut self, fd: Arc<OwnedFd>, target: i32) -> &mut Self {
45        unsafe {
46            self.pre_exec(move || {
47                let mut target = OwnedFd::from_raw_fd(target);
48                // If the fd is already what we want, then just ensure that
49                // O_CLOEXEC is stripped off.
50                if target.as_raw_fd() == fd.as_raw_fd() {
51                    let fl = rustix::io::fcntl_getfd(&target)?;
52                    rustix::io::fcntl_setfd(&mut target, fl.difference(FdFlags::CLOEXEC))?;
53                } else {
54                    // Otherwise create a dup, which will also default to not setting O_CLOEXEC.
55                    rustix::io::dup2(&*fd, &mut target)?;
56                }
57                // Intentionally leak into the child.
58                let _ = target.into_raw_fd();
59                Ok(())
60            });
61        }
62        self
63    }
64
65    fn cwd_dir(&mut self, dir: Dir) -> &mut Self {
66        unsafe {
67            self.pre_exec(move || {
68                rustix::process::fchdir(dir.as_fd())?;
69                Ok(())
70            });
71        }
72        self
73    }
74
75    #[cfg(any(target_os = "linux", target_os = "android"))]
76    fn lifecycle_bind_to_parent_thread(&mut self) -> &mut Self {
77        // SAFETY: This API is safe to call in a forked child.
78        unsafe {
79            self.pre_exec(|| {
80                rustix::process::set_parent_process_death_signal(Some(
81                    rustix::process::Signal::TERM,
82                ))
83                .map_err(Into::into)
84            });
85        }
86        self
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use std::sync::Arc;
94
95    #[test]
96    fn test_take_fdn() -> anyhow::Result<()> {
97        // Pass srcfd == destfd and srcfd != destfd
98        for i in 0..=1 {
99            let tempd = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
100            let tempd_fd = Arc::new(tempd.as_fd().try_clone_to_owned()?);
101            let n = tempd_fd.as_raw_fd() + i;
102            let st = std::process::Command::new("ls")
103                .arg(format!("/proc/self/fd/{n}"))
104                .take_fd_n(tempd_fd, n)
105                .status()?;
106            assert!(st.success());
107        }
108        Ok(())
109    }
110}