| // Copyright 2021, The Android Open Source Project | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| //! A library for passing arbitrary file descriptors when spawning child processes. | |
| //! | |
| //! # Example | |
| //! | |
| //! ```rust | |
| //! use command_fds::{CommandFdExt, FdMapping}; | |
| //! use std::fs::File; | |
| //! use std::io::stdin; | |
| //! use std::os::fd::AsFd; | |
| //! use std::os::unix::io::AsRawFd; | |
| //! use std::process::Command; | |
| //! | |
| //! // Open a file. | |
| //! let file = File::open("Cargo.toml").unwrap(); | |
| //! | |
| //! // Prepare to run `ls -l /proc/self/fd` with some FDs mapped. | |
| //! let mut command = Command::new("ls"); | |
| //! command.arg("-l").arg("/proc/self/fd"); | |
| //! command | |
| //! .fd_mappings(vec![ | |
| //! // Map `file` as FD 3 in the child process. | |
| //! FdMapping { | |
| //! parent_fd: file.into(), | |
| //! child_fd: 3, | |
| //! }, | |
| //! // Map this process's stdin as FD 5 in the child process. | |
| //! FdMapping { | |
| //! parent_fd: stdin().as_fd().try_clone_to_owned().unwrap(), | |
| //! child_fd: 5, | |
| //! }, | |
| //! ]) | |
| //! .unwrap(); | |
| //! | |
| //! // Spawn the child process. | |
| //! let mut child = command.spawn().unwrap(); | |
| //! child.wait().unwrap(); | |
| //! ``` | |
| #[cfg(feature = "tokio")] | |
| pub mod tokio; | |
| use nix::fcntl::{fcntl, FcntlArg, FdFlag}; | |
| use nix::unistd::dup2; | |
| use std::cmp::max; | |
| use std::io; | |
| use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; | |
| use std::os::unix::io::RawFd; | |
| use std::os::unix::process::CommandExt; | |
| use std::process::Command; | |
| use thiserror::Error; | |
| /// A mapping from a file descriptor in the parent to a file descriptor in the child, to be applied | |
| /// when spawning a child process. | |
| /// | |
| /// This takes ownership of the `parent_fd` to ensure that it is kept open until after the child is | |
| /// spawned. | |
| #[derive(Debug)] | |
| pub struct FdMapping { | |
| pub parent_fd: OwnedFd, | |
| pub child_fd: RawFd, | |
| } | |
| /// Error setting up FD mappings, because there were two or more mappings for the same child FD. | |
| #[derive(Copy, Clone, Debug, Eq, Error, PartialEq)] | |
| #[error("Two or more mappings for the same child FD")] | |
| pub struct FdMappingCollision; | |
| /// Extension to add file descriptor mappings to a [`Command`]. | |
| pub trait CommandFdExt { | |
| /// Adds the given set of file descriptors to the command. | |
| /// | |
| /// Warning: Calling this more than once on the same command may result in unexpected behaviour. | |
| /// In particular, it is not possible to check that two mappings applied separately don't use | |
| /// the same `child_fd`. If there is such a collision then one will apply and the other will be | |
| /// lost. | |
| /// | |
| /// Note that the `Command` takes ownership of the file descriptors, which means that they won't | |
| /// be closed in the parent process until the `Command` is dropped. | |
| fn fd_mappings(&mut self, mappings: Vec<FdMapping>) -> Result<&mut Self, FdMappingCollision>; | |
| /// Adds the given set of file descriptors to be passed on to the child process when the command | |
| /// is run. | |
| /// | |
| /// Note that the `Command` takes ownership of the file descriptors, which means that they won't | |
| /// be closed in the parent process until the `Command` is dropped. | |
| fn preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self; | |
| } | |
| impl CommandFdExt for Command { | |
| fn fd_mappings( | |
| &mut self, | |
| mut mappings: Vec<FdMapping>, | |
| ) -> Result<&mut Self, FdMappingCollision> { | |
| let child_fds = validate_child_fds(&mappings)?; | |
| // Register the callback to apply the mappings after forking but before execing. | |
| // Safety: `map_fds` will not allocate, so it is safe to call from this hook. | |
| unsafe { | |
| // If the command is run more than once, the closure will be called multiple times but | |
| // in different forked processes, which will have different copies of `mappings`. So | |
| // their changes to it shouldn't be visible to each other. | |
| self.pre_exec(move || map_fds(&mut mappings, &child_fds)); | |
| } | |
| Ok(self) | |
| } | |
| fn preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self { | |
| unsafe { | |
| self.pre_exec(move || preserve_fds(&fds)); | |
| } | |
| self | |
| } | |
| } | |
| /// Validates that there are no conflicting mappings to the same child FD. | |
| fn validate_child_fds(mappings: &[FdMapping]) -> Result<Vec<RawFd>, FdMappingCollision> { | |
| let mut child_fds: Vec<RawFd> = mappings.iter().map(|mapping| mapping.child_fd).collect(); | |
| child_fds.sort_unstable(); | |
| child_fds.dedup(); | |
| if child_fds.len() != mappings.len() { | |
| return Err(FdMappingCollision); | |
| } | |
| Ok(child_fds) | |
| } | |
| // This function must not do any allocation, as it is called from the pre_exec hook. | |
| fn map_fds(mappings: &mut [FdMapping], child_fds: &[RawFd]) -> io::Result<()> { | |
| if mappings.is_empty() { | |
| // No need to do anything, and finding first_unused_fd would fail. | |
| return Ok(()); | |
| } | |
| // Find the first FD which is higher than any parent or child FD in the mapping, so we can | |
| // safely use it and higher FDs as temporary FDs. There may be other files open with these FDs, | |
| // so we still need to ensure we don't conflict with them. | |
| let first_safe_fd = mappings | |
| .iter() | |
| .map(|mapping| max(mapping.parent_fd.as_raw_fd(), mapping.child_fd)) | |
| .max() | |
| .unwrap() | |
| + 1; | |
| // If any parent FDs conflict with child FDs, then first duplicate them to a temporary FD which | |
| // is clear of either range. Mappings to the same FD are fine though, we can handle them by just | |
| // removing the FD_CLOEXEC flag from the existing (parent) FD. | |
| for mapping in mappings.iter_mut() { | |
| if child_fds.contains(&mapping.parent_fd.as_raw_fd()) | |
| && mapping.parent_fd.as_raw_fd() != mapping.child_fd | |
| { | |
| let parent_fd = fcntl( | |
| mapping.parent_fd.as_raw_fd(), | |
| FcntlArg::F_DUPFD_CLOEXEC(first_safe_fd), | |
| )?; | |
| // SAFETY: We just created `parent_fd` so we can take ownership of it. | |
| unsafe { | |
| mapping.parent_fd = OwnedFd::from_raw_fd(parent_fd); | |
| } | |
| } | |
| } | |
| // Now we can actually duplicate FDs to the desired child FDs. | |
| for mapping in mappings { | |
| if mapping.child_fd == mapping.parent_fd.as_raw_fd() { | |
| // Remove the FD_CLOEXEC flag, so the FD will be kept open when exec is called for the | |
| // child. | |
| fcntl( | |
| mapping.parent_fd.as_raw_fd(), | |
| FcntlArg::F_SETFD(FdFlag::empty()), | |
| )?; | |
| } else { | |
| // This closes child_fd if it is already open as something else, and clears the | |
| // FD_CLOEXEC flag on child_fd. | |
| dup2(mapping.parent_fd.as_raw_fd(), mapping.child_fd)?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| fn preserve_fds(fds: &[OwnedFd]) -> io::Result<()> { | |
| for fd in fds { | |
| // Remove the FD_CLOEXEC flag, so the FD will be kept open when exec is called for the | |
| // child. | |
| fcntl(fd.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::empty()))?; | |
| } | |
| Ok(()) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use nix::unistd::close; | |
| use std::collections::HashSet; | |
| use std::fs::{read_dir, File}; | |
| use std::os::unix::io::AsRawFd; | |
| use std::process::Output; | |
| use std::str; | |
| use std::sync::Once; | |
| static SETUP: Once = Once::new(); | |
| #[test] | |
| fn conflicting_mappings() { | |
| setup(); | |
| let mut command = Command::new("ls"); | |
| let file1 = File::open("testdata/file1.txt").unwrap(); | |
| let file2 = File::open("testdata/file2.txt").unwrap(); | |
| // Mapping two different FDs to the same FD isn't allowed. | |
| assert!(command | |
| .fd_mappings(vec![ | |
| FdMapping { | |
| child_fd: 4, | |
| parent_fd: file1.into(), | |
| }, | |
| FdMapping { | |
| child_fd: 4, | |
| parent_fd: file2.into(), | |
| }, | |
| ]) | |
| .is_err()); | |
| } | |
| #[test] | |
| fn no_mappings() { | |
| setup(); | |
| let mut command = Command::new("ls"); | |
| command.arg("/proc/self/fd"); | |
| assert!(command.fd_mappings(vec![]).is_ok()); | |
| let output = command.output().unwrap(); | |
| expect_fds(&output, &[0, 1, 2, 3], 0); | |
| } | |
| #[test] | |
| fn none_preserved() { | |
| setup(); | |
| let mut command = Command::new("ls"); | |
| command.arg("/proc/self/fd"); | |
| command.preserved_fds(vec![]); | |
| let output = command.output().unwrap(); | |
| expect_fds(&output, &[0, 1, 2, 3], 0); | |
| } | |
| #[test] | |
| fn one_mapping() { | |
| setup(); | |
| let mut command = Command::new("ls"); | |
| command.arg("/proc/self/fd"); | |
| let file = File::open("testdata/file1.txt").unwrap(); | |
| // Map the file an otherwise unused FD. | |
| assert!(command | |
| .fd_mappings(vec![FdMapping { | |
| parent_fd: file.into(), | |
| child_fd: 5, | |
| },]) | |
| .is_ok()); | |
| let output = command.output().unwrap(); | |
| expect_fds(&output, &[0, 1, 2, 3, 5], 0); | |
| } | |
| #[test] | |
| #[ignore = "flaky on GitHub"] | |
| fn one_preserved() { | |
| setup(); | |
| let mut command = Command::new("ls"); | |
| command.arg("/proc/self/fd"); | |
| let file = File::open("testdata/file1.txt").unwrap(); | |
| let file_fd: OwnedFd = file.into(); | |
| let raw_file_fd = file_fd.as_raw_fd(); | |
| assert!(raw_file_fd > 3); | |
| command.preserved_fds(vec![file_fd]); | |
| let output = command.output().unwrap(); | |
| expect_fds(&output, &[0, 1, 2, 3, raw_file_fd], 0); | |
| } | |
| #[test] | |
| fn swap_mappings() { | |
| setup(); | |
| let mut command = Command::new("ls"); | |
| command.arg("/proc/self/fd"); | |
| let file1 = File::open("testdata/file1.txt").unwrap(); | |
| let file2 = File::open("testdata/file2.txt").unwrap(); | |
| let fd1: OwnedFd = file1.into(); | |
| let fd2: OwnedFd = file2.into(); | |
| let fd1_raw = fd1.as_raw_fd(); | |
| let fd2_raw = fd2.as_raw_fd(); | |
| // Map files to each other's FDs, to ensure that the temporary FD logic works. | |
| assert!(command | |
| .fd_mappings(vec![ | |
| FdMapping { | |
| parent_fd: fd1, | |
| child_fd: fd2_raw, | |
| }, | |
| FdMapping { | |
| parent_fd: fd2, | |
| child_fd: fd1_raw, | |
| }, | |
| ]) | |
| .is_ok(),); | |
| let output = command.output().unwrap(); | |
| // Expect one more Fd for the /proc/self/fd directory. We can't predict what number it will | |
| // be assigned, because 3 might or might not be taken already by fd1 or fd2. | |
| expect_fds(&output, &[0, 1, 2, fd1_raw, fd2_raw], 1); | |
| } | |
| #[test] | |
| fn one_to_one_mapping() { | |
| setup(); | |
| let mut command = Command::new("ls"); | |
| command.arg("/proc/self/fd"); | |
| let file1 = File::open("testdata/file1.txt").unwrap(); | |
| let file2 = File::open("testdata/file2.txt").unwrap(); | |
| let fd1: OwnedFd = file1.into(); | |
| let fd1_raw = fd1.as_raw_fd(); | |
| // Map file1 to the same FD it currently has, to ensure the special case for that works. | |
| assert!(command | |
| .fd_mappings(vec![FdMapping { | |
| parent_fd: fd1, | |
| child_fd: fd1_raw, | |
| }]) | |
| .is_ok()); | |
| let output = command.output().unwrap(); | |
| // Expect one more Fd for the /proc/self/fd directory. We can't predict what number it will | |
| // be assigned, because 3 might or might not be taken already by fd1 or fd2. | |
| expect_fds(&output, &[0, 1, 2, fd1_raw], 1); | |
| // Keep file2 open until the end, to ensure that it's not passed to the child. | |
| drop(file2); | |
| } | |
| #[test] | |
| fn map_stdin() { | |
| setup(); | |
| let mut command = Command::new("cat"); | |
| let file = File::open("testdata/file1.txt").unwrap(); | |
| // Map the file to stdin. | |
| assert!(command | |
| .fd_mappings(vec![FdMapping { | |
| parent_fd: file.into(), | |
| child_fd: 0, | |
| },]) | |
| .is_ok()); | |
| let output = command.output().unwrap(); | |
| assert!(output.status.success()); | |
| assert_eq!(output.stdout, b"test 1"); | |
| } | |
| /// Parse the output of ls into a set of filenames | |
| fn parse_ls_output(output: &[u8]) -> HashSet<String> { | |
| str::from_utf8(output) | |
| .unwrap() | |
| .split_terminator("\n") | |
| .map(str::to_owned) | |
| .collect() | |
| } | |
| /// Check that the output of `ls /proc/self/fd` contains the expected set of FDs, plus exactly | |
| /// `extra` extra FDs. | |
| fn expect_fds(output: &Output, expected_fds: &[RawFd], extra: usize) { | |
| assert!(output.status.success()); | |
| let expected_fds: HashSet<String> = expected_fds.iter().map(RawFd::to_string).collect(); | |
| let fds = parse_ls_output(&output.stdout); | |
| if extra == 0 { | |
| assert_eq!(fds, expected_fds); | |
| } else { | |
| assert!(expected_fds.is_subset(&fds)); | |
| assert_eq!(fds.len(), expected_fds.len() + extra); | |
| } | |
| } | |
| fn setup() { | |
| SETUP.call_once(close_excess_fds); | |
| } | |
| /// Close all file descriptors apart from stdin, stdout and stderr. | |
| /// | |
| /// This is necessary because GitHub Actions opens a bunch of others for some reason. | |
| fn close_excess_fds() { | |
| let dir = read_dir("/proc/self/fd").unwrap(); | |
| for entry in dir { | |
| let entry = entry.unwrap(); | |
| let fd: RawFd = entry.file_name().to_str().unwrap().parse().unwrap(); | |
| if fd > 3 { | |
| close(fd).unwrap(); | |
| } | |
| } | |
| } | |
| } |