Skip to main content

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