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