├── testdata ├── file1.txt └── file2.txt ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── Cargo.toml ├── src ├── tokio.rs └── lib.rs ├── CONTRIBUTING.md ├── README.md ├── examples └── spawn.rs ├── Cargo.lock └── LICENSE /testdata/file1.txt: -------------------------------------------------------------------------------- 1 | test 1 -------------------------------------------------------------------------------- /testdata/file2.txt: -------------------------------------------------------------------------------- 1 | test 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode/ 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "command-fds" 3 | version = "0.3.2" 4 | edition = "2024" 5 | authors = ["Andrew Walbran "] 6 | license = "Apache-2.0" 7 | description = "A library for passing arbitrary file descriptors when spawning child processes." 8 | repository = "https://github.com/google/command-fds/" 9 | keywords = ["command", "process", "child", "subprocess", "fd"] 10 | categories = ["os::unix-apis"] 11 | 12 | [dependencies] 13 | nix = { version = "0.30.1", features = ["fs"] } 14 | thiserror = "2.0.17" 15 | tokio = { version = "1.48.0", optional = true, default-features = false, features = [ 16 | "process", 17 | ] } 18 | 19 | [features] 20 | default = [] 21 | tokio = ["dep:tokio"] 22 | -------------------------------------------------------------------------------- /src/tokio.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | CommandFdExt, FdMapping, FdMappingCollision, map_fds, preserve_fds, validate_child_fds, 3 | }; 4 | use std::os::fd::OwnedFd; 5 | use tokio::process::Command; 6 | 7 | impl CommandFdExt for Command { 8 | fn fd_mappings( 9 | &mut self, 10 | mut mappings: Vec, 11 | ) -> Result<&mut Self, FdMappingCollision> { 12 | let child_fds = validate_child_fds(&mappings)?; 13 | 14 | unsafe { 15 | self.pre_exec(move || map_fds(&mut mappings, &child_fds)); 16 | } 17 | 18 | Ok(self) 19 | } 20 | 21 | fn preserved_fds(&mut self, fds: Vec) -> &mut Self { 22 | unsafe { 23 | self.pre_exec(move || preserve_fds(&fds)); 24 | } 25 | 26 | self 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement (CLA). You (or your employer) retain the copyright to your 10 | contribution; this simply gives us permission to use and redistribute your 11 | contributions as part of the project. Head over to 12 | to see your current agreements on file or 13 | to sign a new one. 14 | 15 | You generally only need to submit a CLA once, so if you've already submitted one 16 | (even if it was for a different project), you probably don't need to do it 17 | again. 18 | 19 | ## Code reviews 20 | 21 | All submissions, including submissions by project members, require review. We 22 | use GitHub pull requests for this purpose. Consult 23 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 24 | information on using pull requests. 25 | 26 | ## Community Guidelines 27 | 28 | This project follows 29 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 30 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build: 13 | permissions: 14 | checks: write 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v6 18 | - name: Build 19 | run: cargo build 20 | - name: Run tests 21 | run: cargo test 22 | - name: Run clippy 23 | uses: actions-rs/clippy-check@v1 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | args: --all-features 27 | 28 | format: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v6 32 | - name: Format Rust code 33 | run: cargo fmt --all -- --check 34 | 35 | coverage: 36 | runs-on: ubuntu-latest 37 | env: 38 | RUSTC_BOOTSTRAP: 1 39 | steps: 40 | - uses: actions/checkout@v6 41 | - name: Install cargo-llvm-cov 42 | uses: taiki-e/install-action@v2 43 | with: 44 | tool: cargo-llvm-cov 45 | - name: Run tests with coverage 46 | run: cargo llvm-cov test --all-features --codecov --output-path codecov-report.json 47 | - name: Upload coverage to codecov.io 48 | uses: codecov/codecov-action@v5 49 | with: 50 | fail_ci_if_error: true 51 | files: codecov-report.json 52 | token: ${{ secrets.CODECOV_TOKEN }} 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # command-fds 2 | 3 | [![crates.io page](https://img.shields.io/crates/v/command-fds.svg)](https://crates.io/crates/command-fds) 4 | [![docs.rs page](https://docs.rs/command-fds/badge.svg)](https://docs.rs/command-fds) 5 | 6 | A library for passing arbitrary file descriptors when spawning child processes. 7 | 8 | ## Example 9 | 10 | ```rust 11 | use command_fds::{CommandFdExt, FdMapping}; 12 | use std::fs::File; 13 | use std::io::stdin; 14 | use std::os::fd::AsFd; 15 | use std::os::unix::io::AsRawFd; 16 | use std::process::Command; 17 | 18 | // Open a file. 19 | let file = File::open("Cargo.toml").unwrap(); 20 | 21 | // Prepare to run `ls -l /proc/self/fd` with some FDs mapped. 22 | let mut command = Command::new("ls"); 23 | command.arg("-l").arg("/proc/self/fd"); 24 | command 25 | .fd_mappings(vec![ 26 | // Map `file` as FD 3 in the child process. 27 | FdMapping { 28 | parent_fd: file.into(), 29 | child_fd: 3, 30 | }, 31 | // Map this process's stdin as FD 5 in the child process. 32 | FdMapping { 33 | parent_fd: stdin().as_fd().try_clone_to_owned().unwrap(), 34 | child_fd: 5, 35 | }, 36 | ]) 37 | .unwrap(); 38 | 39 | // Spawn the child process. 40 | let mut child = command.spawn().unwrap(); 41 | child.wait().unwrap(); 42 | ``` 43 | 44 | ## License 45 | 46 | Licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 47 | 48 | ## Contributing 49 | 50 | If you want to contribute to the project, see details of 51 | [how we accept contributions](CONTRIBUTING.md). 52 | -------------------------------------------------------------------------------- /examples/spawn.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 | use command_fds::{CommandFdExt, FdMapping}; 16 | use std::fs::{File, read_dir, read_link}; 17 | use std::io::stdin; 18 | use std::os::fd::AsFd; 19 | use std::os::unix::process::CommandExt; 20 | use std::process::Command; 21 | use std::thread::sleep; 22 | use std::time::Duration; 23 | 24 | /// Print out a list of all open file descriptors. 25 | fn list_fds() { 26 | let dir = read_dir("/proc/self/fd").unwrap(); 27 | for entry in dir { 28 | let entry = entry.unwrap(); 29 | let target = read_link(entry.path()).unwrap(); 30 | println!("{:?} {:?}", entry, target); 31 | } 32 | } 33 | 34 | fn main() { 35 | list_fds(); 36 | 37 | // Open a file. 38 | let file = File::open("Cargo.toml").unwrap(); 39 | println!("File: {:?}", file); 40 | list_fds(); 41 | 42 | // Prepare to run `ls -l /proc/self/fd` with some FDs mapped. 43 | let mut command = Command::new("ls"); 44 | let stdin = stdin().as_fd().try_clone_to_owned().unwrap(); 45 | command.arg("-l").arg("/proc/self/fd"); 46 | command 47 | .fd_mappings(vec![ 48 | // Map `file` as FD 3 in the child process. 49 | FdMapping { 50 | parent_fd: file.into(), 51 | child_fd: 3, 52 | }, 53 | // Map this process's stdin as FD 5 in the child process. 54 | FdMapping { 55 | parent_fd: stdin, 56 | child_fd: 5, 57 | }, 58 | ]) 59 | .unwrap(); 60 | unsafe { 61 | command.pre_exec(move || { 62 | println!("pre_exec"); 63 | list_fds(); 64 | Ok(()) 65 | }); 66 | } 67 | 68 | // Spawn the child process. 69 | println!("Spawning command"); 70 | let mut child = command.spawn().unwrap(); 71 | sleep(Duration::from_millis(100)); 72 | println!("Spawned"); 73 | list_fds(); 74 | 75 | println!("Waiting for command"); 76 | println!("{:?}", child.wait().unwrap()); 77 | } 78 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "bitflags" 7 | version = "2.9.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 10 | 11 | [[package]] 12 | name = "bytes" 13 | version = "1.10.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "cfg_aliases" 25 | version = "0.2.1" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 28 | 29 | [[package]] 30 | name = "command-fds" 31 | version = "0.3.2" 32 | dependencies = [ 33 | "nix", 34 | "thiserror", 35 | "tokio", 36 | ] 37 | 38 | [[package]] 39 | name = "libc" 40 | version = "0.2.172" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 43 | 44 | [[package]] 45 | name = "mio" 46 | version = "1.0.3" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 49 | dependencies = [ 50 | "libc", 51 | "wasi", 52 | "windows-sys 0.52.0", 53 | ] 54 | 55 | [[package]] 56 | name = "nix" 57 | version = "0.30.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 60 | dependencies = [ 61 | "bitflags", 62 | "cfg-if", 63 | "cfg_aliases", 64 | "libc", 65 | ] 66 | 67 | [[package]] 68 | name = "pin-project-lite" 69 | version = "0.2.16" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 72 | 73 | [[package]] 74 | name = "proc-macro2" 75 | version = "1.0.95" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 78 | dependencies = [ 79 | "unicode-ident", 80 | ] 81 | 82 | [[package]] 83 | name = "quote" 84 | version = "1.0.40" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 87 | dependencies = [ 88 | "proc-macro2", 89 | ] 90 | 91 | [[package]] 92 | name = "signal-hook-registry" 93 | version = "1.4.5" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" 96 | dependencies = [ 97 | "libc", 98 | ] 99 | 100 | [[package]] 101 | name = "syn" 102 | version = "2.0.101" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 105 | dependencies = [ 106 | "proc-macro2", 107 | "quote", 108 | "unicode-ident", 109 | ] 110 | 111 | [[package]] 112 | name = "thiserror" 113 | version = "2.0.17" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" 116 | dependencies = [ 117 | "thiserror-impl", 118 | ] 119 | 120 | [[package]] 121 | name = "thiserror-impl" 122 | version = "2.0.17" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" 125 | dependencies = [ 126 | "proc-macro2", 127 | "quote", 128 | "syn", 129 | ] 130 | 131 | [[package]] 132 | name = "tokio" 133 | version = "1.48.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" 136 | dependencies = [ 137 | "bytes", 138 | "libc", 139 | "mio", 140 | "pin-project-lite", 141 | "signal-hook-registry", 142 | "windows-sys 0.61.2", 143 | ] 144 | 145 | [[package]] 146 | name = "unicode-ident" 147 | version = "1.0.18" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 150 | 151 | [[package]] 152 | name = "wasi" 153 | version = "0.11.0+wasi-snapshot-preview1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 156 | 157 | [[package]] 158 | name = "windows-link" 159 | version = "0.2.1" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 162 | 163 | [[package]] 164 | name = "windows-sys" 165 | version = "0.52.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 168 | dependencies = [ 169 | "windows-targets", 170 | ] 171 | 172 | [[package]] 173 | name = "windows-sys" 174 | version = "0.61.2" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 177 | dependencies = [ 178 | "windows-link", 179 | ] 180 | 181 | [[package]] 182 | name = "windows-targets" 183 | version = "0.52.6" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 186 | dependencies = [ 187 | "windows_aarch64_gnullvm", 188 | "windows_aarch64_msvc", 189 | "windows_i686_gnu", 190 | "windows_i686_gnullvm", 191 | "windows_i686_msvc", 192 | "windows_x86_64_gnu", 193 | "windows_x86_64_gnullvm", 194 | "windows_x86_64_msvc", 195 | ] 196 | 197 | [[package]] 198 | name = "windows_aarch64_gnullvm" 199 | version = "0.52.6" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 202 | 203 | [[package]] 204 | name = "windows_aarch64_msvc" 205 | version = "0.52.6" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 208 | 209 | [[package]] 210 | name = "windows_i686_gnu" 211 | version = "0.52.6" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 214 | 215 | [[package]] 216 | name = "windows_i686_gnullvm" 217 | version = "0.52.6" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 220 | 221 | [[package]] 222 | name = "windows_i686_msvc" 223 | version = "0.52.6" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 226 | 227 | [[package]] 228 | name = "windows_x86_64_gnu" 229 | version = "0.52.6" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 232 | 233 | [[package]] 234 | name = "windows_x86_64_gnullvm" 235 | version = "0.52.6" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 238 | 239 | [[package]] 240 | name = "windows_x86_64_msvc" 241 | version = "0.52.6" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 244 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/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")] 54 | pub mod tokio; 55 | 56 | use nix::fcntl::{FcntlArg, FdFlag, fcntl}; 57 | use nix::unistd::dup2_raw; 58 | use std::cmp::max; 59 | use std::io; 60 | use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, 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) -> 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) -> &mut Self; 101 | } 102 | 103 | impl CommandFdExt for Command { 104 | fn fd_mappings( 105 | &mut self, 106 | mut mappings: Vec, 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) -> &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. 132 | fn validate_child_fds(mappings: &[FdMapping]) -> Result, FdMappingCollision> { 133 | let mut child_fds: Vec = 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. 143 | fn 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 | 195 | fn 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)] 206 | mod 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 { 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 = 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 | } 438 | --------------------------------------------------------------------------------