diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0ef35ca81..8bc08d9d7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,7 +32,7 @@ jobs: rust: stable - build: nightly os: ubuntu-latest - rust: nightly-2023-07-03 + rust: nightly - build: 1.48 os: ubuntu-latest rust: 1.48 @@ -62,12 +62,9 @@ jobs: i686-unknown-linux-gnu i686-unknown-linux-musl wasm32-unknown-emscripten - riscv64gc-unknown-linux-gnu aarch64-unknown-linux-gnu aarch64-unknown-linux-musl powerpc64le-unknown-linux-gnu - mipsel-unknown-linux-gnu - mips64el-unknown-linux-gnuabi64 armv5te-unknown-linux-gnueabi s390x-unknown-linux-gnu arm-linux-androideabi @@ -85,7 +82,7 @@ jobs: run: | set -ex sudo apt-get update - sudo apt-get install -y gcc-i686-linux-gnu gcc-aarch64-linux-gnu gcc-riscv64-linux-gnu gcc-arm-linux-gnueabi musl-tools + sudo apt-get install -y gcc-i686-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabi musl-tools - name: Use specific dependency versions for Rust 1.48 compatibility. if: matrix.rust == '1.48' @@ -99,6 +96,7 @@ jobs: cargo update --package=io-lifetimes --precise 1.0.6 cd - >/dev/null done + cargo update --package=tempfile --precise=3.6.0 - run: cargo check --workspace --release -vv --all-targets - run: cargo check --workspace --release -vv --features=all-apis --all-targets @@ -121,13 +119,10 @@ jobs: - run: cargo check --workspace --release -vv --target=i686-unknown-linux-musl --features=all-apis --all-targets - run: cargo check --workspace --release -vv --target=i686-unknown-linux-musl --features=use-libc,all-apis --all-targets - run: cargo check --workspace --release -vv --target=wasm32-unknown-emscripten --features=all-apis --all-targets - - run: cargo check --workspace --release -vv --target=riscv64gc-unknown-linux-gnu --features=all-apis --all-targets - run: cargo check --workspace --release -vv --target=aarch64-unknown-linux-gnu --features=all-apis --all-targets - run: cargo check --workspace --release -vv --target=aarch64-unknown-linux-musl --features=all-apis --all-targets - run: cargo check --workspace --release -vv --target=aarch64-unknown-linux-musl --features=use-libc,all-apis --all-targets - run: cargo check --workspace --release -vv --target=powerpc64le-unknown-linux-gnu --features=all-apis --all-targets - - run: cargo check --workspace --release -vv --target=mipsel-unknown-linux-gnu --features=all-apis --all-targets - - run: cargo check --workspace --release -vv --target=mips64el-unknown-linux-gnuabi64 --features=all-apis --all-targets - run: cargo check --workspace --release -vv --target=armv5te-unknown-linux-gnueabi --features=all-apis --all-targets - run: cargo check --workspace --release -vv --target=s390x-unknown-linux-gnu --features=all-apis --all-targets - run: cargo check --workspace --release -vv --target=arm-linux-androideabi --features=all-apis --all-targets @@ -149,7 +144,7 @@ jobs: include: - build: nightly os: ubuntu-latest - rust: nightly-2023-07-03 + rust: nightly env: # -D warnings is commented out in our install-rust action; re-add it here. @@ -173,7 +168,7 @@ jobs: include: - build: nightly os: ubuntu-latest - rust: nightly-2023-07-03 + rust: nightly steps: - uses: actions/checkout@v3 @@ -200,7 +195,7 @@ jobs: include: - build: nightly os: ubuntu-latest - rust: nightly-2023-07-03 + rust: nightly steps: - uses: actions/checkout@v3 @@ -232,24 +227,24 @@ jobs: QEMU_BUILD_VERSION: 7.0.0 strategy: matrix: - build: [ubuntu, ubuntu-20.04, i686-linux, aarch64-linux, powerpc64le-linux, riscv64-linux, s390x-linux, arm-linux, ubuntu-stable, ubuntu-1.48, i686-linux-stable, aarch64-linux-stable, riscv64-linux-stable, s390x-linux-stable, mipsel-linux-stable, mips64el-linux-stable, powerpc64le-linux-stable, arm-linux-stable, ubuntu-1.48, i686-linux-1.48, aarch64-linux-1.48, riscv64-linux-1.48, s390x-linux-1.48, mipsel-linux-1.48, mips64el-linux-1.48, powerpc64le-linux-1.48, arm-linux-1.48, macos-latest, macos-11, windows, windows-2019] + build: [ubuntu, ubuntu-20.04, i686-linux, aarch64-linux, powerpc64le-linux, s390x-linux, arm-linux, ubuntu-stable, ubuntu-1.48, i686-linux-stable, aarch64-linux-stable, s390x-linux-stable, powerpc64le-linux-stable, arm-linux-stable, ubuntu-1.48, i686-linux-1.48, aarch64-linux-1.48, s390x-linux-1.48, powerpc64le-linux-1.48, arm-linux-1.48, macos-latest, macos-11, windows, windows-2019] include: - build: ubuntu os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 + rust: nightly - build: ubuntu-20.04 os: ubuntu-20.04 - rust: nightly-2023-07-03 + rust: nightly - build: i686-linux os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 + rust: nightly target: i686-unknown-linux-gnu gcc_package: gcc-i686-linux-gnu gcc: i686-linux-gnu-gcc libc_package: libc-dev-i386-cross - build: aarch64-linux os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 + rust: nightly target: aarch64-unknown-linux-gnu gcc_package: gcc-aarch64-linux-gnu gcc: aarch64-linux-gnu-gcc @@ -258,43 +253,16 @@ jobs: qemu_target: aarch64-linux-user - build: powerpc64le-linux os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 + rust: nightly target: powerpc64le-unknown-linux-gnu gcc_package: gcc-powerpc64le-linux-gnu gcc: powerpc64le-linux-gnu-gcc qemu: qemu-ppc64le qemu_args: -L /usr/powerpc64le-linux-gnu qemu_target: ppc64le-linux-user - - build: mips64el-linux - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 - target: mips64el-unknown-linux-gnuabi64 - gcc_package: gcc-mips64el-linux-gnuabi64 - gcc: mips64el-linux-gnuabi64-gcc - qemu: qemu-mips64el - qemu_args: -L /usr/mips64el-linux-gnuabi64 - qemu_target: mips64el-linux-user - - build: mipsel-linux - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 - target: mipsel-unknown-linux-gnu - gcc_package: gcc-mipsel-linux-gnu - gcc: mipsel-linux-gnu-gcc - qemu: qemu-mipsel - qemu_args: -L /usr/mipsel-linux-gnu - qemu_target: mipsel-linux-user - - build: riscv64-linux - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 - target: riscv64gc-unknown-linux-gnu - gcc_package: gcc-riscv64-linux-gnu - gcc: riscv64-linux-gnu-gcc - qemu: qemu-riscv64 - qemu_args: -L /usr/riscv64-linux-gnu - qemu_target: riscv64-linux-user - build: s390x-linux os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 + rust: nightly target: s390x-unknown-linux-gnu gcc_package: gcc-s390x-linux-gnu gcc: s390x-linux-gnu-gcc @@ -303,7 +271,7 @@ jobs: qemu_target: s390x-linux-user - build: arm-linux os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: nightly-2023-07-03 + rust: nightly target: armv5te-unknown-linux-gnueabi gcc_package: gcc-arm-linux-gnueabi gcc: arm-linux-gnueabi-gcc @@ -329,15 +297,6 @@ jobs: qemu: qemu-aarch64 qemu_args: -L /usr/aarch64-linux-gnu qemu_target: aarch64-linux-user - - build: riscv64-linux-stable - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: stable - target: riscv64gc-unknown-linux-gnu - gcc_package: gcc-riscv64-linux-gnu - gcc: riscv64-linux-gnu-gcc - qemu: qemu-riscv64 - qemu_args: -L /usr/riscv64-linux-gnu - qemu_target: riscv64-linux-user - build: s390x-linux-stable os: ubuntu-20.04 # TODO: remove pin when fixed (#483) rust: stable @@ -356,24 +315,6 @@ jobs: qemu: qemu-ppc64le qemu_args: -L /usr/powerpc64le-linux-gnu qemu_target: ppc64le-linux-user - - build: mips64el-linux-stable - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: stable - target: mips64el-unknown-linux-gnuabi64 - gcc_package: gcc-mips64el-linux-gnuabi64 - gcc: mips64el-linux-gnuabi64-gcc - qemu: qemu-mips64el - qemu_args: -L /usr/mips64el-linux-gnuabi64 - qemu_target: mips64el-linux-user - - build: mipsel-linux-stable - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: stable - target: mipsel-unknown-linux-gnu - gcc_package: gcc-mipsel-linux-gnu - gcc: mipsel-linux-gnu-gcc - qemu: qemu-mipsel - qemu_args: -L /usr/mipsel-linux-gnu - qemu_target: mipsel-linux-user - build: arm-linux-stable os: ubuntu-20.04 # TODO: remove pin when fixed (#483) rust: stable @@ -402,15 +343,6 @@ jobs: qemu: qemu-aarch64 qemu_args: -L /usr/aarch64-linux-gnu qemu_target: aarch64-linux-user - - build: riscv64-linux-1.48 - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: 1.48 - target: riscv64gc-unknown-linux-gnu - gcc_package: gcc-riscv64-linux-gnu - gcc: riscv64-linux-gnu-gcc - qemu: qemu-riscv64 - qemu_args: -L /usr/riscv64-linux-gnu - qemu_target: riscv64-linux-user - build: s390x-linux-1.48 os: ubuntu-20.04 # TODO: remove pin when fixed (#483) rust: 1.48 @@ -429,24 +361,6 @@ jobs: qemu: qemu-ppc64le qemu_args: -L /usr/powerpc64le-linux-gnu qemu_target: ppc64le-linux-user - - build: mips64el-linux-1.48 - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: 1.48 - target: mips64el-unknown-linux-gnuabi64 - gcc_package: gcc-mips64el-linux-gnuabi64 - gcc: mips64el-linux-gnuabi64-gcc - qemu: qemu-mips64el - qemu_args: -L /usr/mips64el-linux-gnuabi64 - qemu_target: mips64el-linux-user - - build: mipsel-linux-1.48 - os: ubuntu-20.04 # TODO: remove pin when fixed (#483) - rust: 1.48 - target: mipsel-unknown-linux-gnu - gcc_package: gcc-mipsel-linux-gnu - gcc: mipsel-linux-gnu-gcc - qemu: qemu-mipsel - qemu_args: -L /usr/mipsel-linux-gnu - qemu_target: mipsel-linux-user - build: arm-linux-1.48 os: ubuntu-20.04 # TODO: remove pin when fixed (#483) rust: 1.48 @@ -464,10 +378,10 @@ jobs: rust: stable - build: windows os: windows-latest - rust: nightly-2023-07-03 + rust: nightly - build: windows-2019 os: windows-2019 - rust: nightly-2023-07-03 + rust: nightly steps: - uses: actions/checkout@v3 with: @@ -537,6 +451,7 @@ jobs: cargo update --package=io-lifetimes --precise 1.0.6 cd - >/dev/null done + cargo update --package=tempfile --precise=3.6.0 - run: | # Run the tests, and check the prebuilt release libraries. @@ -575,7 +490,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - build: [ubuntu, i686-linux, aarch64-linux, powerpc64le-linux, mips64el-linux, mipsel-linux, riscv64-linux, arm-linux] + build: [ubuntu, i686-linux, aarch64-linux, powerpc64le-linux, arm-linux] include: - build: ubuntu os: ubuntu-latest @@ -605,33 +520,6 @@ jobs: qemu: qemu-ppc64le qemu_args: -L /usr/powerpc64le-linux-gnu qemu_target: ppc64le-linux-user - - build: mips64el-linux - os: ubuntu-latest - rust: stable - target: mips64el-unknown-linux-gnuabi64 - gcc_package: gcc-mips64el-linux-gnuabi64 - gcc: mips64el-linux-gnuabi64-gcc - qemu: qemu-mips64el - qemu_args: -L /usr/mips64el-linux-gnuabi64 - qemu_target: mips64el-linux-user - - build: mipsel-linux - os: ubuntu-latest - rust: stable - target: mipsel-unknown-linux-gnu - gcc_package: gcc-mipsel-linux-gnu - gcc: mipsel-linux-gnu-gcc - qemu: qemu-mipsel - qemu_args: -L /usr/mipsel-linux-gnu - qemu_target: mipsel-linux-user - - build: riscv64-linux - os: ubuntu-latest - rust: stable - target: riscv64gc-unknown-linux-gnu - gcc_package: gcc-riscv64-linux-gnu - gcc: riscv64-linux-gnu-gcc - qemu: qemu-riscv64 - qemu_args: -L /usr/riscv64-linux-gnu - qemu_target: riscv64-linux-user - build: arm-linux os: ubuntu-latest rust: stable @@ -712,42 +600,22 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - build: [powerpc64le-linux, mipsel-linux, mips64el-linux] + build: [powerpc64le-linux] include: - build: powerpc64le-linux os: ubuntu-latest - rust: nightly-2023-07-03 + rust: nightly target: powerpc64le-unknown-linux-gnu gcc_package: gcc-powerpc64le-linux-gnu gcc: powerpc64le-linux-gnu-gcc qemu: qemu-ppc64le qemu_args: -L /usr/powerpc64le-linux-gnu qemu_target: ppc64le-linux-user - - build: mips64el-linux - os: ubuntu-latest - rust: nightly-2023-07-03 - target: mips64el-unknown-linux-gnuabi64 - gcc_package: gcc-mips64el-linux-gnuabi64 - gcc: mips64el-linux-gnuabi64-gcc - qemu: qemu-mips64el - qemu_args: -L /usr/mips64el-linux-gnuabi64 - qemu_target: mips64el-linux-user - - build: mipsel-linux - os: ubuntu-latest - rust: nightly-2023-07-03 - target: mipsel-unknown-linux-gnu - gcc_package: gcc-mipsel-linux-gnu - gcc: mipsel-linux-gnu-gcc - qemu: qemu-mipsel - qemu_args: -L /usr/mipsel-linux-gnu - qemu_target: mipsel-linux-user env: # -D warnings is commented out in our install-rust action; re-add it here. RUSTFLAGS: --cfg rustix_use_experimental_asm -D warnings RUSTDOCFLAGS: --cfg rustix_use_experimental_asm CARGO_TARGET_POWERPC64LE_UNKNOWN_LINUX_GNU_RUSTFLAGS: --cfg rustix_use_experimental_asm - CARGO_TARGET_MIPSEL_UNKNOWN_LINUX_GNU_RUSTFLAGS: --cfg rustix_use_experimental_asm - CARGO_TARGET_MIPS64EL_UNKNOWN_LINUX_GNUABI64_RUSTFLAGS: --cfg rustix_use_experimental_asm QEMU_BUILD_VERSION: 7.0.0 steps: - uses: actions/checkout@v3 diff --git a/Cargo.toml b/Cargo.toml index 4d1b1dc14..3f27933c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,24 +73,12 @@ tempfile = "3.2.0" libc = "0.2.133" libc_errno = { package = "errno", version = "0.3.0", default-features = false } io-lifetimes = { version = "1.0.0", default-features = false, features = ["close"] } -# Don't upgrade to serial_test 0.7 for now because it depends on a -# `parking_lot_core` version which is not compatible with our MSRV of 1.48. -serial_test = "0.6" memoffset = "0.7.1" flate2 = "1.0" -[target.'cfg(all(criterion, not(any(target_os = "emscripten", target_os = "wasi"))))'.dev-dependencies] -criterion = "0.4" - [target.'cfg(windows)'.dev-dependencies] ctor = "0.1.21" -# Add Criterion configuration, as described here: -# -[[bench]] -name = "mod" -harness = false - [package.metadata.docs.rs] features = ["all-apis"] rustdoc-args = ["--cfg", "doc_cfg"] diff --git a/README.md b/README.md index 276fa9c04..e13bcb336 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ by default. The rest of the API is conditional with cargo feature flags: ## Similar crates -`rustix` is similar to [`nix`], [`simple_libc`], [`unix`], [`nc`], and -[`uapi`]. `rustix` is architected for [I/O safety] with most APIs using +`rustix` is similar to [`nix`], [`simple_libc`], [`unix`], [`nc`], [`uapi`], +and [`rusl`]. `rustix` is architected for [I/O safety] with most APIs using [`OwnedFd`] and [`AsFd`] to manipulate file descriptors rather than `File` or even `c_int`, and supporting multiple backends so that it can use direct syscalls while still being usable on all platforms `libc` supports. Like `nix`, @@ -136,6 +136,7 @@ version of this crate. [`nc`]: https://crates.io/crates/nc [`simple_libc`]: https://crates.io/crates/simple_libc [`uapi`]: https://crates.io/crates/uapi +[`rusl`]: https://lib.rs/crates/rusl [`relibc`]: https://github.com/redox-os/relibc [`syscall`]: https://crates.io/crates/syscall [`sc`]: https://crates.io/crates/sc diff --git a/benches/mod.rs b/benches/mod.rs deleted file mode 100644 index 06831362f..000000000 --- a/benches/mod.rs +++ /dev/null @@ -1,182 +0,0 @@ -/// Benchmarks for rustix. -/// -/// To enable these benchmarks, add `--cfg=criterion` to RUSTFLAGS and enable -/// the "fs", "time", and "process" cargo features. - -#[cfg(any( - not(criterion), - not(feature = "fs"), - not(feature = "process"), - not(feature = "time"), - windows, - target_os = "emscripten", - target_os = "redox", - target_os = "wasi", -))] -fn main() { - unimplemented!() -} - -#[cfg(not(any( - not(criterion), - not(feature = "fs"), - not(feature = "process"), - not(feature = "time"), - windows, - target_os = "emscripten", - target_os = "redox", - target_os = "wasi", -)))] -use criterion::{criterion_group, criterion_main}; - -#[cfg(not(any( - not(criterion), - not(feature = "fs"), - not(feature = "process"), - not(feature = "time"), - windows, - target_os = "emscripten", - target_os = "redox", - target_os = "wasi", -)))] -mod suite { - use criterion::Criterion; - - pub(super) fn simple_statat(c: &mut Criterion) { - use rustix::fs::{cwd, statat, AtFlags}; - - c.bench_function("simple statat", |b| { - b.iter(|| { - statat(cwd(), "/", AtFlags::empty()).unwrap(); - }) - }); - } - - pub(super) fn simple_statat_libc(c: &mut Criterion) { - c.bench_function("simple statat libc", |b| { - b.iter(|| { - let mut s = std::mem::MaybeUninit::::uninit(); - unsafe { - assert_eq!( - libc::fstatat( - libc::AT_FDCWD, - std::ffi::CString::new("/").unwrap().as_c_str().as_ptr() as _, - s.as_mut_ptr(), - 0 - ), - 0 - ); - } - }) - }); - } - - pub(super) fn simple_statat_libc_cstr(c: &mut Criterion) { - c.bench_function("simple statat libc cstr", |b| { - b.iter(|| { - let mut s = std::mem::MaybeUninit::::uninit(); - unsafe { - assert_eq!( - libc::fstatat( - libc::AT_FDCWD, - rustix::cstr!("/").as_ptr() as _, - s.as_mut_ptr(), - 0 - ), - 0 - ); - } - }) - }); - } - - pub(super) fn simple_statat_cstr(c: &mut Criterion) { - use rustix::fs::{cwd, statat, AtFlags}; - - c.bench_function("simple statat cstr", |b| { - b.iter(|| { - statat(cwd(), rustix::cstr!("/"), AtFlags::empty()).unwrap(); - }) - }); - } - - #[cfg(not(target_os = "wasi"))] - pub(super) fn simple_clock_gettime(c: &mut Criterion) { - use rustix::time::{clock_gettime, ClockId}; - - c.bench_function("simple clock_gettime", |b| { - b.iter(|| { - let _ = clock_gettime(ClockId::Monotonic); - }) - }); - } - - #[cfg(not(target_os = "wasi"))] - pub(super) fn simple_clock_gettime_libc(c: &mut Criterion) { - c.bench_function("simple clock_gettime libc", |b| { - b.iter(|| { - let mut s = std::mem::MaybeUninit::::uninit(); - unsafe { - assert_eq!( - libc::clock_gettime(libc::CLOCK_MONOTONIC, s.as_mut_ptr()), - 0 - ); - let _ = s.assume_init(); - } - }) - }); - } - - #[cfg(not(target_os = "wasi"))] - pub(super) fn simple_getpid(c: &mut Criterion) { - use rustix::process::getpid; - - c.bench_function("simple getpid", |b| { - b.iter(|| { - let _ = getpid(); - }) - }); - } - - #[cfg(not(target_os = "wasi"))] - pub(super) fn simple_getpid_libc(c: &mut Criterion) { - c.bench_function("simple getpid libc", |b| { - b.iter(|| unsafe { - let _ = libc::getpid(); - }) - }); - } -} - -#[cfg(not(any( - not(criterion), - not(feature = "fs"), - not(feature = "process"), - not(feature = "time"), - windows, - target_os = "emscripten", - target_os = "redox", - target_os = "wasi", -)))] -criterion_group!( - benches, - suite::simple_statat, - suite::simple_statat_libc, - suite::simple_statat_libc_cstr, - suite::simple_statat_cstr, - suite::simple_clock_gettime, - suite::simple_clock_gettime_libc, - suite::simple_getpid, - suite::simple_getpid_libc -); -#[cfg(not(any( - not(criterion), - not(feature = "fs"), - not(feature = "process"), - not(feature = "time"), - windows, - target_os = "emscripten", - target_os = "redox", - target_os = "wasi", -)))] -criterion_main!(benches); diff --git a/src/backend/libc/fs/dir.rs b/src/backend/libc/fs/dir.rs index 6b69c3600..cb13f05bc 100644 --- a/src/backend/libc/fs/dir.rs +++ b/src/backend/libc/fs/dir.rs @@ -57,8 +57,13 @@ use core::ptr::NonNull; use libc_errno::{errno, set_errno, Errno}; /// `DIR*` -#[repr(transparent)] -pub struct Dir(NonNull); +pub struct Dir { + /// The `libc` `DIR` pointer. + libc_dir: NonNull, + + /// Have we seen any errors in this iteration? + any_errors: bool, +} impl Dir { /// Construct a `Dir` that reads entries from the given directory @@ -69,21 +74,38 @@ impl Dir { } #[inline] + #[allow(unused_mut)] fn _read_from(fd: BorrowedFd<'_>) -> io::Result { + let mut any_errors = false; + // Given an arbitrary `OwnedFd`, it's impossible to know whether the // user holds a `dup`'d copy which could continue to modify the // file description state, which would cause Undefined Behavior after // our call to `fdopendir`. To prevent this, we obtain an independent // `OwnedFd`. let flags = fcntl_getfl(fd)?; - let fd_for_dir = openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty())?; + let fd_for_dir = match openat(fd, cstr!("."), flags | OFlags::CLOEXEC, Mode::empty()) { + Ok(fd) => fd, + #[cfg(not(target_os = "wasi"))] + Err(io::Errno::NOENT) => { + // If "." doesn't exist, it means the directory was removed. + // We treat that as iterating through a directory with no + // entries. + any_errors = true; + crate::io::dup(fd)? + } + Err(err) => return Err(err), + }; let raw = owned_fd(fd_for_dir); unsafe { let libc_dir = c::fdopendir(raw); if let Some(libc_dir) = NonNull::new(libc_dir) { - Ok(Self(libc_dir)) + Ok(Self { + libc_dir, + any_errors, + }) } else { let err = io::Errno::last_os_error(); let _ = c::close(raw); @@ -95,13 +117,19 @@ impl Dir { /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { - unsafe { c::rewinddir(self.0.as_ptr()) } + self.any_errors = false; + unsafe { c::rewinddir(self.libc_dir.as_ptr()) } } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option> { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + set_errno(Errno(0)); - let dirent_ptr = unsafe { libc_readdir(self.0.as_ptr()) }; + let dirent_ptr = unsafe { libc_readdir(self.libc_dir.as_ptr()) }; if dirent_ptr.is_null() { let curr_errno = errno().0; if curr_errno == 0 { @@ -109,6 +137,7 @@ impl Dir { None } else { // `errno` is unknown or non-zero, so an error occurred. + self.any_errors = true; Some(Err(io::Errno(curr_errno))) } } else { @@ -134,7 +163,7 @@ impl Dir { /// `fstat(self)` #[inline] pub fn stat(&self) -> io::Result { - fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstat(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatfs(self)` @@ -148,7 +177,7 @@ impl Dir { )))] #[inline] pub fn statfs(&self) -> io::Result { - fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fstatvfs(self)` @@ -161,14 +190,14 @@ impl Dir { )))] #[inline] pub fn statvfs(&self) -> io::Result { - fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fstatvfs(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } /// `fchdir(self)` #[cfg(not(any(target_os = "fuchsia", target_os = "wasi")))] #[inline] pub fn chdir(&self) -> io::Result<()> { - fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.0.as_ptr())) }) + fchdir(unsafe { BorrowedFd::borrow_raw(c::dirfd(self.libc_dir.as_ptr())) }) } } @@ -342,7 +371,7 @@ unsafe impl Send for Dir {} impl Drop for Dir { #[inline] fn drop(&mut self) { - unsafe { c::closedir(self.0.as_ptr()) }; + unsafe { c::closedir(self.libc_dir.as_ptr()) }; } } @@ -358,7 +387,7 @@ impl Iterator for Dir { impl fmt::Debug for Dir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Dir") - .field("fd", unsafe { &c::dirfd(self.0.as_ptr()) }) + .field("fd", unsafe { &c::dirfd(self.libc_dir.as_ptr()) }) .finish() } } @@ -485,3 +514,52 @@ fn check_dirent_layout(dirent: &c::dirent) { } ); } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `readdir` to fail. + unsafe { + let raw_fd = c::dirfd(dir.libc_dir.as_ptr()); + let mut owned_fd: crate::fd::OwnedFd = crate::fd::FromRawFd::from_raw_fd(raw_fd); + crate::io::dup2(&file_fd, &mut owned_fd).unwrap(); + core::mem::forget(owned_fd); + } + + // FreeBSD and macOS seem to read some directory entries before we call + // `.next()`. + #[cfg(any( + target_os = "macos", + target_os = "ios", + target_os = "watchos", + target_os = "tvos", + target_os = "freebsd", + target_os = "dragonflybsd" + ))] + { + dir.rewind(); + } + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +} diff --git a/src/backend/linux_raw/fs/dir.rs b/src/backend/linux_raw/fs/dir.rs index cfa347d03..54157ade2 100644 --- a/src/backend/linux_raw/fs/dir.rs +++ b/src/backend/linux_raw/fs/dir.rs @@ -17,9 +17,17 @@ pub struct Dir { /// The `OwnedFd` that we read directory entries from. fd: OwnedFd, + /// Have we seen any errors in this iteration? + any_errors: bool, + + /// Should we rewind the stream on the next iteration? + rewind: bool, + + /// The buffer for `linux_dirent64` entries. buf: Vec, + + /// Where we are in the buffer. pos: usize, - next: Option, } impl Dir { @@ -37,25 +45,39 @@ impl Dir { Ok(Self { fd: fd_for_dir, + any_errors: false, + rewind: false, buf: Vec::new(), pos: 0, - next: None, }) } /// `rewinddir(self)` #[inline] pub fn rewind(&mut self) { + self.any_errors = false; + self.rewind = true; self.pos = self.buf.len(); - self.next = Some(0); } /// `readdir(self)`, where `None` means the end of the directory. pub fn read(&mut self) -> Option> { - if let Some(next) = self.next.take() { - match crate::backend::fs::syscalls::_seek(self.fd.as_fd(), next as i64, SEEK_SET) { + // If we've seen errors, don't continue to try to read anyting further. + if self.any_errors { + return None; + } + + // If a rewind was requested, seek to the beginning. + if self.rewind { + self.rewind = false; + match io::retry_on_intr(|| { + crate::backend::fs::syscalls::_seek(self.fd.as_fd(), 0, SEEK_SET) + }) { Ok(_) => (), - Err(err) => return Some(Err(err)), + Err(err) => { + self.any_errors = true; + return Some(Err(err)); + } } } @@ -77,7 +99,7 @@ impl Dir { if self.buf.len() - self.pos < size_of::() { match self.read_more()? { Ok(()) => (), - Err(e) => return Some(Err(e)), + Err(err) => return Some(Err(err)), } } @@ -136,14 +158,31 @@ impl Dir { } fn read_more(&mut self) -> Option> { - let og_len = self.buf.len(); - // Capacity increment currently chosen by wild guess. - self.buf - .resize(self.buf.capacity() + 32 * size_of::(), 0); - let nread = match crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) { + // The first few times we're called, we allocate a relatively small + // buffer, because many directories are small. If we're called more, + // use progressively larger allocations, up to a fixed maximum. + // + // The specific sizes and policy here have not been tuned in detail yet + // and may need to be adjusted. In doing so, we should be careful to + // avoid unbounded buffer growth. This buffer only exists to share the + // cost of a `getdents` call over many entries, so if it gets too big, + // cache and heap usage will outweigh the benefit. And ultimately, + // directories can contain more entries than we can allocate contiguous + // memory for, so we'll always need to cap the size at some point. + if self.buf.len() < 1024 * size_of::() { + self.buf.reserve(32 * size_of::()); + } + self.buf.resize(self.buf.capacity(), 0); + let nread = match io::retry_on_intr(|| { + crate::backend::fs::syscalls::getdents(self.fd.as_fd(), &mut self.buf) + }) { Ok(nread) => nread, + Err(io::Errno::NOENT) => { + self.any_errors = true; + return None; + } Err(err) => { - self.buf.resize(og_len, 0); + self.any_errors = true; return Some(Err(err)); } }; @@ -223,3 +262,33 @@ impl DirEntry { self.d_ino } } + +#[test] +fn dir_iterator_handles_io_errors() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = crate::fs::openat( + crate::fs::cwd(), + tmp.path(), + crate::fs::OFlags::RDONLY | crate::fs::OFlags::CLOEXEC, + crate::fs::Mode::empty(), + ) + .unwrap(); + + let file_fd = crate::fs::openat( + &fd, + tmp.path().join("test.txt"), + crate::fs::OFlags::WRONLY | crate::fs::OFlags::CREATE, + crate::fs::Mode::RWXU, + ) + .unwrap(); + + let mut dir = Dir::read_from(&fd).unwrap(); + + // Reach inside the `Dir` and replace its directory with a file, which + // will cause the subsequent `getdents64` to fail. + crate::io::dup2(&file_fd, &mut dir.fd).unwrap(); + + assert!(matches!(dir.next(), Some(Err(_)))); + assert!(matches!(dir.next(), None)); +} diff --git a/tests/fs/dir.rs b/tests/fs/dir.rs index f5120be96..1745dc092 100644 --- a/tests/fs/dir.rs +++ b/tests/fs/dir.rs @@ -8,7 +8,7 @@ fn test_dir() { ) .unwrap(); - let dir = rustix::fs::Dir::read_from(&t).unwrap(); + let mut dir = rustix::fs::Dir::read_from(&t).unwrap(); let _file = rustix::fs::openat( &t, @@ -18,6 +18,32 @@ fn test_dir() { ) .unwrap(); + // Read the directory entries. We use `while let Some(entry)` so that we + // don't consume the `Dir` so that we can run more tests on it. + let mut saw_dot = false; + let mut saw_dotdot = false; + let mut saw_cargo_toml = false; + while let Some(entry) = dir.read() { + let entry = entry.unwrap(); + if entry.file_name() == rustix::cstr!(".") { + saw_dot = true; + } else if entry.file_name() == rustix::cstr!("..") { + saw_dotdot = true; + } else if entry.file_name() == rustix::cstr!("Cargo.toml") { + saw_cargo_toml = true; + } + } + assert!(saw_dot); + assert!(saw_dotdot); + assert!(saw_cargo_toml); + + // Rewind the directory so we can iterate over the entries again. + dir.rewind(); + + // For what comes next, we don't need `mut` anymore. + let dir = dir; + + // Read the directory entries, again. This time we use `for entry in dir`. let mut saw_dot = false; let mut saw_dotdot = false; let mut saw_cargo_toml = false; @@ -35,3 +61,69 @@ fn test_dir() { assert!(saw_dotdot); assert!(saw_cargo_toml); } + +// Test that `Dir` silently stops iterating if the directory has been removed. +// +// Except on FreeBSD and macOS, where apparently `readdir` just keeps reading. +#[cfg_attr( + any( + target_os = "macos", + target_os = "ios", + target_os = "watchos", + target_os = "tvos", + target_os = "freebsd", + target_os = "dragonflybsd" + ), + ignore +)] +#[test] +fn dir_iterator_handles_dir_removal() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + assert!(matches!(dir.next(), None)); +} + +// Like `dir_iterator_handles_dir_removal`, but close the directory after +// `Dir::read_from`. +#[cfg_attr( + any( + target_os = "macos", + target_os = "ios", + target_os = "watchos", + target_os = "tvos", + target_os = "freebsd", + target_os = "dragonflybsd" + ), + ignore +)] +#[test] +fn dir_iterator_handles_dir_removal_after_open() { + // create a dir, keep the FD, then delete the dir + let tmp = tempfile::tempdir().unwrap(); + let fd = rustix::fs::openat( + rustix::fs::cwd(), + tmp.path(), + rustix::fs::OFlags::RDONLY | rustix::fs::OFlags::CLOEXEC, + rustix::fs::Mode::empty(), + ) + .unwrap(); + + let mut dir = rustix::fs::Dir::read_from(&fd).unwrap(); + + // Drop the `TempDir`, which deletes the directory. + drop(tmp); + + assert!(matches!(dir.next(), None)); +} diff --git a/tests/process/wait.rs b/tests/process/wait.rs index a1f25a631..2262eec33 100644 --- a/tests/process/wait.rs +++ b/tests/process/wait.rs @@ -1,6 +1,5 @@ use libc::{kill, SIGSTOP}; use rustix::process; -use serial_test::serial; use std::process::{Command, Stdio}; // These tests must execute serially to prevent race condition, where @@ -8,7 +7,7 @@ use std::process::{Command, Stdio}; // the tests to get stuck. #[test] -#[serial] +#[ignore] fn test_waitpid() { let child = Command::new("yes") .stdout(Stdio::null()) diff --git a/tests/termios/ttyname.rs b/tests/termios/ttyname.rs index 636178c31..b3f8d4843 100644 --- a/tests/termios/ttyname.rs +++ b/tests/termios/ttyname.rs @@ -4,7 +4,11 @@ use std::fs::File; #[test] fn test_ttyname_ok() { - let file = File::open("/dev/stdin").unwrap(); + let file = match File::open("/dev/stdin") { + Ok(file) => file, + Err(err) if err.kind() == std::io::ErrorKind::PermissionDenied => return, + Err(err) => Err(err).unwrap(), + }; if isatty(&file) { assert!(ttyname(&file, Vec::new()) .unwrap()