/rust/registry/src/index.crates.io-1949cf8c6b5b557f/command-fds-0.3.0/src/lib.rs
Line | Count | Source |
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. |
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 | | |
103 | | impl CommandFdExt for Command { |
104 | 0 | fn fd_mappings( |
105 | 0 | &mut self, |
106 | 0 | mut mappings: Vec<FdMapping>, |
107 | 0 | ) -> Result<&mut Self, FdMappingCollision> { |
108 | 0 | 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 | 0 | self.pre_exec(move || map_fds(&mut mappings, &child_fds)); |
117 | | } |
118 | | |
119 | 0 | Ok(self) |
120 | 0 | } |
121 | | |
122 | 0 | fn preserved_fds(&mut self, fds: Vec<OwnedFd>) -> &mut Self { |
123 | | unsafe { |
124 | 0 | self.pre_exec(move || preserve_fds(&fds)); |
125 | | } |
126 | | |
127 | 0 | self |
128 | 0 | } |
129 | | } |
130 | | |
131 | | /// Validates that there are no conflicting mappings to the same child FD. |
132 | 0 | fn validate_child_fds(mappings: &[FdMapping]) -> Result<Vec<RawFd>, FdMappingCollision> { |
133 | 0 | let mut child_fds: Vec<RawFd> = mappings.iter().map(|mapping| mapping.child_fd).collect(); |
134 | 0 | child_fds.sort_unstable(); |
135 | 0 | child_fds.dedup(); |
136 | 0 | if child_fds.len() != mappings.len() { |
137 | 0 | return Err(FdMappingCollision); |
138 | 0 | } |
139 | 0 | Ok(child_fds) |
140 | 0 | } |
141 | | |
142 | | // This function must not do any allocation, as it is called from the pre_exec hook. |
143 | 0 | fn map_fds(mappings: &mut [FdMapping], child_fds: &[RawFd]) -> io::Result<()> { |
144 | 0 | if mappings.is_empty() { |
145 | | // No need to do anything, and finding first_unused_fd would fail. |
146 | 0 | return Ok(()); |
147 | 0 | } |
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 | 0 | let first_safe_fd = mappings |
153 | 0 | .iter() |
154 | 0 | .map(|mapping| max(mapping.parent_fd.as_raw_fd(), mapping.child_fd)) |
155 | 0 | .max() |
156 | 0 | .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 | 0 | for mapping in mappings.iter_mut() { |
163 | 0 | if child_fds.contains(&mapping.parent_fd.as_raw_fd()) |
164 | 0 | && mapping.parent_fd.as_raw_fd() != mapping.child_fd |
165 | | { |
166 | 0 | let parent_fd = fcntl( |
167 | 0 | mapping.parent_fd.as_raw_fd(), |
168 | 0 | FcntlArg::F_DUPFD_CLOEXEC(first_safe_fd), |
169 | 0 | )?; |
170 | | // SAFETY: We just created `parent_fd` so we can take ownership of it. |
171 | 0 | unsafe { |
172 | 0 | mapping.parent_fd = OwnedFd::from_raw_fd(parent_fd); |
173 | 0 | } |
174 | 0 | } |
175 | | } |
176 | | |
177 | | // Now we can actually duplicate FDs to the desired child FDs. |
178 | 0 | for mapping in mappings { |
179 | 0 | 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 | 0 | fcntl( |
183 | 0 | mapping.parent_fd.as_raw_fd(), |
184 | 0 | FcntlArg::F_SETFD(FdFlag::empty()), |
185 | 0 | )?; |
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 | 0 | dup2(mapping.parent_fd.as_raw_fd(), mapping.child_fd)?; |
190 | | } |
191 | | } |
192 | | |
193 | 0 | Ok(()) |
194 | 0 | } |
195 | | |
196 | 0 | fn preserve_fds(fds: &[OwnedFd]) -> io::Result<()> { |
197 | 0 | 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 | 0 | fcntl(fd.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::empty()))?; |
201 | | } |
202 | | |
203 | 0 | Ok(()) |
204 | 0 | } |
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] |
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] |
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] |
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] |
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"] |
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] |
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] |
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] |
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 |
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. |
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 | | |
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. |
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 | | } |