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
29#[allow(unsafe_code)]
30impl CapStdExtCommandExt for std::process::Command {
31    fn take_fd_n(&mut self, fd: Arc<OwnedFd>, target: i32) -> &mut Self {
32        unsafe {
33            self.pre_exec(move || {
34                let mut target = OwnedFd::from_raw_fd(target);
35                // If the fd is already what we want, then just ensure that
36                // O_CLOEXEC is stripped off.
37                if target.as_raw_fd() == fd.as_raw_fd() {
38                    let fl = rustix::io::fcntl_getfd(&target)?;
39                    rustix::io::fcntl_setfd(&mut target, fl.difference(FdFlags::CLOEXEC))?;
40                } else {
41                    // Otherwise create a dup, which will also default to not setting O_CLOEXEC.
42                    rustix::io::dup2(&*fd, &mut target)?;
43                }
44                // Intentionally leak into the child.
45                let _ = target.into_raw_fd();
46                Ok(())
47            });
48        }
49        self
50    }
51
52    fn cwd_dir(&mut self, dir: Dir) -> &mut Self {
53        unsafe {
54            self.pre_exec(move || {
55                rustix::process::fchdir(dir.as_fd())?;
56                Ok(())
57            });
58        }
59        self
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use std::sync::Arc;
67
68    #[test]
69    fn test_take_fdn() -> anyhow::Result<()> {
70        // Pass srcfd == destfd and srcfd != destfd
71        for i in 0..=1 {
72            let tempd = cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
73            let tempd_fd = Arc::new(tempd.as_fd().try_clone_to_owned()?);
74            let n = tempd_fd.as_raw_fd() + i;
75            let st = std::process::Command::new("ls")
76                .arg(format!("/proc/self/fd/{n}"))
77                .take_fd_n(tempd_fd, n)
78                .status()?;
79            assert!(st.success());
80        }
81        Ok(())
82    }
83}