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.
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")]
54pub mod tokio;
55
56use nix::fcntl::{FcntlArg, FdFlag, fcntl};
57use nix::unistd::dup2_raw;
58use std::cmp::max;
59use std::io;
60use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd};
61use std::os::unix::io::RawFd;
62use std::os::unix::process::CommandExt;
63use std::process::Command;
64use 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)]
72pub 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")]
80pub struct FdMappingCollision;
81
82/// Extension to add file descriptor mappings to a [`Command`].
83pub 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.
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.
100 fn preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self;
101}
102
103impl CommandFdExt for Command {
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
122 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.
132fn 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.
143fn 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(&mapping.parent_fd, FcntlArg::F_DUPFD_CLOEXEC(first_safe_fd))?;
167 // SAFETY: We just created `parent_fd` so we can take ownership of it.
168 unsafe {
169 mapping.parent_fd = OwnedFd::from_raw_fd(parent_fd);
170 }
171 }
172 }
173
174 // Now we can actually duplicate FDs to the desired child FDs.
175 for mapping in mappings {
176 if mapping.child_fd == mapping.parent_fd.as_raw_fd() {
177 // Remove the FD_CLOEXEC flag, so the FD will be kept open when exec is called for the
178 // child.
179 fcntl(&mapping.parent_fd, FcntlArg::F_SETFD(FdFlag::empty()))?;
180 } else {
181 // This closes child_fd if it is already open as something else, and clears the
182 // FD_CLOEXEC flag on child_fd.
183 // SAFETY: `dup2_raw` returns an `OwnedFd` which takes ownership of the child FD and
184 // would close it when it is dropped. We avoid this by calling `into_raw_fd` to give up
185 // ownership again.
186 unsafe {
187 let _ = dup2_raw(&mapping.parent_fd, mapping.child_fd)?.into_raw_fd();
188 }
189 }
190 }
191
192 Ok(())
193}
194
195fn preserve_fds(fds: &[OwnedFd]) -> io::Result<()> {
196 for fd in fds {
197 // Remove the FD_CLOEXEC flag, so the FD will be kept open when exec is called for the
198 // child.
199 fcntl(fd, FcntlArg::F_SETFD(FdFlag::empty()))?;
200 }
201
202 Ok(())
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use nix::unistd::close;
209 use std::collections::HashSet;
210 use std::fs::{File, read_dir};
211 use std::os::unix::io::AsRawFd;
212 use std::process::Output;
213 use std::str;
214 use std::sync::Once;
215
216 static SETUP: Once = Once::new();
217
218 #[test]
219 fn conflicting_mappings() {
220 setup();
221
222 let mut command = Command::new("ls");
223
224 let file1 = File::open("testdata/file1.txt").unwrap();
225 let file2 = File::open("testdata/file2.txt").unwrap();
226
227 // Mapping two different FDs to the same FD isn't allowed.
228 assert!(
229 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
244 #[test]
245 fn no_mappings() {
246 setup();
247
248 let mut command = Command::new("ls");
249 command.arg("/proc/self/fd");
250
251 assert!(command.fd_mappings(vec![]).is_ok());
252
253 let output = command.output().unwrap();
254 expect_fds(&output, &[0, 1, 2, 3], 0);
255 }
256
257 #[test]
258 fn none_preserved() {
259 setup();
260
261 let mut command = Command::new("ls");
262 command.arg("/proc/self/fd");
263
264 command.preserved_fds(vec![]);
265
266 let output = command.output().unwrap();
267 expect_fds(&output, &[0, 1, 2, 3], 0);
268 }
269
270 #[test]
271 fn one_mapping() {
272 setup();
273
274 let mut command = Command::new("ls");
275 command.arg("/proc/self/fd");
276
277 let file = File::open("testdata/file1.txt").unwrap();
278 // Map the file an otherwise unused FD.
279 assert!(
280 command
281 .fd_mappings(vec![FdMapping {
282 parent_fd: file.into(),
283 child_fd: 5,
284 },])
285 .is_ok()
286 );
287
288 let output = command.output().unwrap();
289 expect_fds(&output, &[0, 1, 2, 3, 5], 0);
290 }
291
292 #[test]
293 #[ignore = "flaky on GitHub"]
294 fn one_preserved() {
295 setup();
296
297 let mut command = Command::new("ls");
298 command.arg("/proc/self/fd");
299
300 let file = File::open("testdata/file1.txt").unwrap();
301 let file_fd: OwnedFd = file.into();
302 let raw_file_fd = file_fd.as_raw_fd();
303 assert!(raw_file_fd > 3);
304 command.preserved_fds(vec![file_fd]);
305
306 let output = command.output().unwrap();
307 expect_fds(&output, &[0, 1, 2, 3, raw_file_fd], 0);
308 }
309
310 #[test]
311 fn swap_mappings() {
312 setup();
313
314 let mut command = Command::new("ls");
315 command.arg("/proc/self/fd");
316
317 let file1 = File::open("testdata/file1.txt").unwrap();
318 let file2 = File::open("testdata/file2.txt").unwrap();
319 let fd1: OwnedFd = file1.into();
320 let fd2: OwnedFd = file2.into();
321 let fd1_raw = fd1.as_raw_fd();
322 let fd2_raw = fd2.as_raw_fd();
323 // Map files to each other's FDs, to ensure that the temporary FD logic works.
324 assert!(
325 command
326 .fd_mappings(vec![
327 FdMapping {
328 parent_fd: fd1,
329 child_fd: fd2_raw,
330 },
331 FdMapping {
332 parent_fd: fd2,
333 child_fd: fd1_raw,
334 },
335 ])
336 .is_ok(),
337 );
338
339 let output = command.output().unwrap();
340 // Expect one more Fd for the /proc/self/fd directory. We can't predict what number it will
341 // be assigned, because 3 might or might not be taken already by fd1 or fd2.
342 expect_fds(&output, &[0, 1, 2, fd1_raw, fd2_raw], 1);
343 }
344
345 #[test]
346 fn one_to_one_mapping() {
347 setup();
348
349 let mut command = Command::new("ls");
350 command.arg("/proc/self/fd");
351
352 let file1 = File::open("testdata/file1.txt").unwrap();
353 let file2 = File::open("testdata/file2.txt").unwrap();
354 let fd1: OwnedFd = file1.into();
355 let fd1_raw = fd1.as_raw_fd();
356 // Map file1 to the same FD it currently has, to ensure the special case for that works.
357 assert!(
358 command
359 .fd_mappings(vec![FdMapping {
360 parent_fd: fd1,
361 child_fd: fd1_raw,
362 }])
363 .is_ok()
364 );
365
366 let output = command.output().unwrap();
367 // Expect one more Fd for the /proc/self/fd directory. We can't predict what number it will
368 // be assigned, because 3 might or might not be taken already by fd1 or fd2.
369 expect_fds(&output, &[0, 1, 2, fd1_raw], 1);
370
371 // Keep file2 open until the end, to ensure that it's not passed to the child.
372 drop(file2);
373 }
374
375 #[test]
376 fn map_stdin() {
377 setup();
378
379 let mut command = Command::new("cat");
380
381 let file = File::open("testdata/file1.txt").unwrap();
382 // Map the file to stdin.
383 assert!(
384 command
385 .fd_mappings(vec![FdMapping {
386 parent_fd: file.into(),
387 child_fd: 0,
388 },])
389 .is_ok()
390 );
391
392 let output = command.output().unwrap();
393 assert!(output.status.success());
394 assert_eq!(output.stdout, b"test 1");
395 }
396
397 /// Parse the output of ls into a set of filenames
398 fn parse_ls_output(output: &[u8]) -> HashSet<String> {
399 str::from_utf8(output)
400 .unwrap()
401 .split_terminator("\n")
402 .map(str::to_owned)
403 .collect()
404 }
405
406 /// Check that the output of `ls /proc/self/fd` contains the expected set of FDs, plus exactly
407 /// `extra` extra FDs.
408 fn expect_fds(output: &Output, expected_fds: &[RawFd], extra: usize) {
409 assert!(output.status.success());
410 let expected_fds: HashSet<String> = expected_fds.iter().map(RawFd::to_string).collect();
411 let fds = parse_ls_output(&output.stdout);
412 if extra == 0 {
413 assert_eq!(fds, expected_fds);
414 } else {
415 assert!(expected_fds.is_subset(&fds));
416 assert_eq!(fds.len(), expected_fds.len() + extra);
417 }
418 }
419
420 fn setup() {
421 SETUP.call_once(close_excess_fds);
422 }
423
424 /// Close all file descriptors apart from stdin, stdout and stderr.
425 ///
426 /// This is necessary because GitHub Actions opens a bunch of others for some reason.
427 fn close_excess_fds() {
428 let dir = read_dir("/proc/self/fd").unwrap();
429 for entry in dir {
430 let entry = entry.unwrap();
431 let fd: RawFd = entry.file_name().to_str().unwrap().parse().unwrap();
432 if fd > 3 {
433 close(fd).unwrap();
434 }
435 }
436 }
437}