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