close_fds-0.3.2/.cargo_vcs_info.json0000644000000001120000000000000127700ustar { "git": { "sha1": "444a61682d98c0603c7570b652b9de4747629dd3" } } close_fds-0.3.2/.cirrus.yml000064400000000000000000000037240000000000000136620ustar 00000000000000task: name: FreeBSD $FREEBSD_VERSION ($TOOLCHAIN) freebsd_instance: cpu: 1 image_family: $FREEBSD_IMAGE matrix: - env: FREEBSD_VERSION: 12.1 FREEBSD_IMAGE: freebsd-12-1 - env: FREEBSD_VERSION: 12.2 FREEBSD_IMAGE: freebsd-12-2 - env: FREEBSD_VERSION: 12.2 STABLE FREEBSD_IMAGE: freebsd-12-2-snap # - env: # FREEBSD_VERSION: 13.0 # FREEBSD_IMAGE: freebsd-13-0-snap matrix: - env: TOOLCHAIN: stable - env: TOOLCHAIN: beta - env: TOOLCHAIN: nightly allow_failures: $TOOLCHAIN == 'nightly' env: CODECOV_TOKEN: ENCRYPTED[d63ddb4bf9c049623ccbacc93fec2d356473ad31202cfa83ded904b6720b8c3e59167a4a0fe88e423e66cc7ac21797f6] install_script: - pkg install -y curl kcov bash - curl -sSf https://sh.rustup.rs -o rustup.sh - sh rustup.sh -y --profile default --default-toolchain $TOOLCHAIN build_script: - . $HOME/.cargo/env - cargo build unmount_fdescfs_script: - umount /dev/fd || true no_fdescfs_test_script: - . $HOME/.cargo/env - cargo test - | mkdir -p coverage-no-fdescfs for f in target/debug/deps/*; do if [ -x "$f" ]; then kcov --exclude-pattern=/.cargo,/.rustup,/usr/lib --include-path=$(pwd) --verify coverage-no-fdescfs "$f" fi done mount_fdescfs_script: - mount -t fdescfs fdescfs /dev/fd fdescfs_test_script: - . $HOME/.cargo/env - cargo test - | mkdir -p coverage-fdescfs for f in target/debug/deps/*; do if [ -x "$f" ]; then kcov --exclude-pattern=/.cargo,/.rustup,/usr/lib --include-path=$(pwd) --verify coverage-fdescfs "$f" fi done coverage_collect_upload_script: - kcov --merge coverage-merged/ coverage-no-fdescfs/ coverage-fdescfs/ - OS="$FREEBSD_IMAGE" bash <(curl -s https://codecov.io/bash) -e OS,TOOLCHAIN -n "$FREEBSD_IMAGE-$TOOLCHAIN" -Z -f coverage-merged/kcov-merged/cobertura.xml close_fds-0.3.2/.github/actions-rs/grcov.yml000064400000000000000000000001760000000000000170350ustar 00000000000000branch: true ignore-not-existing: true llvm: true filter: covered output-type: lcov output-path: ./lcov.info ignore: - "/*" close_fds-0.3.2/.github/workflows/ci.yml000064400000000000000000000130620000000000000162610ustar 00000000000000name: CI on: [push, pull_request] defaults: run: shell: bash jobs: build: name: Build strategy: fail-fast: false matrix: toolchain: [stable, beta, nightly] target: - x86_64-unknown-linux-gnu - x86_64-unknown-linux-musl - i686-unknown-linux-gnu - i686-unknown-linux-musl - x86_64-unknown-netbsd - x86_64-pc-solaris - x86_64-unknown-illumos os: [ubuntu-latest] include: - toolchain: stable target: x86_64-apple-darwin os: macos-latest - toolchain: beta target: x86_64-apple-darwin os: macos-latest - toolchain: nightly target: x86_64-apple-darwin os: macos-latest # Allow nightly builds to fail continue-on-error: ${{ matrix.toolchain == 'nightly' }} runs-on: ${{ matrix.os }} steps: - name: Set up repo uses: actions/checkout@v2 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.toolchain }} target: ${{ matrix.target }} - name: Install 32-bit glibc build dependencies run: sudo apt-get update && sudo apt-get -y install gcc-multilib if: matrix.os == 'ubuntu-latest' && matrix.target == 'i686-unknown-linux-gnu' - name: Build uses: actions-rs/cargo@v1 with: toolchain: ${{ matrix.toolchain }} command: build args: --verbose --target ${{ matrix.target }} - name: Run tests uses: actions-rs/cargo@v1 with: toolchain: ${{ matrix.toolchain }} command: test args: --verbose --target ${{ matrix.target }} # Only try to run the tests if the OS/architecture we're building for # matches the host machine. if: >- matrix.os == 'ubuntu-latest' && (startsWith(matrix.target, 'x86_64-unknown-linux-') || startsWith(matrix.target, 'i686-unknown-linux-')) || matrix.os == 'macos-latest' && startsWith(matrix.target, 'x86_64-apple-darwin') cross-build: name: Build strategy: fail-fast: false matrix: target: - x86_64-unknown-openbsd - x86_64-unknown-dragonfly runs-on: ubuntu-latest steps: - name: Set up repo uses: actions/checkout@v2 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: nightly default: true target: x86_64-unknown-linux-gnu components: rust-src - name: Install xargo uses: actions-rs/install@v0.1 with: crate: xargo version: latest use-tool-cache: true - name: Build run: xargo build --verbose --target ${{ matrix.target }} coverage-tarpaulin: name: Tarpaulin strategy: fail-fast: false matrix: toolchain: [stable] target: - x86_64-unknown-linux-gnu - x86_64-unknown-linux-musl os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.toolchain }} target: ${{ matrix.target }} - name: Install tarpaulin uses: actions-rs/install@v0.1 with: crate: cargo-tarpaulin version: latest use-tool-cache: true - name: Run tarpaulin uses: actions-rs/cargo@v1 with: toolchain: ${{ matrix.toolchain }} command: tarpaulin args: --verbose --out Xml --target ${{ matrix.target }} - name: Upload coverage to Codecov uses: codecov/codecov-action@v1.0.13 with: name: ${{ matrix.toolchain }}-${{ matrix.target }} fail_ci_if_error: true env_vars: OS,TARGET,TOOLCHAIN,JOB env: JOB: ${{ github.job }} OS: ${{ matrix.os }} TARGET: ${{ matrix.target }} TOOLCHAIN: ${{ matrix.toolchain }} coverage-grcov: name: Grcov strategy: fail-fast: false matrix: toolchain: [nightly] target: [x86_64-apple-darwin] os: [macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.toolchain }} target: ${{ matrix.target }} - name: Run tests uses: actions-rs/cargo@v1 with: toolchain: ${{ matrix.toolchain }} command: test args: --verbose --target ${{ matrix.target }} env: CARGO_INCREMENTAL: '0' RUSTFLAGS: -Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort RUSTDOCFLAGS: -Cpanic=abort - name: Install grcov uses: actions-rs/install@v0.1 with: crate: grcov version: latest use-tool-cache: true - name: Run grcov uses: actions-rs/grcov@v0.1 - name: Upload coverage to Codecov uses: codecov/codecov-action@v1.0.13 with: name: ${{ matrix.toolchain }}-${{ matrix.target }} fail_ci_if_error: true env_vars: OS,TARGET,TOOLCHAIN,JOB files: ./lcov.info env: JOB: ${{ github.job }} OS: ${{ matrix.os }} TARGET: ${{ matrix.target }} TOOLCHAIN: ${{ matrix.toolchain }} close_fds-0.3.2/.gitignore000064400000000000000000000000530000000000000135320ustar 00000000000000/target /Cargo.lock tarpaulin-report.html close_fds-0.3.2/Cargo.lock0000644000000010730000000000000107520ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "close_fds" version = "0.3.2" dependencies = [ "cfg-if", "libc", ] [[package]] name = "libc" version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" close_fds-0.3.2/Cargo.toml0000644000000015670000000000000110050ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "close_fds" version = "0.3.2" authors = ["cptpcrd"] description = "A library that makes it easy to close all open file descriptors." readme = "README.md" categories = ["os::unix-apis"] license = "MIT" repository = "https://github.com/cptpcrd/close_fds" [dependencies.cfg-if] version = "1.0" [dependencies.libc] version = "0.2" close_fds-0.3.2/Cargo.toml.orig000064400000000000000000000005140000000000000144330ustar 00000000000000[package] name = "close_fds" version = "0.3.2" edition = "2018" description = "A library that makes it easy to close all open file descriptors." readme = "README.md" authors = ["cptpcrd"] license = "MIT" categories = ["os::unix-apis"] repository = "https://github.com/cptpcrd/close_fds" [dependencies] libc = "0.2" cfg-if = "1.0" close_fds-0.3.2/LICENSE000064400000000000000000000020500000000000000125460ustar 00000000000000MIT License Copyright (c) 2020 cptpcrd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. close_fds-0.3.2/README.md000064400000000000000000000075150000000000000130330ustar 00000000000000# close_fds [![crates.io](https://img.shields.io/crates/v/close_fds.svg)](https://crates.io/crates/close_fds) [![Docs](https://docs.rs/close_fds/badge.svg)](https://docs.rs/close_fds) [![GitHub Actions](https://github.com/cptpcrd/close_fds/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/cptpcrd/close_fds/actions?query=workflow%3ACI+branch%3Amaster+event%3Apush) [![Cirrus CI](https://api.cirrus-ci.com/github/cptpcrd/close_fds.svg?branch=master)](https://cirrus-ci.com/github/cptpcrd/close_fds) [![codecov](https://codecov.io/gh/cptpcrd/close_fds/branch/master/graph/badge.svg)](https://codecov.io/gh/cptpcrd/close_fds) A small Rust library that makes it easy to close all open file descriptors. ## Usage Add to your `Cargo.toml`: ``` [dependencies] close_fds = "0.2" ``` In your application: ``` use close_fds::close_open_fds; fn main() { // ... unsafe { close_open_fds(3, &[]); } // ... } ``` **IMPORTANT**: Please read the documentation for [`close_open_fds()`](http://docs.rs/close_fds/latest/close_fds/fn.close_open_fds.html) for an explanation of why it is `unsafe`. The first argument to `close_open_fds()` is the lowest file descriptor that should be closed; all file descriptors less than this will be left open. The second argument is a slice containing a list of additional file descriptors that should be left open. (Note: `close_open_fds()` will be more efficient if this list is sorted, especially if it is more than a few elements long.) `close_open_fds()` *always* succeeds. If one method of closing the file descriptors fails, it will fall back on another. More details, along with other helpful functions, can be found in the [documentation](http://docs.rs/close_fds/latest). ## OS support `close_fds` has two OS support tiers, similar to [Rust's support tiers](https://forge.rust-lang.org/release/platform-support.html): #### Tier 1: "Guaranteed to work" (tested in CI) - Linux (glibc and musl) - macOS - FreeBSD #### Tier 2: "Guaranteed to build" (built, but not tested, in CI) - NetBSD - OpenBSD - DragonflyBSD - Solaris - Illumos *Note: As stated in the [license](LICENSE), `close_fds` comes with no warranty.* ## OS-specific notes Here is a list of the methods that the this crate will try on various platforms to improve performance when listing the open file descriptors: - Linux - `/proc/self/fd` if `/proc` is mounted (very efficient) - macOS - `/dev/fd` (very efficient) - FreeBSD - `/dev/fd` if an [`fdescfs`](https://www.freebsd.org/cgi/man.cgi?query=fdescfs) appears to be mounted there (very efficient) - The `kern.proc.nfds` sysctl to get the number of open file descriptors (moderately efficient unless large numbers of file descriptors are open; not used by the "thread-safe" functions or when closing file descriptors) - OpenBSD - `getdtablecount()` to get the number of open file descriptors (moderately efficient unless large numbers of file descriptors are open; not used by the "thread-safe" functions or when closing file descriptors) - NetBSD - `/proc/self/fd` if `/proc` is mounted (very efficient) - `fcntl(0, F_MAXFD)` to get the maximum open file descriptor (moderately efficient) - Solaris and Illumos - `/dev/fd` or `/proc/self/fd` if either is available (very efficient) When closing file descriptors, or setting the close-on-exec flag, this crate may also call `closefrom()` on the BSDs and/or `close_range()` on Linux 5.9+/FreeBSD 12.2+, both of which are very efficient. If none of the methods listed above are available, it will fall back on a simple loop through every possible file descriptor number -- from `minfd` to `sysconf(_SC_OPEN_MAX)`. This is slow, but it will always work. Note: The most common use case, `close_open_fds(3, &[])`, is very efficient on Linux (with `/proc` mounted, or on kernel 5.9+), macOS, all of the BSDs, and Solaris/Illumos. close_fds-0.3.2/codecov.yml000064400000000000000000000001060000000000000137060ustar 00000000000000coverage: status: project: on patch: off ignore: - tests close_fds-0.3.2/examples/command-close.rs000064400000000000000000000026210000000000000164520ustar 00000000000000use std::fs; use std::os::unix::prelude::*; use std::process::Command; fn unset_cloexec(fd: RawFd) { let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; assert!(flags >= 0); if flags & libc::FD_CLOEXEC != 0 { assert_eq!( unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) }, 0 ); } } fn main() { // Open three files (really, directories), and unset the close-on-exec flag on all three let f1 = fs::File::open("/").unwrap(); let f2 = fs::File::open("/").unwrap(); let f3 = fs::File::open("/").unwrap(); unset_cloexec(f1.as_raw_fd()); unset_cloexec(f2.as_raw_fd()); unset_cloexec(f3.as_raw_fd()); let mut keep_fds = [f1.as_raw_fd(), f3.as_raw_fd()]; // ALWAYS sort the list of file descriptors if possible! keep_fds.sort_unstable(); let mut cmd = Command::new("true"); unsafe { cmd.pre_exec(move || { // On macOS/iOS, just set them as close-on-exec // Some sources indicate libdispatch may crash if the file descriptors are *actually* // closed #[cfg(any(target_os = "macos", target_os = "ios"))] close_fds::set_fds_cloexec(3, &keep_fds); #[cfg(not(any(target_os = "macos", target_os = "ios")))] close_fds::close_open_fds(3, &keep_fds); Ok(()) }); } cmd.status().unwrap(); } close_fds-0.3.2/src/closefds/cloexec.rs000064400000000000000000000047320000000000000161330ustar 00000000000000use crate::util; #[cfg(target_os = "linux")] use core::sync::atomic::{AtomicBool, Ordering}; #[cfg(target_os = "linux")] static MAY_HAVE_CLOSE_RANGE_CLOEXEC: AtomicBool = AtomicBool::new(true); #[cfg(target_os = "linux")] #[inline] fn set_cloexec_range(minfd: libc::c_uint, maxfd: libc::c_uint) -> Result<(), ()> { debug_assert!(minfd <= maxfd, "{} > {}", minfd, maxfd); if unsafe { libc::syscall( crate::sys::SYS_CLOSE_RANGE, minfd as libc::c_uint, maxfd as libc::c_uint, crate::sys::CLOSE_RANGE_CLOEXEC, ) } == 0 { Ok(()) } else { MAY_HAVE_CLOSE_RANGE_CLOEXEC.store(false, Ordering::Relaxed); Err(()) } } #[cfg(target_os = "linux")] #[inline] fn set_cloexec_shortcut( minfd: libc::c_int, keep_fds: &[libc::c_int], max_keep_fd: libc::c_int, fds_sorted: bool, ) -> Result<(), ()> { if !MAY_HAVE_CLOSE_RANGE_CLOEXEC.load(Ordering::Relaxed) { Err(()) } else if max_keep_fd < minfd { set_cloexec_range(minfd as libc::c_uint, libc::c_uint::MAX) } else if fds_sorted { util::apply_range(minfd, keep_fds, |low, high| { set_cloexec_range(low as libc::c_uint, high as libc::c_uint) }) } else { Err(()) } } pub(crate) fn set_fds_cloexec( mut minfd: libc::c_int, keep_fds: super::KeepFds, mut itbuilder: crate::FdIterBuilder, ) { let super::KeepFds { max: max_keep_fd, fds: mut keep_fds, sorted: fds_sorted, } = keep_fds; keep_fds = util::simplify_keep_fds(keep_fds, fds_sorted, &mut minfd); #[cfg(target_os = "linux")] if set_cloexec_shortcut(minfd, keep_fds, max_keep_fd, fds_sorted).is_ok() { return; } itbuilder.possible(true); for fd in itbuilder.iter_from(minfd) { if fd > max_keep_fd { // We know that none of the file descriptors we encounter from here onward can be in // keep_fds. #[cfg(target_os = "linux")] if MAY_HAVE_CLOSE_RANGE_CLOEXEC.load(Ordering::Relaxed) && set_cloexec_range(fd as libc::c_uint, libc::c_uint::MAX).is_ok() { return; } // Otherwise, this just lets us skip calling check_should_keep() util::set_cloexec(fd); } else if !util::check_should_keep(&mut keep_fds, fd, fds_sorted) { // It's not in keep_fds util::set_cloexec(fd); } } } close_fds-0.3.2/src/closefds/close.rs000064400000000000000000000203730000000000000156150ustar 00000000000000#[cfg(target_os = "linux")] use core::sync::atomic::{AtomicBool, Ordering}; #[cfg(target_os = "freebsd")] use core::sync::atomic::{AtomicU8, Ordering}; pub(crate) unsafe fn close_fds( mut minfd: libc::c_int, keep_fds: super::KeepFds, mut itbuilder: crate::FdIterBuilder, ) { let super::KeepFds { max: max_keep_fd, fds: mut keep_fds, sorted: fds_sorted, } = keep_fds; keep_fds = crate::util::simplify_keep_fds(keep_fds, fds_sorted, &mut minfd); // Some OSes have (or may have) a closefrom() or close_range() syscall that we can use to // improve performance if certain conditions are true. if close_fds_shortcut(minfd, keep_fds, max_keep_fd, fds_sorted).is_ok() { return; } itbuilder.possible(true); // On systems with closefrom(), skip the "nfds" method when determining maxfd -- these systems // have a working closefrom(), so we can just call that once we pass the end of keep_fds. #[cfg(any( target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly", ))] itbuilder.threadsafe(true); let mut fditer = itbuilder.iter_from(minfd); // We have to use a while loop so we can drop() the iterator in the closefrom() case #[allow(clippy::while_let_on_iterator)] while let Some(fd) = fditer.next() { #[allow(clippy::if_same_then_else)] if fd > max_keep_fd { // If fd > max_keep_fd, we know that none of the file descriptors we encounter from // here onward can be in keep_fds. cfg_if::cfg_if! { if #[cfg(any( target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly", ))] { // On the BSDs we can use closefrom() to close the rest // Close the directory file descriptor (if one is being used) first drop(fditer); crate::sys::closefrom(fd); return; } else { // On Linux we can do the same thing with close_range() if it's available #[cfg(target_os = "linux")] if MAY_HAVE_CLOSE_RANGE.load(Ordering::Relaxed) && try_close_range(fd as libc::c_uint, libc::c_uint::MAX).is_ok() { // We can't close the directory file descriptor *first*, because // close_range() might not be available. So there's a slight race condition // here where the call to close() might accidentally close another file // descriptor. // Then again, this function is documented as being unsafe if other threads // are interacting with file descriptors. drop(fditer); return; } // On other systems, this just allows us to skip the contains() check libc::close(fd); // We also know that none of the remaining file descriptors are in keep_fds, so // we can just iterate through and close all of them directly for fd in fditer { debug_assert!(fd > max_keep_fd); libc::close(fd); } return; } } } else if !crate::util::check_should_keep(&mut keep_fds, fd, fds_sorted) { // Close it if it's not in keep_fds libc::close(fd); } } } #[cfg(target_os = "linux")] static MAY_HAVE_CLOSE_RANGE: AtomicBool = AtomicBool::new(true); #[cfg(target_os = "linux")] unsafe fn try_close_range(minfd: libc::c_uint, maxfd: libc::c_uint) -> Result<(), ()> { // Sanity check // This shouldn't happen -- code that calls this function is usually careful to validate the // arguments -- but we want to make sure it doesn't happen because it could cause close_range() // to fail and make the code incorrectly assume that it isn't available. debug_assert!(minfd <= maxfd, "{} > {}", minfd, maxfd); #[allow(clippy::unnecessary_cast)] if libc::syscall( crate::sys::SYS_CLOSE_RANGE, minfd as libc::c_uint, maxfd as libc::c_uint, 0 as libc::c_uint, ) == 0 { Ok(()) } else { MAY_HAVE_CLOSE_RANGE.store(false, Ordering::Relaxed); Err(()) } } #[cfg(target_os = "freebsd")] fn check_has_close_range() -> Result<(), ()> { // On FreeBSD, trying to make a syscall that the kernel doesn't recognize will result in the // process being killed with SIGSYS. So before we try making a syscall(), we have to check if // the kernel is new enough. (We also have to cache the presence/absence differently because of // this). // 0=present, 1=absent, other values=uninitialized static HAS_CLOSE_RANGE: AtomicU8 = AtomicU8::new(2); match HAS_CLOSE_RANGE.load(Ordering::Relaxed) { // We know it's present 1 => Ok(()), // We know it *isn't* present 0 => Err(()), // Check if it's present // Here, we check the `kern.osreldate` sysctl _ => { const OSRELDATE_MIB: [libc::c_int; 2] = [libc::CTL_KERN, libc::KERN_OSRELDATE]; let mut osreldate = 0; let mut oldlen = core::mem::size_of::(); if unsafe { libc::sysctl( OSRELDATE_MIB.as_ptr(), OSRELDATE_MIB.len() as _, &mut osreldate as *mut _ as *mut _, &mut oldlen, core::ptr::null(), 0, ) } != 0 || osreldate < 1202000 { // Either: // - sysctl() failed somehow (???); assume close_range() is not present // - The kernel is too old and it doesn't support close_range() HAS_CLOSE_RANGE.store(0, Ordering::Relaxed); Err(()) } else { HAS_CLOSE_RANGE.store(1, Ordering::Relaxed); Ok(()) } } } } #[cfg(target_os = "freebsd")] unsafe fn try_close_range(minfd: libc::c_uint, maxfd: libc::c_uint) -> Result<(), ()> { debug_assert!(minfd <= maxfd, "{} > {}", minfd, maxfd); // This should have been checked previously debug_assert!(check_has_close_range().is_ok()); if libc::syscall( crate::sys::SYS_CLOSE_RANGE, minfd as libc::c_uint, maxfd as libc::c_uint, 0, ) == 0 { Ok(()) } else { Err(()) } } #[allow(unused_variables)] #[inline] unsafe fn close_fds_shortcut( minfd: libc::c_int, keep_fds: &[libc::c_int], max_keep_fd: libc::c_int, fds_sorted: bool, ) -> Result<(), ()> { #[cfg(any( target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly" ))] if max_keep_fd < minfd { // On the BSDs, if all the file descriptors in keep_fds are less than // minfd (or if keep_fds is empty), we can just call closefrom() crate::sys::closefrom(minfd); return Ok(()); } #[cfg(target_os = "linux")] if !MAY_HAVE_CLOSE_RANGE.load(Ordering::Relaxed) { // If we know that close_range() definitely isn't available, there's nothing we can do. return Err(()); } else if max_keep_fd < minfd { // Same case as closefrom() on the BSDs return try_close_range(minfd as libc::c_uint, libc::c_uint::MAX); } #[cfg(any(target_os = "linux", target_os = "freebsd"))] if fds_sorted { // If the list of file descriptors is sorted, we can use close_range() to close the "gaps" // between file descriptors. debug_assert!(!keep_fds.is_empty()); #[cfg(target_os = "freebsd")] check_has_close_range()?; return crate::util::apply_range(minfd, keep_fds, |low, high| { try_close_range(low as libc::c_uint, high as libc::c_uint) }); } // We can't do any optimizations without calling iter_possible_fds() Err(()) } close_fds-0.3.2/src/closefds/mod.rs000064400000000000000000000161370000000000000152720ustar 00000000000000use crate::FdIterBuilder; mod cloexec; mod close; /// A "builder" for either closing all open file descriptors or setting them as close-on-exec. #[derive(Clone, Debug)] pub struct CloseFdsBuilder<'a> { keep_fds: KeepFds<'a>, it: FdIterBuilder, } impl<'a> CloseFdsBuilder<'a> { /// Create a new builder. #[inline] pub fn new() -> Self { Self { keep_fds: KeepFds::empty(), it: FdIterBuilder::new(), } } /// Leave the file descriptors listed in `keep_fds` alone; i.e. do not close them or set the /// close-on-exec flag on them. /// /// Calling this method multiple times will *replace* the list of file descriptors to be left /// alone, not extend it. Additionally, it's not recommended to call this method multiple /// times, since it pre-scans the list to collect information for later use. /// /// # Efficiency /// /// It is **highly** recommended to sort the `keep_fds` slice first (see also /// [`Self::keep_fds_sorted()`]. This will give you significant performance improvements /// (especially on Linux 5.9+ and FreeBSD 12.2+). /// /// `close_fds` can't just copy the slice and sort it for you because allocating memory is not /// async-signal-safe (see ["Async-signal-safety"](./index.html#async-signal-safety)). #[inline] pub fn keep_fds(&mut self, keep_fds: &'a [libc::c_int]) -> &mut Self { self.keep_fds = KeepFds::new(keep_fds); self } /// Identical to [`Self::keep_fds()`], but assumes that the given list of file descriptors is /// sorted. /// /// # Safety /// /// `keep_fds` must be sorted in ascending order. #[inline] pub unsafe fn keep_fds_sorted(&mut self, keep_fds: &'a [libc::c_int]) -> &mut Self { self.keep_fds = KeepFds::new_sorted(keep_fds); self } /// Set whether [`Self::cloexecfrom()`] needs to behave reliably in multithreaded programs /// (default is `false`). /// /// See [`FdIterBuilder::threadsafe()`](./struct.FdIterBuilder.html#method.threadsafe) for more /// information. /// /// Note that this only applies when using [`Self::cloexecfrom()`]. [`Self::closefrom()`] /// is unsafe in the presence of threads anyway, so it is not required to honor this. #[inline] pub fn threadsafe(&mut self, threadsafe: bool) -> &mut Self { self.it.threadsafe(threadsafe); self } /// Set whether this crate is allowed to look at special files for speedups when closing the /// specified file descriptors (default is `true`). /// /// See /// [`FdIterBuilder::allow_filesystem()`](./struct.FdIterBuilder.html#method.allow_filesystem) /// for more information. #[inline] pub fn allow_filesystem(&mut self, fs: bool) -> &mut Self { self.it.allow_filesystem(fs); self } /// Identical to [`Self::closefrom()`], but sets the `FD_CLOEXEC` flag on the file descriptors /// instead of closing them. /// /// On some platforms (most notably, some of the BSDs), this is significantly less efficient than /// [`Self::closefrom()`], and use of that function should be preferred when possible. pub fn cloexecfrom(&self, minfd: libc::c_int) { cloexec::set_fds_cloexec( core::cmp::max(minfd, 0), self.keep_fds.clone(), self.it.clone(), ); } /// Close all of the file descriptors starting at `minfd` and not excluded by /// [`Self::keep_fds()`]. /// /// # Safety /// /// This function is NOT safe to use if other threads are interacting with files, networking, /// or anything else that could possibly involve file descriptors in any way, shape, or form. /// (Note: On some systems, file descriptor use may be more common than you think! For example, /// on Linux with some versions of musl libc, `std::fs::canonicalize()` will open a file /// descriptor to the given path.) /// /// In addition, some objects, such as `std::fs::File`, may open file descriptors and then /// assume that they will remain open. This function, by closing those file descriptors, /// violates those assumptions. /// /// This function is safe to use if it can be verified that these are not concerns. For /// example, it *should* be safe at startup or just before an `exec()`. At all other times, /// exercise extreme caution when using this function, as it may lead to race conditions and/or /// security issues. /// /// (Note: The above warnings, by definition, make it unsafe to call this function concurrently /// from multiple threads. As a result, this function may perform other non-thread-safe /// operations.) pub unsafe fn closefrom(&self, minfd: libc::c_int) { close::close_fds( core::cmp::max(minfd, 0), self.keep_fds.clone(), self.it.clone(), ); } } impl<'a> Default for CloseFdsBuilder<'a> { #[inline] fn default() -> Self { Self::new() } } #[derive(Clone, Debug)] pub(crate) struct KeepFds<'a> { fds: &'a [libc::c_int], max: libc::c_int, sorted: bool, } impl<'a> KeepFds<'a> { #[inline] pub fn empty() -> Self { Self { fds: &[], max: -1, sorted: true, } } #[inline] pub fn new(fds: &'a [libc::c_int]) -> Self { let (max, sorted) = crate::util::inspect_keep_fds(fds); Self { fds, max, sorted } } #[inline] pub unsafe fn new_sorted(fds: &'a [libc::c_int]) -> Self { Self { fds, max: fds.last().copied().unwrap_or(-1), sorted: true, } } } /// Identical to [`close_open_fds()`], but sets the `FD_CLOEXEC` flag on the file descriptors instead /// of closing them. /// /// This is equivalent to `CloseFdsBuilder::new().keep_fds(keep_fds).cloexecfrom(minfd)`. /// /// See [`CloseFdsBuilder::cloexecfrom()`] for more information. #[inline] pub fn set_fds_cloexec(minfd: libc::c_int, keep_fds: &[libc::c_int]) { CloseFdsBuilder::new().keep_fds(keep_fds).cloexecfrom(minfd) } /// Equivalent to `set_fds_cloexec()`, but behaves more reliably in multithreaded programs (at the /// cost of decreased performance on some platforms). /// /// This is equivalent to /// `CloseFdsBuilder::new().keep_fds(keep_fds).threadsafe(true).cloexecfrom(minfd)`. /// /// See [`CloseFdsBuilder::cloexecfrom()`] and [`FdIterBuilder::threadsafe()`] for more information. #[inline] pub fn set_fds_cloexec_threadsafe(minfd: libc::c_int, keep_fds: &[libc::c_int]) { CloseFdsBuilder::new() .keep_fds(keep_fds) .threadsafe(true) .cloexecfrom(minfd) } /// Close all open file descriptors starting at `minfd`, except for the file descriptors in /// `keep_fds`. /// /// This is equivalent to `CloseFdsBuilder::new().keep_fds(keep_fds).closefrom(minfd)`. /// /// See [`CloseFdsBuilder::closefrom()`] for more information. /// /// # Safety /// /// See [`CloseFdsBuilder::closefrom()`]. pub unsafe fn close_open_fds(minfd: libc::c_int, keep_fds: &[libc::c_int]) { CloseFdsBuilder::new().keep_fds(keep_fds).closefrom(minfd) } close_fds-0.3.2/src/iterfds/dirfd.rs000064400000000000000000000273010000000000000154340ustar 00000000000000#[cfg(target_os = "linux")] type RawDirent = libc::dirent64; #[cfg(target_os = "linux")] #[inline] unsafe fn getdents(fd: libc::c_int, buf: &mut [u8]) -> isize { libc::syscall( libc::SYS_getdents64, fd as libc::c_uint, buf.as_mut_ptr(), buf.len(), ) as isize } #[cfg(target_os = "freebsd")] type RawDirent = crate::sys::dirent; #[cfg(target_os = "freebsd")] #[inline] unsafe fn getdents(fd: libc::c_int, buf: &mut [u8]) -> isize { crate::sys::getdirentries( fd, buf.as_mut_ptr() as *mut libc::c_char, buf.len(), core::ptr::null_mut(), ) as isize } #[cfg(any(target_os = "macos", target_os = "ios"))] type RawDirent = libc::dirent; #[cfg(any(target_os = "macos", target_os = "ios"))] #[inline] unsafe fn getdents(fd: libc::c_int, buf: &mut [u8]) -> isize { let mut offset = core::mem::MaybeUninit::::uninit(); libc::syscall( crate::sys::SYS_GETDIRENTRIES64, fd, buf.as_mut_ptr(), buf.len(), offset.as_mut_ptr(), ) as isize } #[cfg(any(target_os = "netbsd", target_os = "solaris", target_os = "illumos"))] type RawDirent = libc::dirent; #[cfg(any(target_os = "netbsd", target_os = "solaris", target_os = "illumos"))] #[inline] unsafe fn getdents(fd: libc::c_int, buf: &mut [u8]) -> isize { crate::sys::getdents(fd, buf.as_mut_ptr() as *mut _, buf.len()) as isize } fn parse_int_bytes>(it: I) -> Option { let mut num: libc::c_int = 0; let mut seen_any = false; for ch in it { if (b'0'..=b'9').contains(&ch) { num = num .checked_mul(10)? .checked_add((ch - b'0') as libc::c_int)?; seen_any = true; } else { return None; } } if seen_any { Some(num) } else { None } } #[repr(align(8))] struct DirFdIterBuf { data: [u8; core::mem::size_of::()], } pub struct DirFdIter { minfd: libc::c_int, // This is ONLY < 0 if the iterator was exhausted during iteration and has now been closed. dirfd: libc::c_int, dirent_buf: DirFdIterBuf, dirent_nbytes: usize, dirent_offset: usize, } impl DirFdIter { #[inline] pub fn open(minfd: libc::c_int) -> Option { #[cfg(target_os = "linux")] let dirfd = unsafe { // Try /proc/self/fd on Linux. // However, on WSL 1, getdents64() doesn't always return the entries in order, and also // seems to skip some file descriptors. So skip it on WSL 1. if crate::util::is_wsl_1() { return None; } libc::open( "/proc/self/fd\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC, ) }; #[cfg(target_os = "freebsd")] let dirfd = { // On FreeBSD platforms, /dev/fd is usually a static directory with only entries // for 0, 1, and 2. This is obviously incorrect. // However, it can also be a fdescfs filesystem, in which case it's correct. // So we only trust /dev/fd if it's on a different device than /dev. let dev_path_ptr = "/dev\0".as_ptr() as *const libc::c_char; let devfd_path_ptr = "/dev/fd\0".as_ptr() as *const libc::c_char; let mut dev_stat = core::mem::MaybeUninit::uninit(); let mut devfd_stat = core::mem::MaybeUninit::uninit(); unsafe { if libc::stat(dev_path_ptr, dev_stat.as_mut_ptr()) == 0 && libc::stat(devfd_path_ptr, devfd_stat.as_mut_ptr()) == 0 && dev_stat.assume_init().st_dev != devfd_stat.assume_init().st_dev { // /dev and /dev/fd are on different devices; /dev/fd is probably an fdescfs libc::open( devfd_path_ptr, libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC, ) } else { // /dev/fd is probably a static directory -1 } } }; #[cfg(target_os = "netbsd")] let dirfd = unsafe { // On NetBSD, /dev/fd is a static directory, but /proc/self/fd is correct libc::open( "/proc/self/fd\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC, ) }; #[cfg(any(target_os = "macos", target_os = "ios"))] let dirfd = unsafe { // On macOS, /dev/fd is correct libc::open( "/dev/fd\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC, ) }; #[cfg(any(target_os = "solaris", target_os = "illumos"))] let dirfd = unsafe { // On Solaris/Illumos, both /dev/fd and /proc/self/fd should be correct // So let's try /dev/fd, then /proc/self/fd if that fails let fd = libc::open( "/dev/fd\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_CLOEXEC, ); if fd < 0 { libc::open( "/proc/self/fd\0".as_ptr() as *const libc::c_char, libc::O_RDONLY | libc::O_CLOEXEC, ) } else { fd } }; if dirfd >= 0 { Some(Self { minfd, dirfd, dirent_buf: DirFdIterBuf { data: [0; core::mem::size_of::()], }, dirent_nbytes: 0, dirent_offset: 0, }) } else { None } } #[inline] unsafe fn get_entry_info(&self, offset: usize) -> (Option, usize) { #[allow(clippy::cast_ptr_alignment)] // We trust the kernel not to make us segfault let entry = &*(self.dirent_buf.data.as_ptr().add(offset) as *const RawDirent); cfg_if::cfg_if! { if #[cfg(any( target_os = "freebsd", target_os = "netbsd", target_os = "macos", target_os = "ios", ))] { // Trailing NUL debug_assert_eq!(entry.d_name[entry.d_namlen as usize], 0); // No NULs before that debug_assert_eq!( entry.d_name[..entry.d_namlen as usize] .iter() .position(|&c| c == 0), None ); let fd = parse_int_bytes( entry.d_name[..entry.d_namlen as usize] .iter() .map(|c| *c as u8), ); } else { let fd = parse_int_bytes( entry .d_name .iter() .take_while(|c| **c != 0) .map(|c| *c as u8), ); } } (fd, entry.d_reclen as usize) } #[inline] pub fn next(&mut self) -> Result, ()> { if self.dirfd < 0 { // Exhausted return Ok(None); } loop { if self.dirent_offset >= self.dirent_nbytes { let nbytes = unsafe { getdents(self.dirfd, &mut self.dirent_buf.data) }; match nbytes.cmp(&0) { // > 0 -> Found at least one entry core::cmp::Ordering::Greater => { self.dirent_nbytes = nbytes as usize; self.dirent_offset = 0; } // 0 -> EOF core::cmp::Ordering::Equal => { // Close the directory file descriptor and return None unsafe { libc::close(self.dirfd); } self.dirfd = -1; return Ok(None); } // < 0 -> Error _ => return Err(()), } } // Note: We're assuming the OS will return the file descriptors in ascending order. // This's probably the case, considering that the kernel probably stores them in that // order. let (fd, reclen) = unsafe { self.get_entry_info(self.dirent_offset) }; // Adjust the offset for next time self.dirent_offset += reclen as usize; // Were we able to parse it? if let Some(fd) = fd { // Only return it if 1) it's in the correct range and 2) it's not // the directory file descriptor we're using if fd >= self.minfd && fd != self.dirfd { return Ok(Some(fd)); } } } } #[inline] pub fn size_hint(&self) -> (usize, Option) { if self.dirfd < 0 { // Exhausted return (0, Some(0)); } // Let's try to determine a lower limit let mut low = 0; let mut dirent_offset = self.dirent_offset; while dirent_offset < self.dirent_nbytes { // Get the next entry let (fd, reclen) = unsafe { self.get_entry_info(dirent_offset) }; // Adjust the offset for next time dirent_offset += reclen as usize; // Were we able to parse it? if let Some(fd) = fd { // Sanity check debug_assert!(fd >= self.minfd); if fd != self.dirfd { // We found one low += 1; } } } (low, Some(libc::c_int::MAX as usize)) } } impl Drop for DirFdIter { #[inline] fn drop(&mut self) { // Close the directory file descriptor if it's still open if self.dirfd >= 0 { unsafe { libc::close(self.dirfd); } } } } #[cfg(test)] mod tests { use super::*; use core::fmt::Write; pub struct BufWriter { pub buf: [u8; 80], pub i: usize, } impl BufWriter { pub fn new() -> Self { Self { buf: [0; 80], i: 0 } } pub fn iter_bytes(&'_ self) -> impl Iterator + '_ { self.buf.iter().take(self.i).cloned() } } impl Write for BufWriter { fn write_str(&mut self, s: &str) -> core::fmt::Result { if self.i + s.len() > self.buf.len() { return Err(core::fmt::Error); } for &ch in s.as_bytes() { self.buf[self.i] = ch; self.i += 1; } Ok(()) } } #[test] fn test_parse_int_bytes() { assert_eq!(parse_int_bytes(b"0".iter().cloned()), Some(0)); assert_eq!(parse_int_bytes(b"10".iter().cloned()), Some(10)); assert_eq!(parse_int_bytes(b"1423".iter().cloned()), Some(1423)); assert_eq!(parse_int_bytes(b" 0".iter().cloned()), None); assert_eq!(parse_int_bytes(b"0 ".iter().cloned()), None); assert_eq!(parse_int_bytes(b"-1".iter().cloned()), None); assert_eq!(parse_int_bytes(b"+1".iter().cloned()), None); assert_eq!(parse_int_bytes(b"1.".iter().cloned()), None); assert_eq!(parse_int_bytes(b"".iter().cloned()), None); let mut buf = BufWriter::new(); write!(&mut buf, "{}", libc::c_int::MAX as libc::c_uint + 1).unwrap(); assert_eq!(parse_int_bytes(buf.iter_bytes()), None); } } close_fds-0.3.2/src/iterfds/fditer.rs000064400000000000000000000240250000000000000156210ustar 00000000000000/// An iterator over the current process's file descriptors. /// /// The recommended way to create an `FdIter` is with /// [`FdIterBuilder`](./struct.FdIterBuilder.html); however, the "iter" /// functions (such as [`iter_open_fds()`](./fn.iter_open_fds.html)) can also be used. /// /// If this iterator is created with [`FdIterBuilder::possible()`](./struct.FdIterBuilder.html) /// set, or with one of the "possible" functions, then it may yield invalid file descriptors. This /// can be checked with [`Self::is_possible_iter()`]. pub struct FdIter { #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] pub(crate) dirfd_iter: Option, pub(crate) curfd: libc::c_int, pub(crate) possible: bool, pub(crate) maxfd: Option, /// If this is true, it essentially means "don't try the 'nfds' methods of finding the maximum /// open file descriptor." /// `close_open_fds()` passes this as true on some systems becaus the system has a working /// closefrom() and at some point it can just close the rest of the file descriptors in one go. /// Additionally, the "nfds" method is not thread-safe. #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] pub(crate) skip_nfds: bool, } impl FdIter { fn get_maxfd_direct(&self) -> libc::c_int { // This function can return -1 if no file descriptors are open. Otherwise it should return // a nonnegative integer indicating the maximum file descriptor that might be open. #[cfg(target_os = "netbsd")] unsafe { // NetBSD allows us to get the maximum open file descriptor *libc::__errno() = 0; let maxfd = libc::fcntl(0, libc::F_MAXFD); if maxfd >= 0 { return maxfd; } else if maxfd == -1 && *libc::__errno() == 0 { // fcntl(F_MAXFD) actually succeeded and returned -1, which means that no file // descriptors are open. return -1; } } #[cfg(target_os = "freebsd")] if !self.skip_nfds { // On FreeBSD, we can get the *number* of open file descriptors. From that, we can use // an is_fd_valid() loop to get the maximum open file descriptor. let mib = [ libc::CTL_KERN, libc::KERN_PROC, crate::sys::KERN_PROC_NFDS, 0, ]; let mut nfds: libc::c_int = 0; let mut oldlen = core::mem::size_of::(); if unsafe { libc::sysctl( mib.as_ptr(), mib.len() as libc::c_uint, &mut nfds as *mut libc::c_int as *mut libc::c_void, &mut oldlen, core::ptr::null(), 0, ) } == 0 { if let Some(maxfd) = Self::nfds_to_maxfd(nfds) { return maxfd; } } } #[cfg(target_os = "openbsd")] if !self.skip_nfds { // On OpenBSD, we have a similar situation as with FreeBSD -- we can get the *number* // of open file descriptors, and perhaps work from that to get the maximum open file // descriptor. if let Some(maxfd) = Self::nfds_to_maxfd(unsafe { crate::sys::getdtablecount() }) { return maxfd; } } let fdlimit = unsafe { libc::sysconf(libc::_SC_OPEN_MAX) }; // Clamp it at 65536 because that's a LOT of file descriptors // Also don't trust values below 1024 fdlimit.max(1024).min(65536) as libc::c_int - 1 } #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] #[inline] fn nfds_to_maxfd(nfds: libc::c_int) -> Option { // Given the number of open file descriptors, return the largest open file descriptor (or // None if it can't be reasonably determined). if nfds == 0 { // No open file descriptors -- nothing to do! return Some(-1); } else if nfds < 0 { // Probably failure of the underlying function return None; } else if nfds >= 100 { // We're probably better off just iterating through return None; } let mut nfds_found = 0; // We know the number of open file descriptors; let's use that to try to find the largest // open file descriptor. for fd in 0..(nfds * 2) { if crate::util::is_fd_valid(fd) { // Valid file descriptor nfds_found += 1; if nfds_found >= nfds { // We've found all the open file descriptors. // We now know that the current `fd` is the largest open file descriptor return Some(fd); } } } // We haven't found all of the open file descriptors yet, but it seems like we *should* // have. // // This usually means one of two things: // // 1. The process opened a large number of file descriptors, then closed many of them. // However, it left several of the high-numbered file descriptors open. (For example, // consider the case where the open file descriptors are 0, 1, 2, 50, and 100. nfds=5, // but the highest open file descriptor is actually 100!) // 2. The 'nfds' method is vulnerable to a race condition: if a file descriptor is closed // after the number of open file descriptors has been obtained, but before the fcntl() // loop reaches that file descriptor, then the loop will never find all of the open file // descriptors because it will be stuck at n_fds_found = nfds-1. // If this happens, without this check the loop would essentially become an infinite // loop. // (For example, consider the case where the open file descriptors are 0, 1, 2, and 3. If // file descriptor 3 is closed before the fd=3 iteration, then we will be stuck at // n_fds_found=3 and will never be able to find the 4th file descriptor.) // // Error on the side of caution (case 2 is dangerous) and let the caller select another // method. None } #[inline] fn get_maxfd(&mut self) -> libc::c_int { match self.maxfd { Some(maxfd) => maxfd, None => { let maxfd = self.get_maxfd_direct(); debug_assert!(maxfd >= -1); self.maxfd = Some(maxfd); maxfd } } } /// Returns whether this iterator was created with one of the "possible" iteration functions, /// in which case it may yield invalid file descriptors and the caller is responsible for /// checking their validity. #[inline] pub fn is_possible_iter(&self) -> bool { self.possible } } impl Iterator for FdIter { type Item = libc::c_int; fn next(&mut self) -> Option { #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] if let Some(dfd_iter) = self.dirfd_iter.as_mut() { // Try iterating using the directory file descriptor we opened match dfd_iter.next() { Ok(Some(fd)) => { debug_assert!(fd >= self.curfd); // We set self.curfd so that if something goes wrong we can switch to the maxfd // loop without repeating file descriptors self.curfd = fd + 1; return Some(fd); } Ok(None) => return None, // Something went wrong. Close the directory file descriptor and fall back on a // maxfd loop Err(_) => self.dirfd_iter = None, } } let maxfd = self.get_maxfd(); while self.curfd <= maxfd { // Get the current file descriptor let fd = self.curfd; // Increment it for next time self.curfd += 1; // If we weren't given the "possible" flag, we have to check that it's a valid file // descriptor first. if self.possible || crate::util::is_fd_valid(fd) { return Some(fd); } } // Exhausted the range None } fn size_hint(&self) -> (usize, Option) { #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] if let Some(dfd_iter) = self.dirfd_iter.as_ref() { // Delegate to the directory file descriptor return dfd_iter.size_hint(); } if let Some(maxfd) = self.maxfd { if maxfd == -1 { // No file descriptors open return (0, Some(0)); } debug_assert!(maxfd > 0); // maxfd is set; we can give an upper bound by comparing to curfd let diff = (maxfd as usize + 1).saturating_sub(self.curfd as usize); // If we were given the "possible" flag, then this is also the lower limit. (if self.possible { diff } else { 0 }, Some(diff)) } else { // Unknown (0, Some(libc::c_int::MAX as usize)) } } #[inline] fn min(mut self) -> Option { self.next() } #[inline] fn max(self) -> Option { self.last() } } impl core::iter::FusedIterator for FdIter {} close_fds-0.3.2/src/iterfds/mod.rs000064400000000000000000000313450000000000000151260ustar 00000000000000mod fditer; pub use fditer::FdIter; #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] mod dirfd; /// A "builder" to construct an [`FdIter`] with custom parameters. /// /// # Warnings /// /// **TL;DR**: Don't use `FdIter`/`FdIterBuilder` in multithreaded programs unless you know what /// you're doing, and avoid opening/closing file descriptors while consuming an `FdIter`. /// /// 1. File descriptors that are opened *during* iteration may or may not be included in the results /// (exact behavior is platform-specific and depends on several factors). /// /// 2. **IMPORTANT**: On some platforms, if other threads open file descriptors at very specific /// times during a call to `FdIter::next()`, that may result in other file descriptors being /// skipped. Use with caution. (If this is a problem for you, set `.threadsafe(true)`, which /// avoids this issue). /// /// 3. *Closing* file descriptors during iteration (in the same thread or in another thread) will /// not affect the iterator's ability to list other open file descriptors (if it does, that is a /// bug). However, in most cases you should use /// [`CloseFdsBuilder`](./struct.CloseFdsBuilder.html) to do this. /// /// 4. Some of the file descriptors yielded by this iterator may be in active use by other sections /// of code. Be very careful about which operations you perform on them. /// /// If your program is multi-threaded, this is especially true, since a file descriptor returned /// by this iterator may have been closed by the time your code tries to do something with it. #[derive(Clone, Debug)] pub struct FdIterBuilder { possible: bool, #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] skip_nfds: bool, #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] dirfd: bool, } impl FdIterBuilder { /// Create a new builder. /// /// `minfd` specifies the number of the file descriptor at which iteration will begin. #[inline] pub fn new() -> Self { Self { possible: false, #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] skip_nfds: false, #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] dirfd: true, } } /// Set whether the returned `FdIter` is allowed to yield invalid file descriptors for /// efficiency (default is `false`). /// /// If this flag is set, the caller is responsible for checking if the returned file descriptors /// are valid. /// /// # Proper usage /// /// You should only use this flag if you immediately perform an operation on each file /// descriptor that implicitly checks if the file descriptor is valid. #[inline] pub fn possible(&mut self, possible: bool) -> &mut Self { self.possible = possible; self } /// Set whether the returned `FdIter` needs to behave reliably in multithreaded programs /// (default is `false`). /// /// If other threads open file descriptors at specific times, an `FdIter` may skip over other /// file descriptors. Setting `.threadsafe(true)` prevents this, but may come at the cost of /// significantly increased performance on some platforms (because the code which may behave /// strangely in the presence of threads provides a potential performance improvement). /// /// Currently, setting this flag will only affect performance on 1) OpenBSD and 2) FreeBSD /// without an `fdescfs` mounted on `/dev/fd`. #[allow(unused_variables)] #[inline] pub fn threadsafe(&mut self, threadsafe: bool) -> &mut Self { #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] { self.skip_nfds = threadsafe; } self } /// Set whether returned `FdIter` is allowed to look at special files for speedups (default is /// `true`). /// /// On some systems, `/dev/fd` and/or `/proc/self/fd` provide an accurate view of the file /// descriptors that the current process has open; if this flag is set to `true` then those /// may be examined as an optimization. /// /// It may be desirable to set this to `false` e.g. if `chroot()`ing into an environment where /// untrusted code may be able to replace `/proc` or `/dev`. However, on some platforms (such /// as Linux<5.9 and macOS) setting this to `false` may significantly decrease performance. #[allow(unused_variables)] #[inline] pub fn allow_filesystem(&mut self, fs: bool) -> &mut Self { #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] { self.dirfd = fs; } self } /// Create an `FdIter` that iterates over the open file descriptors starting at `minfd`. pub fn iter_from(&self, mut minfd: libc::c_int) -> FdIter { if minfd < 0 { minfd = 0; } FdIter { curfd: minfd, possible: self.possible, maxfd: None, #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] skip_nfds: self.skip_nfds, #[cfg(any( target_os = "linux", target_os = "macos", target_os = "ios", target_os = "freebsd", target_os = "netbsd", target_os = "solaris", target_os = "illumos", ))] dirfd_iter: if self.dirfd { dirfd::DirFdIter::open(minfd) } else { None }, } } } impl Default for FdIterBuilder { #[inline] fn default() -> Self { Self::new() } } /// Iterate over all open file descriptors for the current process, starting at `minfd`. The file /// descriptors are guaranteed to be returned in ascending order. /// /// This is equivalent to `FdIterBuilder::new().iter_from(minfd)`. /// /// See the warnings for [`FdIterBuilder`]. #[inline] pub fn iter_open_fds(minfd: libc::c_int) -> FdIter { FdIterBuilder::new().iter_from(minfd) } /// Equivalent to [`iter_open_fds()`], but behaves more reliably in multithreaded programs (at the /// cost of decreased performance on some platforms). /// /// This is equivalent to `FdIterBuilder::new().threadsafe(true).iter_from(minfd)`. /// /// See [`FdIterBuilder::threadsafe()`] for more information. #[inline] pub fn iter_open_fds_threadsafe(minfd: libc::c_int) -> FdIter { FdIterBuilder::new().threadsafe(true).iter_from(minfd) } /// Identical to `iter_open_fds()`, but may -- for efficiency -- yield invalid file descriptors. /// /// This is equivalent to `FdIterBuilder::new().possible(true).iter_from(minfd)`. /// /// See [`FdIterBuilder::possible()`] for more information. #[inline] pub fn iter_possible_fds(minfd: libc::c_int) -> FdIter { FdIterBuilder::new().possible(true).iter_from(minfd) } /// Identical to `iter_open_fds_threadsafe()`, but may -- for efficiency -- yield invalid file /// descriptors. /// /// This is equivalent to `FdIterBuilder::new().possible(true).threadafe(true).iter_from(minfd)`. /// /// See [`FdIterBuilder::possible()`] and [`FdIterBuilder::threadsafe()`] for more information. #[inline] pub fn iter_possible_fds_threadsafe(minfd: libc::c_int) -> FdIter { FdIterBuilder::new() .possible(true) .threadsafe(true) .iter_from(minfd) } #[cfg(test)] mod tests { use super::*; fn open_files() -> [libc::c_int; 10] { let mut fds = [-1; 10]; for cur_fd in fds.iter_mut() { *cur_fd = unsafe { libc::open("/\0".as_ptr() as *const libc::c_char, libc::O_RDONLY) }; assert!(*cur_fd >= 0); } fds } unsafe fn close_files(fds: &[libc::c_int]) { for &fd in fds { libc::close(fd); } } #[test] fn test_size_hint_open() { test_size_hint_generic(FdIterBuilder::new().threadsafe(false).iter_from(0)); test_size_hint_generic(FdIterBuilder::new().threadsafe(true).iter_from(0)); let fds = open_files(); test_size_hint_generic(FdIterBuilder::new().threadsafe(false).iter_from(0)); test_size_hint_generic(FdIterBuilder::new().threadsafe(true).iter_from(0)); unsafe { close_files(&fds); } } #[test] fn test_size_hint_possible() { test_size_hint_generic( FdIterBuilder::new() .possible(true) .threadsafe(false) .iter_from(0), ); test_size_hint_generic( FdIterBuilder::new() .possible(true) .threadsafe(true) .iter_from(0), ); let fds = open_files(); test_size_hint_generic( FdIterBuilder::new() .possible(true) .threadsafe(false) .iter_from(0), ); test_size_hint_generic( FdIterBuilder::new() .possible(true) .threadsafe(true) .iter_from(0), ); unsafe { close_files(&fds); } } fn test_size_hint_generic(mut fditer: FdIter) { let (mut init_low, mut init_high) = fditer.size_hint(); if let Some(init_high) = init_high { // Sanity check assert!(init_high >= init_low); } let mut i = 0; while let Some(_fd) = fditer.next() { let (cur_low, cur_high) = fditer.size_hint(); // Adjust them so they're comparable to init_low and init_high let adj_low = cur_low + i + 1; let adj_high = if let Some(cur_high) = cur_high { // Sanity check assert!(cur_high >= cur_low); Some(cur_high + i + 1) } else { None }; // Now we adjust init_low and init_high to be the most restrictive limits that we've // received so far. if adj_low > init_low { init_low = adj_low; } if let Some(adj_high) = adj_high { if let Some(ihigh) = init_high { if adj_high < ihigh { init_high = Some(adj_high); } } else { init_high = Some(adj_high); } } i += 1; } // At the end, the lower boundary should be 0. The upper boundary can be anything. let (final_low, _) = fditer.size_hint(); assert_eq!(final_low, 0); // Now make sure that the actual count falls within the boundaries we were given assert!(i >= init_low); if let Some(init_high) = init_high { assert!(i <= init_high); } } #[test] fn test_fused_open() { test_fused_generic(FdIterBuilder::new().threadsafe(false).iter_from(0)); test_fused_generic(FdIterBuilder::new().threadsafe(true).iter_from(0)); let fds = open_files(); test_fused_generic(FdIterBuilder::new().threadsafe(false).iter_from(0)); test_fused_generic(FdIterBuilder::new().threadsafe(true).iter_from(0)); unsafe { close_files(&fds); } } #[test] fn test_fused_possible() { test_fused_generic( FdIterBuilder::new() .possible(true) .threadsafe(false) .iter_from(0), ); test_fused_generic( FdIterBuilder::new() .possible(true) .threadsafe(true) .iter_from(0), ); let fds = open_files(); test_fused_generic( FdIterBuilder::new() .possible(true) .threadsafe(false) .iter_from(0), ); test_fused_generic( FdIterBuilder::new() .possible(true) .threadsafe(true) .iter_from(0), ); unsafe { close_files(&fds); } } fn test_fused_generic(mut fditer: FdIter) { // Exhaust the iterator fditer.by_ref().count(); assert_eq!(fditer.next(), None); } } close_fds-0.3.2/src/lib.rs000064400000000000000000000122120000000000000134450ustar 00000000000000//! # Why is this crate useful? //! //! By default, any file descriptors opened by a process are inherited by any of that process's //! children. This can cause major bugs and security issues, but this behavior can't be changed //! without massively breaking backwards compatibility. //! //! Rust hides most of these problems by setting the close-on-exec flag on all file descriptors //! opened by the standard library (which causes them to *not* be inherited by child processes). //! However, in some scenarios it may be necessary to have a way to close all open file //! descriptors. //! //! ## Scenarios where this is helpful //! //! - Writing set-UID programs (which may not be able to fully trust their environment; for //! example, `sudo` closes all open file descriptors when it starts as a security measure) //! - Spawning processes while interacting with FFI code that *doesn't* set the close-on-exec flag //! on file descriptors it opens (the functionality offered by this crate is the ONLY way to //! safely do this) //! - On some platforms (notably, macOS/iOS), Rust isn't always able to set the close-on-exec flag //! *atomically*, which creates race conditions if one thread is e.g. opening sockets while //! another thread is spawning processes. This crate may be useful in helping to avoid those //! race conditions. //! //! # Example usage //! //! Here is a short program that uses `close_fds` to close all file descriptors (except the ones //! in a specified list) in a child process before launching it: //! //! ``` //! use std::process::Command; //! use std::os::unix::prelude::*; //! //! // Add any file descriptors that should stay open here //! let mut keep_fds = []; //! // ALWAYS sort the slice! It will give significant performance improvements. //! keep_fds.sort_unstable(); //! //! let mut cmd = Command::new("true"); //! //! // ... //! // Set up `cmd` here //! // ... //! //! unsafe { //! cmd.pre_exec(move || { //! // On macOS/iOS, just set them as close-on-exec (some sources indicate closing them //! // directly may cause problems) //! #[cfg(any(target_os = "macos", target_os = "ios"))] //! close_fds::set_fds_cloexec(3, &keep_fds); //! #[cfg(not(any(target_os = "macos", target_os = "ios")))] //! close_fds::close_open_fds(3, &keep_fds); //! //! Ok(()) //! }); //! } //! //! // Launch the child process //! //! cmd.status().unwrap(); //! ``` //! //! # Builders vs. helper functions //! //! [`close_open_fds()`], [`set_fds_cloexec()`], [`iter_open_fds()`], etc. provide simple ways to //! close, set the close-on-exec flag on, or iterate over all open file descriptors. //! //! However, for more advanced use cases, there are also "builders" ([`CloseFdsBuilder`], //! [`FdIterBuilder`]) which can be used to further customize aspects of the actions that will be //! taken. The documentation of each helper function describes how the same task could be performed //! using one of the builders. //! //! # Async-signal-safety //! //! ## Background //! //! An async-signal-safe function is one that is safe to call from a signal handler (many functions //! in the system libc are not!). In a multi-threaded program, when running in the child after a //! `fork()` (such as in a closure registered with //! `std::os::unix::process::CommandExt::pre_exec()`, it is only safe to call async-signal-safe //! functions. See `signal-safety(7)` and `fork(2)` for more information. //! //! ## Async-signal-safety in this crate //! //! **TL;DR**: The functions in this crate are async-signal-safe on Linux, macOS/iOS, the BSDs, and //! Solaris/Illumos. They *should* also be async-signal-safe on other \*nix-like OSes. //! //! Since the functions in this crate are most useful in the child process after a `fork()`, this //! crate tries to make all of them async-signal-safe. However, many of the optimizations that this //! crate performs in order to be efficient would not be possible by simply calling functions that //! POSIX requires to be async-signal-safe. //! //! As a result, this crate assumes that the following functions are async-signal-safe (in addition //! to the ones required by POSIX): //! //! - `closefrom()` on the BSDs //! - The `close_range()` syscall on Linux and FreeBSD //! - `sysctl()` on FreeBSD //! - `getdtablecount()` on OpenBSD //! - `getdirentries()`/`getdents()` (whichever is available) on Linux, NetBSD, FreeBSD, macOS/iOS, //! and Solaris/Illumos //! - `sysconf(_SC_OPEN_MAX)` on all OSes //! //! All of these except for `sysconf()` are implemented as system calls (or thin wrappers around //! other system calls) on whichever OS(es) they are present on. As a result, they should be //! async-signal-safe, even though they are not explicitly documented as such. //! //! `sysconf()` is not guaranteed to be async-signal-safe. However, on Linux, macOS/iOS, the BSDs, //! and Solaris/Illumos, `sysconf(_SC_OPEN_MAX)` is implemented in terms of //! `getrlimit(RLIMIT_NOFILE)`. On those platforms, `getrlimit()` is a system call, so //! `sysconf(_SC_OPEN_MAX)` (and thus, the functions in this crate) should be async-signal-safe. #![no_std] mod closefds; mod iterfds; mod sys; mod util; pub use closefds::*; pub use iterfds::*; close_fds-0.3.2/src/sys.rs000064400000000000000000000032240000000000000135200ustar 00000000000000// This is the correct value for every architecture except alpha, which Rust doesn't support. #[cfg(target_os = "linux")] pub const SYS_CLOSE_RANGE: libc::c_long = 436; #[cfg(target_os = "linux")] pub const CLOSE_RANGE_CLOEXEC: libc::c_uint = 1 << 2; #[cfg(target_os = "freebsd")] pub const SYS_CLOSE_RANGE: libc::c_int = 575; #[cfg(target_os = "freebsd")] pub const KERN_PROC_NFDS: libc::c_int = 43; #[cfg(any(target_os = "macos", target_os = "ios"))] pub const SYS_GETDIRENTRIES64: libc::c_int = 344; #[cfg(target_os = "freebsd")] #[repr(C)] pub struct dirent { pub d_fileno: libc::ino_t, pub d_off: libc::off_t, pub d_reclen: u16, pub d_type: u8, d_pad0: u8, pub d_namlen: u16, d_pad1: u16, pub d_name: [libc::c_char; 256], } #[cfg(target_os = "freebsd")] extern "C" { pub fn closefrom(lowfd: libc::c_int); pub fn getdirentries( fd: libc::c_int, buf: *mut libc::c_char, nbytes: libc::size_t, basep: *mut libc::off_t, ) -> libc::ssize_t; } #[cfg(target_os = "openbsd")] extern "C" { pub fn getdtablecount() -> libc::c_int; } #[cfg(any(target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly"))] extern "C" { pub fn closefrom(fd: libc::c_int) -> libc::c_int; } #[cfg(target_os = "netbsd")] extern "C" { #[link_name = "__getdents30"] pub fn getdents( fildes: libc::c_int, buf: *mut libc::c_char, nbyte: libc::size_t, ) -> libc::c_int; } #[cfg(any(target_os = "solaris", target_os = "illumos"))] extern "C" { pub fn getdents( fildes: libc::c_int, buf: *mut libc::dirent, nbyte: libc::size_t, ) -> libc::c_int; } close_fds-0.3.2/src/util.rs000064400000000000000000000303340000000000000136610ustar 00000000000000pub fn inspect_keep_fds(keep_fds: &[libc::c_int]) -> (libc::c_int, bool) { // Get the maximum file descriptor from the list, and also check if it's sorted. let mut max_keep_fd = -1; let mut last_fd = -1; let mut fds_sorted = true; for fd in keep_fds.iter().cloned() { // Check for a new maximum file descriptor if fd > max_keep_fd { max_keep_fd = fd; debug_assert!(last_fd <= fd); } else if last_fd > fd { // Out of order fds_sorted = false; } last_fd = fd; } (max_keep_fd, fds_sorted) } pub fn simplify_keep_fds<'a>( mut keep_fds: &'a [libc::c_int], fds_sorted: bool, minfd: &mut libc::c_int, ) -> &'a [libc::c_int] { use core::cmp::Ordering; if fds_sorted { // Example: specifying keep_fds=[3, 4, 6, 7]; minfd=3 has the same result as specifying // keep_fds=[6, 7]; minfd=5. // In some cases, this translation may reduce the number of syscalls and/or eliminate the // need to call iter_fds() in the first place. while let Some((first, rest)) = keep_fds.split_first() { match first.cmp(&minfd) { // keep_fds[0] > minfd // No further simplification can be done Ordering::Greater => break, // keep_fds[0] == minfd // We can remove keep_fds[0] and increment minfd Ordering::Equal => { keep_fds = rest; *minfd += 1; } // keep_fds[0] < minfd // We can remove keep_fds[0] Ordering::Less => keep_fds = rest, } } } keep_fds } pub fn check_should_keep(keep_fds: &mut &[libc::c_int], fd: libc::c_int, fds_sorted: bool) -> bool { if fds_sorted { // If the file descriptor list is sorted, we can do a more efficient lookup // Skip over any elements less than the current file descriptor. // For example if keep_fds is [0, 1, 4, 5] and fd is either 3 or 4, we can skip over 0 and 1 // -- those cases have been covered already. if let Some(index) = keep_fds.iter().position(|&x| x >= fd) { *keep_fds = &(*keep_fds)[index..]; } // Is the file descriptor we're searching for present? keep_fds.first() == Some(&fd) } else { // Otherwise, we have to fall back on contains() keep_fds.contains(&fd) } } #[cfg(target_os = "linux")] #[inline] pub fn is_wsl_1() -> bool { use core::sync::atomic::{AtomicU8, Ordering}; // 0=Not running on WSL 1 // 1=Running on WSL 1 // >1=Uninitialized static IS_WSL1: AtomicU8 = AtomicU8::new(2); match IS_WSL1.load(Ordering::Relaxed) { // Already initialized; return the result 1 => true, 0 => false, _ => { let mut uname = unsafe { core::mem::zeroed() }; unsafe { libc::uname(&mut uname); } let uname_release_len = uname .release .iter() .position(|c| *c == 0) .unwrap_or_else(|| uname.release.len()); // uname.release is an array of `libc::c_char`s. `libc::c_char` may be either a u8 or // an i8, so unfortunately we have to use unsafe operations to get a reference as a // &[u8]. let uname_release = unsafe { core::slice::from_raw_parts(uname.release.as_ptr() as *const u8, uname_release_len) }; // It seems that on WSL 1 the kernel "release name" ends with "-Microsoft", and on WSL // 2 the release name ends with "-microsoft-standard". So we look for "Microsoft" at // the end to mean WSL 1. let is_wsl1 = uname_release.ends_with(b"Microsoft"); // Store the result IS_WSL1.store(is_wsl1 as u8, Ordering::Relaxed); is_wsl1 } } } #[inline] pub fn is_fd_valid(fd: libc::c_int) -> bool { unsafe { libc::fcntl(fd, libc::F_GETFD) >= 0 } } #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub fn apply_range Result<(), ()>>( minfd: libc::c_int, mut keep_fds: &[libc::c_int], mut func: F, ) -> Result<(), ()> { // Skip over any elements of keep_fds that are less than minfd if let Some(index) = keep_fds.iter().position(|&fd| fd >= minfd) { keep_fds = &keep_fds[index..]; } else { // keep_fds is empty (or would be when all elements < minfd are removed) return func(minfd, libc::c_int::MAX); } if keep_fds[0] > minfd { func(minfd, keep_fds[0] - 1)?; } for i in 0..(keep_fds.len() - 1) { // Safety: i will only ever be in the range [0, keep_fds.len() - 2]. // So i and i + 1 will always be valid indices in keep_fds. let low = unsafe { *keep_fds.get_unchecked(i) }; let high = unsafe { *keep_fds.get_unchecked(i + 1) }; debug_assert!(high >= low); if high - low >= 2 { func(low + 1, high - 1)?; } } func(keep_fds[keep_fds.len() - 1] + 1, libc::c_int::MAX) } pub fn set_cloexec(fd: libc::c_int) { let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; if flags >= 0 && (flags & libc::FD_CLOEXEC) != libc::FD_CLOEXEC { // fcntl(F_GETFD) succeeded, and it did *not* return the FD_CLOEXEC flag unsafe { libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC); } } } #[cfg(test)] mod tests { use super::*; fn with_fd(f: F) { let fd = unsafe { libc::open(b"/\0".as_ptr() as *const _, libc::O_RDONLY) }; assert!(fd >= 0); f(fd); unsafe { libc::close(fd); } } #[test] fn test_inspect_keep_fds() { assert_eq!(inspect_keep_fds(&[]), (-1, true)); assert_eq!(inspect_keep_fds(&[0]), (0, true)); assert_eq!(inspect_keep_fds(&[0, 1]), (1, true)); assert_eq!(inspect_keep_fds(&[1, 0]), (1, false)); assert_eq!(inspect_keep_fds(&[0, 1, 2, 5, 7]), (7, true)); assert_eq!(inspect_keep_fds(&[0, 1, 2, 7, 5]), (7, false)); } #[test] fn test_check_should_keep_sorted() { let mut keep_fds: &[libc::c_int] = &[0, 1, 5, 8, 10]; assert!(check_should_keep(&mut keep_fds, 0, true)); assert_eq!(keep_fds, &[0, 1, 5, 8, 10]); assert!(check_should_keep(&mut keep_fds, 1, true)); assert_eq!(keep_fds, &[1, 5, 8, 10]); assert!(!check_should_keep(&mut keep_fds, 2, true)); assert_eq!(keep_fds, &[5, 8, 10]); assert!(!check_should_keep(&mut keep_fds, 2, true)); assert_eq!(keep_fds, &[5, 8, 10]); assert!(!check_should_keep(&mut keep_fds, 3, true)); assert_eq!(keep_fds, &[5, 8, 10]); assert!(check_should_keep(&mut keep_fds, 5, true)); assert_eq!(keep_fds, &[5, 8, 10]); assert!(!check_should_keep(&mut keep_fds, 6, true)); assert_eq!(keep_fds, &[8, 10]); } #[test] fn test_check_should_keep_not_sorted() { let mut keep_fds: &[libc::c_int] = &[0, 1, 5, 8, 10]; assert!(check_should_keep(&mut keep_fds, 0, false)); assert_eq!(keep_fds, &[0, 1, 5, 8, 10]); assert!(check_should_keep(&mut keep_fds, 1, false)); assert_eq!(keep_fds, &[0, 1, 5, 8, 10]); assert!(!check_should_keep(&mut keep_fds, 2, false)); assert_eq!(keep_fds, &[0, 1, 5, 8, 10]); assert!(!check_should_keep(&mut keep_fds, 3, false)); assert_eq!(keep_fds, &[0, 1, 5, 8, 10]); assert!(check_should_keep(&mut keep_fds, 5, false)); assert_eq!(keep_fds, &[0, 1, 5, 8, 10]); assert!(!check_should_keep(&mut keep_fds, 6, false)); assert_eq!(keep_fds, &[0, 1, 5, 8, 10]); } #[test] fn test_simplify_keep_fds() { let mut keep_fds: &[libc::c_int]; let mut minfd; // Here, the entire list can be emptied without changing minfd keep_fds = &[0, 1, 2]; minfd = 3; assert_eq!(simplify_keep_fds(keep_fds, true, &mut minfd), &[]); assert_eq!(minfd, 3); // Here, it can be emptied if minfd is increased keep_fds = &[2, 3, 4, 5, 6, 7]; minfd = 3; assert_eq!(simplify_keep_fds(keep_fds, true, &mut minfd), &[]); assert_eq!(minfd, 8); // Here, the first 3 elements can be removed if minfd is increased keep_fds = &[3, 4, 5, 8, 10]; minfd = 3; assert_eq!(simplify_keep_fds(keep_fds, true, &mut minfd), &[8, 10]); assert_eq!(minfd, 6); // Here it's just the first element keep_fds = &[3, 5, 8, 10]; minfd = 3; assert_eq!(simplify_keep_fds(keep_fds, true, &mut minfd), &[5, 8, 10]); assert_eq!(minfd, 4); // Can't simplify any further keep_fds = &[4, 5, 8, 10]; minfd = 3; assert_eq!( simplify_keep_fds(keep_fds, true, &mut minfd), &[4, 5, 8, 10] ); assert_eq!(minfd, 3); // And if fds_sorted=false, no simplification can be performed keep_fds = &[2, 1, 0]; minfd = 3; assert_eq!(simplify_keep_fds(keep_fds, false, &mut minfd), &[2, 1, 0]); assert_eq!(minfd, 3); keep_fds = &[4, 5, 8, 10, 3]; minfd = 3; assert_eq!( simplify_keep_fds(keep_fds, false, &mut minfd), &[4, 5, 8, 10, 3] ); assert_eq!(minfd, 3); } #[cfg(any(target_os = "linux", target_os = "freebsd"))] #[test] fn test_apply_range() { macro_rules! check_ok { ($minfd:expr, [$($keep_fds:expr),* $(,)?], [$($calls:expr),* $(,)?] $(,)?) => {{ let mut ranges = [(0, 0); 100]; let mut len = 0; apply_range($minfd, &[$($keep_fds),*], |low, high| { *ranges.get_mut(len).unwrap() = (low, high); len += 1; Ok(()) }).unwrap(); assert_eq!(&ranges[..len], [$($calls),*]); }} } check_ok!(0, [], [(0, libc::c_int::MAX)]); check_ok!(-100, [], [(-100, libc::c_int::MAX)]); check_ok!(3, [0, 2, 3, 4, 5, 6], [(7, libc::c_int::MAX)]); check_ok!(3, [0, 2], [(3, libc::c_int::MAX)]); check_ok!(3, [3, 4, 5, 6], [(7, libc::c_int::MAX)]); check_ok!(3, [4, 5, 6], [(3, 3), (7, libc::c_int::MAX)]); check_ok!(3, [5, 6, 9, 10], [(3, 4), (7, 8), (11, libc::c_int::MAX)]); check_ok!( 3, [5, 6, 9, 10, 20, 23], [(3, 4), (7, 8), (11, 19), (21, 22), (24, libc::c_int::MAX)], ); macro_rules! check_err { ($minfd:expr, [$($keep_fds:expr),* $(,)?], $call:expr $(,)?) => {{ let mut call = None; apply_range($minfd, &[$($keep_fds),*], |low, high| { assert!(call.is_none()); call = Some((low, high)); Err(()) }).unwrap_err(); assert_eq!(call.unwrap(), $call); }} } check_err!(0, [], (0, libc::c_int::MAX)); check_err!(-100, [], (-100, libc::c_int::MAX)); check_err!(3, [0, 2, 3, 4, 5, 6], (7, libc::c_int::MAX)); check_err!(3, [0, 2], (3, libc::c_int::MAX)); check_err!(3, [3, 4, 5, 6], (7, libc::c_int::MAX)); check_err!(3, [4, 5, 6], (3, 3)); check_err!(3, [5, 6, 9, 10], (3, 4)); check_err!(3, [5, 6, 9, 10, 20, 23], (3, 4),); } #[test] fn test_is_fd_valid() { assert!(!is_fd_valid(-1)); assert!(!is_fd_valid(libc::c_int::MAX)); assert!(is_fd_valid(0)); with_fd(|fd| { assert!(is_fd_valid(fd)); }); } #[test] fn test_set_cloexec() { // No panic on errors like this set_cloexec(-1); set_cloexec(libc::c_int::MAX); fn is_cloexec(fd: libc::c_int) -> bool { let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; assert!(flags >= 0); flags & libc::FD_CLOEXEC == libc::FD_CLOEXEC } with_fd(|fd| { assert!(!is_cloexec(fd)); set_cloexec(fd); assert!(is_cloexec(fd)); }); } } close_fds-0.3.2/tests/test_close_fds.rs000064400000000000000000000373140000000000000162640ustar 00000000000000use std::os::unix::prelude::*; fn is_fd_open(fd: libc::c_int) -> bool { unsafe { libc::fcntl(fd, libc::F_GETFD) >= 0 } } fn set_fd_cloexec(fd: libc::c_int, cloexec: bool) { let mut flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; if flags < 0 { return; } // We could eliminate the second fcntl() in some cases, but this is just testing code. if cloexec { flags |= libc::FD_CLOEXEC; } else { flags &= !libc::FD_CLOEXEC; } unsafe { libc::fcntl(fd, libc::F_SETFD, flags); } } fn is_fd_cloexec(fd: libc::c_int) -> Option { let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) }; if flags < 0 { None } else if flags & libc::FD_CLOEXEC == libc::FD_CLOEXEC { Some(true) } else { Some(false) } } fn check_sorted(items: &[T]) { let mut last_item = None; for item in items.iter() { if let Some(last_item) = last_item { assert!(last_item <= item); } last_item = Some(item); } } fn run_basic_test( callback: fn( fd1: libc::c_int, fd2: libc::c_int, fd3: libc::c_int, builder: close_fds::CloseFdsBuilder, ), builder: close_fds::CloseFdsBuilder, ) { let f1 = std::fs::File::open("/").unwrap(); let f2 = std::fs::File::open("/").unwrap(); let f3 = std::fs::File::open("/").unwrap(); let fd1 = f1.as_raw_fd(); let fd2 = f2.as_raw_fd(); let fd3 = f3.as_raw_fd(); drop(f3); assert!(is_fd_open(fd1)); assert!(is_fd_open(fd2)); assert!(!is_fd_open(fd3)); callback(fd1, fd2, fd3, builder.clone()); } fn iter_open_fds_test( fd1: libc::c_int, fd2: libc::c_int, fd3: libc::c_int, _builder: close_fds::CloseFdsBuilder, ) { let mut fds: Vec; for (fs, threadsafe) in [(true, true), (true, false), (false, true), (false, false)] .iter() .cloned() { let mut builder = close_fds::FdIterBuilder::new(); builder.allow_filesystem(fs); builder.threadsafe(threadsafe); fds = builder.iter_from(-1).collect(); check_sorted(&fds); assert!(!fds.contains(&-1)); assert!(fds.contains(&0)); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); assert_eq!(builder.iter_from(-1).min(), Some(0)); assert_eq!(builder.iter_from(-1).max().as_ref(), fds.last()); assert_eq!(builder.iter_from(-1).count(), fds.len()); fds = builder.iter_from(0).collect(); check_sorted(&fds); assert!(fds.contains(&0)); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); // Test handling of minfd fds = builder.iter_from(fd1).collect(); check_sorted(&fds); assert!(!fds.contains(&0)); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); fds = builder.iter_from(fd2).collect(); check_sorted(&fds); assert!(!fds.contains(&0)); assert!(!fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); fds = builder.iter_from(fd3).collect(); check_sorted(&fds); assert!(!fds.contains(&0)); assert!(!fds.contains(&fd1)); assert!(!fds.contains(&fd2)); assert!(!fds.contains(&fd3)); } } fn iter_possible_fds_test( fd1: libc::c_int, fd2: libc::c_int, fd3: libc::c_int, _builder: close_fds::CloseFdsBuilder, ) { let mut fds: Vec; for (fs, threadsafe) in [(true, true), (true, false), (false, true), (false, false)] .iter() .cloned() { let mut builder = close_fds::FdIterBuilder::new(); builder.allow_filesystem(fs); builder.threadsafe(threadsafe); builder.possible(true); fds = builder.iter_from(-1).collect(); check_sorted(&fds); assert!(!fds.contains(&-1)); assert!(fds.contains(&0)); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); // Don't check for fd3 not being present because it might legitimately be // returned by iter_possible_fds() assert_eq!(builder.iter_from(-1).min(), Some(0)); assert_eq!(builder.iter_from(-1).max().as_ref(), fds.last()); assert_eq!(builder.iter_from(-1).count(), fds.len()); fds = builder.iter_from(0).collect(); check_sorted(&fds); assert!(fds.contains(&0)); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); // Test handling of minfd fds = builder.iter_from(fd1).collect(); check_sorted(&fds); assert!(!fds.contains(&0)); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); fds = builder.iter_from(fd2).collect(); check_sorted(&fds); assert!(!fds.contains(&0)); assert!(!fds.contains(&fd1)); assert!(fds.contains(&fd2)); fds = builder.iter_from(fd3).collect(); check_sorted(&fds); assert!(!fds.contains(&0)); assert!(!fds.contains(&fd1)); assert!(!fds.contains(&fd2)); } } fn close_fds_test( fd1: libc::c_int, fd2: libc::c_int, fd3: libc::c_int, builder: close_fds::CloseFdsBuilder, ) { let mut fds: Vec; fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); // Add 0 just to check the case where keep_fds[0] < minfd unsafe { builder.clone().keep_fds(&[0]).closefrom(fd1); } fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(!fds.contains(&fd1)); assert!(!fds.contains(&fd2)); assert!(!fds.contains(&fd3)); } fn close_fds_keep1_test( fd1: libc::c_int, fd2: libc::c_int, fd3: libc::c_int, builder: close_fds::CloseFdsBuilder, ) { let mut fds: Vec; fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); unsafe { builder.clone().keep_fds(&[fd1, fd3]).closefrom(fd1); } fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(fds.contains(&fd1)); assert!(!fds.contains(&fd2)); assert!(!fds.contains(&fd3)); } fn close_fds_keep2_test( fd1: libc::c_int, fd2: libc::c_int, fd3: libc::c_int, builder: close_fds::CloseFdsBuilder, ) { let mut fds: Vec; fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); unsafe { builder.clone().keep_fds(&[fd2, fd3]).closefrom(fd1); } fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(!fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); } fn close_fds_keep3_test( fd1: libc::c_int, fd2: libc::c_int, fd3: libc::c_int, builder: close_fds::CloseFdsBuilder, ) { let mut fds: Vec; fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); unsafe { builder.clone().keep_fds_sorted(&[fd2]).closefrom(fd1); } fds = close_fds::iter_open_fds(fd1).collect(); check_sorted(&fds); assert!(!fds.contains(&fd1)); assert!(fds.contains(&fd2)); assert!(!fds.contains(&fd3)); } fn large_open_fds_test( mangle_keep_fds: fn(&mut [libc::c_int]), builder: close_fds::CloseFdsBuilder, ) { let mut openfds = Vec::new(); for _ in 0..150 { let fd = std::fs::File::open("/").unwrap().into_raw_fd(); openfds.push(fd); // Test that is_fd_cloexec() and set_fd_cloexec() behave properly assert_eq!(is_fd_cloexec(fd), Some(true)); set_fd_cloexec(fd, false); assert_eq!(is_fd_cloexec(fd), Some(false)); set_fd_cloexec(fd, true); assert_eq!(is_fd_cloexec(fd), Some(true)); } let lowfd = openfds[0]; let mut closedfds = Vec::new(); // Close a few for &index in &[140, 110, 100, 75, 10] { let fd = openfds.remove(index); unsafe { libc::close(fd); } closedfds.push(fd); assert_eq!(is_fd_cloexec(fd), None); } let mut cur_open_fds: Vec; // Check that our expectations of which should be open and which // should be closed are correct cur_open_fds = close_fds::iter_open_fds(lowfd).collect(); check_sorted(&cur_open_fds); for fd in openfds.iter() { assert_eq!(is_fd_cloexec(*fd), Some(true)); assert!(cur_open_fds.contains(fd)); } for fd in closedfds.iter() { assert_eq!(is_fd_cloexec(*fd), None); assert!(!cur_open_fds.contains(fd)); } // Set them all as non-close-on-exec for fd in openfds.iter() { set_fd_cloexec(*fd, false); assert_eq!(is_fd_cloexec(*fd), Some(false)); } // Make them all close-on-exec builder.cloexecfrom(lowfd); // Now make sure they're all close-on-exec for fd in openfds.iter() { assert_eq!(is_fd_cloexec(*fd), Some(true)); } // Set them all as non-close-on-exec again for fd in openfds.iter() { set_fd_cloexec(*fd, false); assert_eq!(is_fd_cloexec(*fd), Some(false)); } // Make them all close-on-exec with set_fds_cloexec_threadsafe() builder.clone().threadsafe(true).cloexecfrom(lowfd); // Now make sure they're all close-on-exec again for fd in openfds.iter() { assert_eq!(is_fd_cloexec(*fd), Some(true)); } // Now, let's only keep a few file descriptors open. let mut extra_closedfds = openfds.to_vec(); extra_closedfds.sort_unstable(); openfds.clear(); for &index in &[130, 110, 90, 89, 65, 34, 21, 3] { openfds.push(extra_closedfds.remove(index)); } closedfds.extend_from_slice(&extra_closedfds); mangle_keep_fds(&mut openfds); // Set them all as non-close-on-exec for fd in openfds.iter().chain(extra_closedfds.iter()) { set_fd_cloexec(*fd, false); } // Make most of them close-on-exec builder.clone().keep_fds(&openfds).cloexecfrom(lowfd); // Now make sure they're all close-on-exec again for fd in openfds.iter() { assert_eq!(is_fd_cloexec(*fd), Some(false)); } for fd in extra_closedfds.iter() { assert_eq!(is_fd_cloexec(*fd), Some(true)); } // Set them all as non-close-on-exec for fd in openfds.iter().chain(extra_closedfds.iter()) { set_fd_cloexec(*fd, false); } // Make most of them close-on-exec builder .clone() .threadsafe(true) .keep_fds(&openfds) .cloexecfrom(lowfd); // Check that it worked properly for fd in openfds.iter() { assert_eq!(is_fd_cloexec(*fd), Some(false)); } for fd in extra_closedfds.iter() { assert_eq!(is_fd_cloexec(*fd), Some(true)); } unsafe { builder.clone().keep_fds(&openfds).closefrom(lowfd); } // Again, check that our expectations match reality cur_open_fds = close_fds::iter_open_fds(lowfd).collect(); check_sorted(&cur_open_fds); for fd in openfds.iter() { assert!(cur_open_fds.contains(fd)); } for fd in closedfds.iter() { assert!(!cur_open_fds.contains(fd)); } assert_eq!( cur_open_fds, close_fds::iter_open_fds_threadsafe(lowfd).collect::>() ); for &fd in openfds.iter() { unsafe { libc::close(fd); } } } #[test] fn run_tests() { // Run all tests here because these tests can't be run in parallel for (fs, threadsafe) in [(true, true), (true, false), (false, true), (false, false)] .iter() .cloned() { let mut builder = close_fds::CloseFdsBuilder::new(); builder.allow_filesystem(fs).threadsafe(threadsafe); run_basic_test(iter_open_fds_test, builder.clone()); run_basic_test(iter_possible_fds_test, builder.clone()); run_basic_test(close_fds_test, builder.clone()); run_basic_test(close_fds_keep1_test, builder.clone()); run_basic_test(close_fds_keep2_test, builder.clone()); run_basic_test(close_fds_keep3_test, builder.clone()); large_open_fds_test(|keep_fds| keep_fds.sort_unstable(), builder.clone()); large_open_fds_test(|_keep_fds| (), builder.clone()); large_open_fds_test( |keep_fds| { keep_fds.sort_unstable_by(|a, b| ((a + b) % 5).cmp(&3)); }, builder.clone(), ); } unsafe { close_fds::close_open_fds(3, &[]); } assert_eq!( close_fds::iter_open_fds(0).collect::>(), vec![0, 1, 2] ); unsafe { close_fds::close_open_fds(0, &[0, 1, 2]); } assert_eq!( close_fds::iter_open_fds(0).collect::>(), vec![0, 1, 2] ); unsafe { close_fds::close_open_fds(-1, &[0, 1, 2]); } assert_eq!( close_fds::iter_open_fds(0).collect::>(), vec![0, 1, 2] ); } #[cfg(not(tarpaulin))] // Breaks when running under some versions of tarpaulin #[test] fn run_no_fds_tests() { // This is an edge case of the fused tests. The case where *no* file descriptors are open // requires special handling on some platforms, and this test exercises that code. // // We run this test in a separate process because we can't just close all file descriptors in // the current process. That also means that we can run it in parallel with the other tests, // since it only affects the subprocess. match unsafe { libc::fork() } { 0 => unsafe { // Close all open file descriptors close_fds::close_open_fds(0, &[]); // (Note: From here on, panic()ing is of limited value since it can't display a // backtrace. So we exit() with different values on error, so that the error code will // tell us what failed.) // Create an FdIter let mut fditer = close_fds::iter_open_fds(0); // It's empty. if fditer.next().is_some() { libc::_exit(1); } // Now we open another file descriptor. if libc::open("/\0".as_ptr() as *const libc::c_char, libc::O_RDONLY) < 0 { libc::_exit(2); } // A new FdIter knows about the new file descriptor... if close_fds::iter_open_fds(0).next() != Some(0) { libc::_exit(3); } // But the old one doesn't, because it's fused. if fditer.next().is_some() { libc::_exit(4); } // And it knows it. if fditer.size_hint() != (0, Some(0)) { libc::_exit(5); } if fditer.count() != 0 { libc::_exit(6); } libc::_exit(0); }, ret if ret < 0 => panic!("Error fork()ing: {}", std::io::Error::last_os_error()), pid => unsafe { let mut stat = 0; if libc::waitpid(pid, &mut stat, 0) < 0 { panic!( "Error wait()ing for child: {}", std::io::Error::last_os_error() ) } assert!(libc::WIFEXITED(stat), "Process did not exit normally"); assert_eq!( libc::WEXITSTATUS(stat), 0, "Process exited with non-zero value" ); }, } }