• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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")]
54 pub mod tokio;
55 
56 use nix::fcntl::{fcntl, FcntlArg, FdFlag};
57 use nix::unistd::dup2;
58 use std::cmp::max;
59 use std::io;
60 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
61 use std::os::unix::io::RawFd;
62 use std::os::unix::process::CommandExt;
63 use std::process::Command;
64 use 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)]
72 pub 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")]
80 pub struct FdMappingCollision;
81 
82 /// Extension to add file descriptor mappings to a [`Command`].
83 pub 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.
fd_mappings(&mut self, mappings: Vec<FdMapping>) -> Result<&mut Self, FdMappingCollision>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.
preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self100     fn preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self;
101 }
102 
103 impl CommandFdExt for Command {
fd_mappings( &mut self, mut mappings: Vec<FdMapping>, ) -> Result<&mut Self, FdMappingCollision>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 
preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self122     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.
validate_child_fds(mappings: &[FdMapping]) -> Result<Vec<RawFd>, FdMappingCollision>132 fn 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.
map_fds(mappings: &mut [FdMapping], child_fds: &[RawFd]) -> io::Result<()>143 fn 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 
preserve_fds(fds: &[OwnedFd]) -> io::Result<()>196 fn 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)]
207 mod 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]
conflicting_mappings()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]
no_mappings()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]
none_preserved()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]
one_mapping()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"]
one_preserved()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]
swap_mappings()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]
one_to_one_mapping()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]
map_stdin()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
parse_ls_output(output: &[u8]) -> HashSet<String>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.
expect_fds(output: &Output, expected_fds: &[RawFd], extra: usize)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 
setup()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.
close_excess_fds()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 }
429