command_fds/
lib.rs

1// Copyright 2021, The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A library for passing arbitrary file descriptors when spawning child processes.
16//!
17//! # Example
18//!
19//! ```rust
20//! use command_fds::{CommandFdExt, FdMapping};
21//! use std::fs::File;
22//! use std::io::stdin;
23//! use std::os::fd::AsFd;
24//! use std::os::unix::io::AsRawFd;
25//! use std::process::Command;
26//!
27//! // Open a file.
28//! let file = File::open("Cargo.toml").unwrap();
29//!
30//! // Prepare to run `ls -l /proc/self/fd` with some FDs mapped.
31//! let mut command = Command::new("ls");
32//! command.arg("-l").arg("/proc/self/fd");
33//! command
34//!     .fd_mappings(vec![
35//!         // Map `file` as FD 3 in the child process.
36//!         FdMapping {
37//!             parent_fd: file.into(),
38//!             child_fd: 3,
39//!         },
40//!         // Map this process's stdin as FD 5 in the child process.
41//!         FdMapping {
42//!             parent_fd: stdin().as_fd().try_clone_to_owned().unwrap(),
43//!             child_fd: 5,
44//!         },
45//!     ])
46//!     .unwrap();
47//!
48//! // Spawn the child process.
49//! let mut child = command.spawn().unwrap();
50//! child.wait().unwrap();
51//! ```
52
53#[cfg(feature = "tokio")]
54pub mod tokio;
55
56use nix::fcntl::{fcntl, FcntlArg, FdFlag};
57use nix::unistd::dup2;
58use std::cmp::max;
59use std::io;
60use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
61use std::os::unix::io::RawFd;
62use std::os::unix::process::CommandExt;
63use std::process::Command;
64use thiserror::Error;
65
66/// A mapping from a file descriptor in the parent to a file descriptor in the child, to be applied
67/// when spawning a child process.
68///
69/// This takes ownership of the `parent_fd` to ensure that it is kept open until after the child is
70/// spawned.
71#[derive(Debug)]
72pub struct FdMapping {
73    pub parent_fd: OwnedFd,
74    pub child_fd: RawFd,
75}
76
77/// Error setting up FD mappings, because there were two or more mappings for the same child FD.
78#[derive(Copy, Clone, Debug, Eq, Error, PartialEq)]
79#[error("Two or more mappings for the same child FD")]
80pub struct FdMappingCollision;
81
82/// Extension to add file descriptor mappings to a [`Command`].
83pub trait CommandFdExt {
84    /// Adds the given set of file descriptors to the command.
85    ///
86    /// Warning: Calling this more than once on the same command may result in unexpected behaviour.
87    /// In particular, it is not possible to check that two mappings applied separately don't use
88    /// the same `child_fd`. If there is such a collision then one will apply and the other will be
89    /// lost.
90    ///
91    /// Note that the `Command` takes ownership of the file descriptors, which means that they won't
92    /// be closed in the parent process until the `Command` is dropped.
93    fn fd_mappings(&mut self, mappings: Vec<FdMapping>) -> Result<&mut Self, FdMappingCollision>;
94
95    /// Adds the given set of file descriptors to be passed on to the child process when the command
96    /// is run.
97    ///
98    /// Note that the `Command` takes ownership of the file descriptors, which means that they won't
99    /// be closed in the parent process until the `Command` is dropped.
100    fn preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self;
101}
102
103impl CommandFdExt for Command {
104    fn fd_mappings(
105        &mut self,
106        mut mappings: Vec<FdMapping>,
107    ) -> Result<&mut Self, FdMappingCollision> {
108        let child_fds = validate_child_fds(&mappings)?;
109
110        // Register the callback to apply the mappings after forking but before execing.
111        // Safety: `map_fds` will not allocate, so it is safe to call from this hook.
112        unsafe {
113            // If the command is run more than once, the closure will be called multiple times but
114            // in different forked processes, which will have different copies of `mappings`. So
115            // their changes to it shouldn't be visible to each other.
116            self.pre_exec(move || map_fds(&mut mappings, &child_fds));
117        }
118
119        Ok(self)
120    }
121
122    fn preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self {
123        unsafe {
124            self.pre_exec(move || preserve_fds(&fds));
125        }
126
127        self
128    }
129}
130
131/// Validates that there are no conflicting mappings to the same child FD.
132fn validate_child_fds(mappings: &[FdMapping]) -> Result<Vec<RawFd>, FdMappingCollision> {
133    let mut child_fds: Vec<RawFd> = mappings.iter().map(|mapping| mapping.child_fd).collect();
134    child_fds.sort_unstable();
135    child_fds.dedup();
136    if child_fds.len() != mappings.len() {
137        return Err(FdMappingCollision);
138    }
139    Ok(child_fds)
140}
141
142// This function must not do any allocation, as it is called from the pre_exec hook.
143fn map_fds(mappings: &mut [FdMapping], child_fds: &[RawFd]) -> io::Result<()> {
144    if mappings.is_empty() {
145        // No need to do anything, and finding first_unused_fd would fail.
146        return Ok(());
147    }
148
149    // Find the first FD which is higher than any parent or child FD in the mapping, so we can
150    // safely use it and higher FDs as temporary FDs. There may be other files open with these FDs,
151    // so we still need to ensure we don't conflict with them.
152    let first_safe_fd = mappings
153        .iter()
154        .map(|mapping| max(mapping.parent_fd.as_raw_fd(), mapping.child_fd))
155        .max()
156        .unwrap()
157        + 1;
158
159    // If any parent FDs conflict with child FDs, then first duplicate them to a temporary FD which
160    // is clear of either range. Mappings to the same FD are fine though, we can handle them by just
161    // removing the FD_CLOEXEC flag from the existing (parent) FD.
162    for mapping in mappings.iter_mut() {
163        if child_fds.contains(&mapping.parent_fd.as_raw_fd())
164            && mapping.parent_fd.as_raw_fd() != mapping.child_fd
165        {
166            let parent_fd = fcntl(
167                mapping.parent_fd.as_raw_fd(),
168                FcntlArg::F_DUPFD_CLOEXEC(first_safe_fd),
169            )?;
170            // SAFETY: We just created `parent_fd` so we can take ownership of it.
171            unsafe {
172                mapping.parent_fd = OwnedFd::from_raw_fd(parent_fd);
173            }
174        }
175    }
176
177    // Now we can actually duplicate FDs to the desired child FDs.
178    for mapping in mappings {
179        if mapping.child_fd == mapping.parent_fd.as_raw_fd() {
180            // Remove the FD_CLOEXEC flag, so the FD will be kept open when exec is called for the
181            // child.
182            fcntl(
183                mapping.parent_fd.as_raw_fd(),
184                FcntlArg::F_SETFD(FdFlag::empty()),
185            )?;
186        } else {
187            // This closes child_fd if it is already open as something else, and clears the
188            // FD_CLOEXEC flag on child_fd.
189            dup2(mapping.parent_fd.as_raw_fd(), mapping.child_fd)?;
190        }
191    }
192
193    Ok(())
194}
195
196fn preserve_fds(fds: &[OwnedFd]) -> io::Result<()> {
197    for fd in fds {
198        // Remove the FD_CLOEXEC flag, so the FD will be kept open when exec is called for the
199        // child.
200        fcntl(fd.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::empty()))?;
201    }
202
203    Ok(())
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use nix::unistd::close;
210    use std::collections::HashSet;
211    use std::fs::{read_dir, File};
212    use std::os::unix::io::AsRawFd;
213    use std::process::Output;
214    use std::str;
215    use std::sync::Once;
216
217    static SETUP: Once = Once::new();
218
219    #[test]
220    fn conflicting_mappings() {
221        setup();
222
223        let mut command = Command::new("ls");
224
225        let file1 = File::open("testdata/file1.txt").unwrap();
226        let file2 = File::open("testdata/file2.txt").unwrap();
227
228        // Mapping two different FDs to the same FD isn't allowed.
229        assert!(command
230            .fd_mappings(vec![
231                FdMapping {
232                    child_fd: 4,
233                    parent_fd: file1.into(),
234                },
235                FdMapping {
236                    child_fd: 4,
237                    parent_fd: file2.into(),
238                },
239            ])
240            .is_err());
241    }
242
243    #[test]
244    fn no_mappings() {
245        setup();
246
247        let mut command = Command::new("ls");
248        command.arg("/proc/self/fd");
249
250        assert!(command.fd_mappings(vec![]).is_ok());
251
252        let output = command.output().unwrap();
253        expect_fds(&output, &[0, 1, 2, 3], 0);
254    }
255
256    #[test]
257    fn none_preserved() {
258        setup();
259
260        let mut command = Command::new("ls");
261        command.arg("/proc/self/fd");
262
263        command.preserved_fds(vec![]);
264
265        let output = command.output().unwrap();
266        expect_fds(&output, &[0, 1, 2, 3], 0);
267    }
268
269    #[test]
270    fn one_mapping() {
271        setup();
272
273        let mut command = Command::new("ls");
274        command.arg("/proc/self/fd");
275
276        let file = File::open("testdata/file1.txt").unwrap();
277        // Map the file an otherwise unused FD.
278        assert!(command
279            .fd_mappings(vec![FdMapping {
280                parent_fd: file.into(),
281                child_fd: 5,
282            },])
283            .is_ok());
284
285        let output = command.output().unwrap();
286        expect_fds(&output, &[0, 1, 2, 3, 5], 0);
287    }
288
289    #[test]
290    #[ignore = "flaky on GitHub"]
291    fn one_preserved() {
292        setup();
293
294        let mut command = Command::new("ls");
295        command.arg("/proc/self/fd");
296
297        let file = File::open("testdata/file1.txt").unwrap();
298        let file_fd: OwnedFd = file.into();
299        let raw_file_fd = file_fd.as_raw_fd();
300        assert!(raw_file_fd > 3);
301        command.preserved_fds(vec![file_fd]);
302
303        let output = command.output().unwrap();
304        expect_fds(&output, &[0, 1, 2, 3, raw_file_fd], 0);
305    }
306
307    #[test]
308    fn swap_mappings() {
309        setup();
310
311        let mut command = Command::new("ls");
312        command.arg("/proc/self/fd");
313
314        let file1 = File::open("testdata/file1.txt").unwrap();
315        let file2 = File::open("testdata/file2.txt").unwrap();
316        let fd1: OwnedFd = file1.into();
317        let fd2: OwnedFd = file2.into();
318        let fd1_raw = fd1.as_raw_fd();
319        let fd2_raw = fd2.as_raw_fd();
320        // Map files to each other's FDs, to ensure that the temporary FD logic works.
321        assert!(command
322            .fd_mappings(vec![
323                FdMapping {
324                    parent_fd: fd1,
325                    child_fd: fd2_raw,
326                },
327                FdMapping {
328                    parent_fd: fd2,
329                    child_fd: fd1_raw,
330                },
331            ])
332            .is_ok(),);
333
334        let output = command.output().unwrap();
335        // Expect one more Fd for the /proc/self/fd directory. We can't predict what number it will
336        // be assigned, because 3 might or might not be taken already by fd1 or fd2.
337        expect_fds(&output, &[0, 1, 2, fd1_raw, fd2_raw], 1);
338    }
339
340    #[test]
341    fn one_to_one_mapping() {
342        setup();
343
344        let mut command = Command::new("ls");
345        command.arg("/proc/self/fd");
346
347        let file1 = File::open("testdata/file1.txt").unwrap();
348        let file2 = File::open("testdata/file2.txt").unwrap();
349        let fd1: OwnedFd = file1.into();
350        let fd1_raw = fd1.as_raw_fd();
351        // Map file1 to the same FD it currently has, to ensure the special case for that works.
352        assert!(command
353            .fd_mappings(vec![FdMapping {
354                parent_fd: fd1,
355                child_fd: fd1_raw,
356            }])
357            .is_ok());
358
359        let output = command.output().unwrap();
360        // Expect one more Fd for the /proc/self/fd directory. We can't predict what number it will
361        // be assigned, because 3 might or might not be taken already by fd1 or fd2.
362        expect_fds(&output, &[0, 1, 2, fd1_raw], 1);
363
364        // Keep file2 open until the end, to ensure that it's not passed to the child.
365        drop(file2);
366    }
367
368    #[test]
369    fn map_stdin() {
370        setup();
371
372        let mut command = Command::new("cat");
373
374        let file = File::open("testdata/file1.txt").unwrap();
375        // Map the file to stdin.
376        assert!(command
377            .fd_mappings(vec![FdMapping {
378                parent_fd: file.into(),
379                child_fd: 0,
380            },])
381            .is_ok());
382
383        let output = command.output().unwrap();
384        assert!(output.status.success());
385        assert_eq!(output.stdout, b"test 1");
386    }
387
388    /// Parse the output of ls into a set of filenames
389    fn parse_ls_output(output: &[u8]) -> HashSet<String> {
390        str::from_utf8(output)
391            .unwrap()
392            .split_terminator("\n")
393            .map(str::to_owned)
394            .collect()
395    }
396
397    /// Check that the output of `ls /proc/self/fd` contains the expected set of FDs, plus exactly
398    /// `extra` extra FDs.
399    fn expect_fds(output: &Output, expected_fds: &[RawFd], extra: usize) {
400        assert!(output.status.success());
401        let expected_fds: HashSet<String> = expected_fds.iter().map(RawFd::to_string).collect();
402        let fds = parse_ls_output(&output.stdout);
403        if extra == 0 {
404            assert_eq!(fds, expected_fds);
405        } else {
406            assert!(expected_fds.is_subset(&fds));
407            assert_eq!(fds.len(), expected_fds.len() + extra);
408        }
409    }
410
411    fn setup() {
412        SETUP.call_once(close_excess_fds);
413    }
414
415    /// Close all file descriptors apart from stdin, stdout and stderr.
416    ///
417    /// This is necessary because GitHub Actions opens a bunch of others for some reason.
418    fn close_excess_fds() {
419        let dir = read_dir("/proc/self/fd").unwrap();
420        for entry in dir {
421            let entry = entry.unwrap();
422            let fd: RawFd = entry.file_name().to_str().unwrap().parse().unwrap();
423            if fd > 3 {
424                close(fd).unwrap();
425            }
426        }
427    }
428}