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}