Coverage Report

Created: 2026-03-28 06:55

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}