diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..c32342f4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# git and GitHub-related files. +/.git* + +# No need to break the COPY cache for Docker-specific files. +/Dockerfile +/.dockerignore + +# Rust. +/target +**/*.rs.bk + +# Python +**/__pycache__/ +/contrib/bindings/python/dist +/contrib/bindings/python/*.egg-info +/contrib/bindings/python/*_cache + +# Releases directory. +/release + +# pkg-config generated by install.sh. +/pathrs.pc + +# nextest archives generated by CI. +/nextest-pathrs*.tar.zst + +# examples and e2e-test binaries. +/examples/*/cat +/examples/go/sysctl +/examples/c/cat_multithreaded +/e2e-tests/cmd/*/pathrs-cmd +/e2e-tests/cmd/python/.venv/ diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f7c8bdfb..ef0424a7 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,6 +24,7 @@ name: e2e-tests env: BATS_VERSION: "1.11.1" + CI_IMAGE: cyphar/libpathrs:ci-latest jobs: e2e-test: @@ -34,7 +35,7 @@ jobs: - go - rust - python - runas: + run-as: - "" - "root" lang-desc: [""] @@ -60,7 +61,7 @@ jobs: ${{ format('({0}{1})', matrix.lang-desc || matrix.lang, - matrix.runas && format(', {0}', matrix.runas) || '', + matrix.run-as && format(', {0}', matrix.run-as) || '', ) }} runs-on: ubuntu-latest @@ -114,11 +115,81 @@ jobs: - name: make -C e2e-tests test-${{ matrix.lang }} run: |- export BATS=$(which bats) - make -C e2e-tests RUN_AS=${{ matrix.runas }} test-${{ matrix.lang }} + make -C e2e-tests RUN_AS=${{ matrix.run-as }} test-${{ matrix.lang }} + + ctr-ci-image: + runs-on: ubuntu-latest + name: build ci docker image + steps: + - uses: actions/checkout@v6 + - name: setup docker buildx + uses: docker/setup-buildx-action@v4 + - name: build and cache ci image + uses: docker/build-push-action@v7 + with: + context: . + tags: ${{ env.CI_IMAGE }} + cache-from: type=gha + cache-to: type=gha,mode=max + + ctr-e2e-test: + runs-on: ubuntu-latest + needs: + - ctr-ci-image + strategy: + fail-fast: false + matrix: + lang: + - python + - go + - rust + runtime: + - docker + run-as: + - unpriv + - CAP_SYS_ADMIN + env: + CONTAINER_RUNTIME: ${{ matrix.runtime }} + # NOTE: For the root tests we need to disable AppArmor because it blocks + # mount operations, even in child mount namespaces. + CONTAINER_RUN_ARGS: >- + ${{ matrix.run-as == 'CAP_SYS_ADMIN' && '--cap-add sys_admin --security-opt=apparmor=unconfined' || '' }} + ${{ matrix.run-as == 'unpriv' && '--user 1000:1000' || '' }} + E2E_LANG: ${{ matrix.lang }} + name: >- + (${{ matrix.runtime }}) + run e2e-tests + (${{ matrix.lang }}, ${{ matrix.run-as }}) + steps: + - uses: actions/checkout@v6 + # Pull the image from the cache by triggering a "new build". + - name: setup docker buildx + uses: docker/setup-buildx-action@v4 + # TODO: Ideally we would be able to pull the image from the cache without + # needing to trigger another build. In the worst case we could just + # upload the CI image in the ctr-ci-image job and load it here. + - name: build and cache ci image + uses: docker/build-push-action@v7 + with: + context: . + tags: ${{ env.CI_IMAGE }} + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + # Run the tests. + - run: >- + mkdir -p ./target && chmod a+rwx ./target + - name: ${{ matrix.runtime }} run ${{ matrix.lang }} e2e-tests (run as ${{ matrix.run-as }}) + run: >- + "$CONTAINER_RUNTIME" run --rm $CONTAINER_RUN_ARGS \ + -v $PWD/target:/usr/src/libpathrs/target \ + "$CI_IMAGE" \ + make -C e2e-tests "test-$E2E_LANG" e2e-complete: needs: - e2e-test + - ctr-e2e-test runs-on: ubuntu-latest steps: - run: echo "End-to-end test CI jobs completed successfully." diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 44cb57f0..2f60a490 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -25,6 +25,7 @@ name: rust-ci env: RUST_MSRV: &RUST_MSRV "1.63" CBINDGEN_VERSION: "0.29.2" + CI_IMAGE: cyphar/libpathrs:ci-latest jobs: codespell: @@ -231,7 +232,7 @@ jobs: FEATURES: >- capi _test_race - ${{ matrix.run-as == 'root' && '_test_as_root' || '' }} + ${{ matrix.run-as == 'root' && '_test_as_root _test_can_mknod' || '' }} steps: - uses: actions/checkout@v6 # Nightly rust is required for llvm-cov --doc. @@ -296,6 +297,7 @@ jobs: echo "data=$(jq -ScM 'map("\(.)")' <<<"$partitions")" >>"$GITHUB_OUTPUT" nextest: + runs-on: ubuntu-latest needs: - compute-test-partitions - nextest-archive @@ -329,7 +331,6 @@ jobs: matrix.enosys && format(', {0}=enosys', matrix.enosys) || '', ) }} - runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 # Nightly rust is required for llvm-cov --doc. @@ -385,6 +386,106 @@ jobs: slug: cyphar/libpathrs files: ${{ steps.codecov-coverage.outputs.file }} + ctr-ci-image: + runs-on: ubuntu-latest + name: build ci docker image + steps: + - uses: actions/checkout@v6 + - name: setup docker buildx + uses: docker/setup-buildx-action@v4 + - name: build and cache ci image + uses: docker/build-push-action@v7 + with: + context: . + tags: ${{ env.CI_IMAGE }} + cache-from: type=gha + cache-to: type=gha,mode=max + + ctr-nextest: + runs-on: ubuntu-latest + needs: + - ctr-ci-image + - compute-test-partitions + strategy: + fail-fast: false + matrix: + tests: ${{ fromJSON(needs.compute-test-partitions.outputs.tests) }} + runtime: + - docker + run-as: + - unpriv + - CAP_SYS_ADMIN + env: + NEXTEST_PATTERN_SPEC: ${{ fromJSON(matrix.tests).pattern }} + CONTAINER_RUNTIME: ${{ matrix.runtime }} + # NOTE: For the root tests we need to disable AppArmor because it blocks + # mount operations, even in child mount namespaces. + CONTAINER_RUN_ARGS: >- + ${{ matrix.run-as == 'CAP_SYS_ADMIN' && '--cap-add sys_admin --security-opt=apparmor=unconfined' || '' }} + ${{ matrix.run-as == 'unpriv' && '--user 1000:1000' || '' }} + name: >- + (${{ matrix.runtime }}) + cargo nextest + (${{ fromJSON(matrix.tests).name }}, ${{ matrix.run-as }}) + steps: + - uses: actions/checkout@v6 + # Nightly rust is required for llvm-cov --doc. + - uses: dtolnay/rust-toolchain@nightly + with: + components: llvm-tools + - uses: taiki-e/install-action@cargo-llvm-cov + - uses: taiki-e/install-action@nextest + - name: install llvm-tools wrappers + uses: taiki-e/install-action@v2 + with: + tool: cargo-binutils + # Pull the image from the cache by triggering a "new build". + - name: setup docker buildx + uses: docker/setup-buildx-action@v4 + # TODO: Ideally we would be able to pull the image from the cache without + # needing to trigger another build. In the worst case we could just + # upload the CI image in the ctr-ci-image job and load it here. + - name: build and cache ci image + uses: docker/build-push-action@v7 + with: + context: . + tags: ${{ env.CI_IMAGE }} + load: true + cache-from: type=gha + cache-to: type=gha,mode=max + # Run the tests. + - run: >- + mkdir -p ./target && chmod a+rwx ./target + - name: ${{ matrix.runtime }} run ./hack/rust-tests.sh (run as ${{ matrix.run-as }}) + run: >- + "$CONTAINER_RUNTIME" run --rm $CONTAINER_RUN_ARGS \ + -v $PWD/target:/usr/src/libpathrs/target \ + "$CI_IMAGE" \ + ./hack/rust-tests.sh "$NEXTEST_PATTERN_SPEC" + - run: >- + sudo chown -R "$UID" ./target + + # Upload to CodeCov. + - name: generate codecov-friendly coverage + id: codecov-coverage + run: |- + CODECOV_FILE="$(mktemp coverage-codecov.lcov.txt.XXXXXX)" + cargo llvm-cov report --lcov --output-path="$CODECOV_FILE" + echo "file=$CODECOV_FILE" >>"$GITHUB_OUTPUT" + - name: upload rust coverage (codecov) + uses: codecov/codecov-action@v6 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: cyphar/libpathrs + files: ${{ steps.codecov-coverage.outputs.file }} + + - name: upload rust coverage (artifact) + uses: actions/upload-artifact@v7 + with: + name: profraw-${{ github.job }}-${{ strategy.job-index }} + path: "target/llvm-cov-target/*.profraw" + retention-days: 7 # no need to waste disk space + # Smoke-test for our %check section in the libpathrs RPM. # # TODO: I guess we should run this as root too... @@ -401,6 +502,7 @@ jobs: needs: - doctest - nextest + - ctr-nextest name: compute coverage runs-on: ubuntu-latest steps: @@ -545,6 +647,7 @@ jobs: - rustdoc - doctest - nextest + - ctr-nextest - cargo-test - coverage - examples diff --git a/Cargo.toml b/Cargo.toml index 2a292631..981a81c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ capi = ["dep:bytemuck", "bitflags/bytemuck", "dep:rand", "dep:open-enum"] # not be used by actual users of libpathrs! The leading "_" should mean that # they are hidden from documentation (such as the features list on crates.io). _test_as_root = [] +_test_can_mknod = [] _test_race = [] [profile.release] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..74196c4c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,139 @@ +# SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later +# +# libpathrs: safe path resolution on Linux +# Copyright (C) 2026 Aleksa Sarai +# +# == MPL-2.0 == +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +# +# Alternatively, this Source Code Form may also (at your option) be used +# under the terms of the GNU Lesser General Public License Version 3, as +# described below: +# +# == LGPL-3.0-or-later == +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +ARG DEBIAN_RELEASE=trixie +ARG RUST_VERSION=1.96 + +# --------------------------------------------------------------------------- # +# build: builds libpathrs for use by CI and the "install" image. +# --------------------------------------------------------------------------- # +FROM rust:${RUST_VERSION}-${DEBIAN_RELEASE} AS build + +RUN apt-get update -y && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + clang \ + lld \ + make \ + pkg-config && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /usr/src/libpathrs +COPY . /usr/src/libpathrs +RUN make release && \ + DESTDIR=/opt/libpathrs ./install.sh --prefix=/usr --libdir=/usr/lib + +# ---------------------------------------------------------------------------- +# install: minimal runtime image with libpathrs installed system-wide. +# Intended to be used as a base image by downstream projects on distros that do +# not ship a libpathrs package yet. +# ---------------------------------------------------------------------------- +FROM debian:${DEBIAN_RELEASE} AS install + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + pkg-config && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=build /opt/libpathrs/ / +# debian doesn't use /usr/lib for the native architecture so we need to make +# sure it gets searched by the link loader with ldconfig. +RUN ldconfig + +# ---------------------------------------------------------------------------- +# ci: full test runner for CI and local test runs. +# This can run the Rust unit/integration tests and the e2e tests. +# ---------------------------------------------------------------------------- +ARG RUST_VERSION=1.96 +FROM rust:${RUST_VERSION}-${DEBIAN_RELEASE} AS ci + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + bats \ + curl \ + clang \ + git \ + golang-go \ + jq \ + lld \ + llvm \ + moreutils \ + python3 \ + python3-build \ + python3-dev \ + python3-pip \ + python3-setuptools \ + python3-venv \ + sudo && \ + apt-get clean -y && \ + rm -rf /var/lib/apt/lists/* + +ARG CARGO_BINSTALL_VERSION=1.19.1 +RUN CARGO_BINSTALL_VERSION="$CARGO_BINSTALL_VERSION" \ + curl -L --proto '=https' --tlsv1.2 -sSf \ + "https://raw.githubusercontent.com/cargo-bins/cargo-binstall/v$CARGO_BINSTALL_VERSION/install-from-binstall-release.sh" | bash + +ARG CARGO_LLVM_COV_VERSION=0.8.7 +ARG CARGO_HACK_VERSION=0.6.45 +ARG CARGO_NEXTEST_VERSION=0.9.137 +RUN cargo binstall --no-confirm \ + "cargo-llvm-cov@$CARGO_LLVM_COV_VERSION" \ + "cargo-hack@$CARGO_HACK_VERSION" \ + "cargo-nextest@$CARGO_NEXTEST_VERSION" + +ARG RUST_NIGHTLY=nightly-2026-06-03 +RUN rustup toolchain install "$RUST_NIGHTLY" && \ + rustup component add llvm-tools llvm-tools-preview && \ + rustup component add --toolchain "$RUST_NIGHTLY" llvm-tools llvm-tools-preview +ENV CARGO_NIGHTLY="cargo +$RUST_NIGHTLY" + +# We want the installed libpathrs library for the Python and Go tests. +COPY --from=build /opt/libpathrs/ / +# Debian doesn't use /usr/lib for the native architecture so we need to make +# sure it gets searched by the link loader with ldconfig. +RUN ldconfig + +WORKDIR /usr/src/libpathrs +COPY . /usr/src/libpathrs + +# Populate the cache for test runs and make sure the ownership is friendly for +# non-root. +FROM ci AS ci-with-cache +RUN cargo test --workspace --all-features --no-run && \ + $CARGO_NIGHTLY llvm-cov --workspace --doc --all-features --no-report && \ + find "$CARGO_HOME" /usr/src/libpathrs -type d -print0 | xargs -0 -P$(nproc) chmod a+rwx && \ + find "$CARGO_HOME" /usr/src/libpathrs -type f -print0 | xargs -0 -P$(nproc) chmod a+rw diff --git a/hack/rust-tests.sh b/hack/rust-tests.sh index 08822a4f..e8c512df 100755 --- a/hack/rust-tests.sh +++ b/hack/rust-tests.sh @@ -34,8 +34,12 @@ set -Eeuo pipefail SRC_ROOT="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")/..")" +function error() { + echo "[err]" "$@" >&2 +} + function bail() { - echo "rust tests: $*" >&2 + error "rust tests:" "$*" exit 1 } @@ -62,13 +66,28 @@ function strjoin() { echo "$str" } -TEMP="$(getopt -o sc:p:S: --long sudo,cargo:,partition:,enosys:,archive-file: -- "$@")" +TEMP="$(getopt -o h,sc:p:S: --long help,sudo,cargo:,partition:,enosys:,archive-file:,report-output-path: -- "$@")" eval set -- "$TEMP" +function usage() { + [ "$#" -gt 0 ] && error "$@" + cat <] + [--partition=] + [--archive-file=] + [--report-output-path=] + [--enosys=,...] + [TESTS_TO_RUN]... +EOF + # shellcheck disable=SC2048 # We want to only expand to nothing or 1. + exit ${*:+1} +} + sudo= partition= enosys_syscalls=() nextest_archive= +report_output_path= CARGO="${CARGO_NIGHTLY:-cargo +nightly}" while [ "$#" -gt 0 ]; do case "$1" in @@ -92,12 +111,19 @@ while [ "$#" -gt 0 ]; do [ -n "$2" ] && enosys_syscalls+=("$2") shift 2 ;; + --report-output-path) + report_output_path="$2" + shift 2 + ;; + -h|--help) + usage + ;; --) shift break ;; *) - bail "unknown option $1" + usage "unknown option $1" esac done tests_to_run=("$@") @@ -145,6 +171,7 @@ function llvm-profdata() { function merge_llvmcov_profdata() { local llvmcov_targetdir=target/llvm-cov-target + local report_output_path="${1:-$llvmcov_targetdir/libpathrs-combined.profraw}" # Get a list of *.profraw files for merging. local profraw_list @@ -162,7 +189,7 @@ function merge_llvmcov_profdata() { # Remove the old profiling data and replace it with the merged version. As # long as the file has a ".profraw" suffix, cargo-llvm-cov will use it. find "$llvmcov_targetdir" -name '*.profraw' -type f -delete - mv "$combined_profraw" "$llvmcov_targetdir/libpathrs-combined.profraw" + mv "$combined_profraw" "$report_output_path" } function nextest_run() { @@ -191,6 +218,8 @@ function nextest_run() { # This CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER magic lets us run # Rust tests as root without needing to run the build step as root. export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER="sudo -E " + elif [ "$(id -u)" -eq 0 ]; then + features+=("_test_as_root") fi build_args=() @@ -273,3 +302,8 @@ else nextest_run --no-fail-fast -E "not test(#tests::test_race_*)" nextest_run --no-fail-fast -E "test(#tests::test_race_*)" fi + +# Output the final report to the requested file. +if [ -n "$report_output_path" ]; then + merge_llvmcov_profdata "$report_output_path" +fi diff --git a/src/tests/capi/test_compat.rs b/src/tests/capi/test_compat.rs index dfdfa12f..ab4e5fa2 100644 --- a/src/tests/capi/test_compat.rs +++ b/src/tests/capi/test_compat.rs @@ -53,12 +53,13 @@ use pretty_assertions::{assert_eq, assert_matches}; #[test] fn reopen_v1() -> Result<(), Error> { - let file: OwnedFd = File::open(".")?.into(); + let file: OwnedFd = File::open(".").context("open dummy file")?.into(); - let oflags = OpenFlags::O_DIRECTORY | OpenFlags::O_RDONLY | OpenFlags::O_NOATIME; + let oflags = OpenFlags::O_DIRECTORY | OpenFlags::O_RDONLY | OpenFlags::O_NOCTTY; let reopened_fd = capi_utils::call_capi_fd(|| { capi::core::__pathrs_reopen_v1(file.as_fd().into(), oflags.bits() as i32) - })?; + }) + .with_context(|| format!("__pathrs_reopen_v1({oflags:?})"))?; assert_ne!( file.as_raw_fd(), @@ -66,23 +67,26 @@ fn reopen_v1() -> Result<(), Error> { "new and reopened fds should have different fd numbers" ); assert_eq!( - file.as_unsafe_path_unchecked()?, - reopened_fd.as_unsafe_path_unchecked()?, + file.as_unsafe_path_unchecked() + .expect("get real path of original fd"), + reopened_fd + .as_unsafe_path_unchecked() + .expect("get real path of reopened fd"), "new and reopened fds should have the same 'real' path", ); - tests_common::check_oflags(&reopened_fd, oflags)?; + tests_common::check_oflags(&reopened_fd, oflags).expect("check reopened fd flags"); Ok(()) } #[test] fn inroot_open_v1() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(root_dir.path())?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(root_dir.path()).context("Root::open basic tree")?; { let path = capi_utils::path_to_cstring("b/c"); - let oflags = OpenFlags::O_DIRECTORY | OpenFlags::O_RDONLY | OpenFlags::O_NOATIME; + let oflags = OpenFlags::O_DIRECTORY | OpenFlags::O_RDONLY | OpenFlags::O_NOCTTY; // SAFETY: Called with valid C-like arguments. let file = capi_utils::call_capi_fd(|| unsafe { capi::core::__pathrs_inroot_open_v1( @@ -90,8 +94,10 @@ fn inroot_open_v1() -> Result<(), Error> { path.as_ptr(), oflags.bits() as _, ) - })?; - tests_common::check_oflags(&file, oflags)?; + }) + .with_context(|| format!("__pathrs_inroot_open_v1({path:?}, {oflags:?})"))?; + tests_common::check_oflags(&file, oflags) + .with_context(|| format!("check {path:?} {oflags:?} oflags"))?; } { @@ -104,8 +110,10 @@ fn inroot_open_v1() -> Result<(), Error> { path.as_ptr(), oflags.bits() as _, ) - })?; - tests_common::check_oflags(&file, oflags)?; + }) + .with_context(|| format!("__pathrs_inroot_open_v1({path:?}, {oflags:?})"))?; + tests_common::check_oflags(&file, oflags) + .with_context(|| format!("check {path:?} {oflags:?} oflags"))?; } { @@ -118,8 +126,10 @@ fn inroot_open_v1() -> Result<(), Error> { path.as_ptr(), oflags.bits() as _, ) - })?; - tests_common::check_oflags(&file, oflags)?; + }) + .with_context(|| format!("__pathrs_inroot_open_v1({path:?}, {oflags:?})"))?; + tests_common::check_oflags(&file, oflags) + .with_context(|| format!("check {path:?} {oflags:?} oflags"))?; } Ok(()) @@ -127,12 +137,12 @@ fn inroot_open_v1() -> Result<(), Error> { #[test] fn inroot_creat_v1() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(root_dir.path())?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(root_dir.path()).context("Root::open basic tree")?; { let path = capi_utils::path_to_cstring("b/c/new-file"); - let oflags = OpenFlags::O_RDWR | OpenFlags::O_NOATIME | OpenFlags::O_EXCL; + let oflags = OpenFlags::O_RDWR | OpenFlags::O_NOCTTY | OpenFlags::O_EXCL; let mode = 0o644; // SAFETY: Called with valid C-like arguments. let file = capi_utils::call_capi_fd(|| unsafe { @@ -142,9 +152,12 @@ fn inroot_creat_v1() -> Result<(), Error> { oflags.bits() as _, mode, ) - })?; - tests_common::check_oflags(&file, oflags | OpenFlags::O_CREAT)?; - tests_common::check_mode(&file, libc::S_IFREG | mode)?; + }) + .with_context(|| format!("__pathrs_inroot_creat_v1({path:?}, {oflags:?}, 0o{mode:o})"))?; + tests_common::check_mode(&file, libc::S_IFREG | mode) + .with_context(|| format!("check created {path:?} file mode 0o{mode:o}"))?; + tests_common::check_oflags(&file, oflags) + .with_context(|| format!("check created {path:?} {oflags:?} oflags"))?; } Ok(()) @@ -152,8 +165,8 @@ fn inroot_creat_v1() -> Result<(), Error> { #[test] fn hardlink_v1() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(root_dir.path())?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(root_dir.path()).context("Root::open basic tree")?; let path1 = capi_utils::path_to_cstring("abc"); let path2 = capi_utils::path_to_cstring("b/c/file"); @@ -172,14 +185,15 @@ fn hardlink_v1() -> Result<(), Error> { ); let old_meta = root - .resolve_nofollow("b/c/file")? + .resolve_nofollow("b/c/file") + .expect("resolve b/c/file") .metadata() - .context("fstat b/c/file")?; + .expect("fstat b/c/file"); let new_meta = root .resolve_nofollow("abc") - .context("hardlink abc should've been created")? + .expect("hardlink abc should've been created") .metadata() - .context("fstat hardlink abc")?; + .expect("fstat hardlink abc"); assert_eq!( old_meta.ino(), new_meta.ino(), @@ -191,9 +205,9 @@ fn hardlink_v1() -> Result<(), Error> { #[test] fn hardlink_v2() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root1 = Root::open(root_dir.path())?; - let root2 = Root::open(root_dir.path())?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root1 = Root::open(root_dir.path()).context("Root::open basic tree (#1)")?; + let root2 = Root::open(root_dir.path()).context("Root::open basic tree (#2)")?; let path1 = capi_utils::path_to_cstring("abc"); let path2 = capi_utils::path_to_cstring("b/c/file"); @@ -234,8 +248,8 @@ fn hardlink_v2() -> Result<(), Error> { #[test] fn symlink_v1() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(root_dir.path())?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(root_dir.path()).context("Root::open basic tree")?; let path1 = capi_utils::path_to_cstring("abc"); let path2 = capi_utils::path_to_cstring("b/c/file"); @@ -264,8 +278,8 @@ fn symlink_v1() -> Result<(), Error> { #[test] fn rename_v1() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(root_dir.path())?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(root_dir.path()).context("Root::open basic tree")?; let path1 = capi_utils::path_to_cstring("b/c/file"); let path2 = capi_utils::path_to_cstring("abc"); @@ -301,9 +315,9 @@ fn rename_v1() -> Result<(), Error> { #[test] fn rename_v2() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root1 = Root::open(root_dir.path())?; - let root2 = Root::open(root_dir.path())?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root1 = Root::open(root_dir.path()).context("Root::open basic tree (#1)")?; + let root2 = Root::open(root_dir.path()).context("Root::open basic tree (#2)")?; let path1 = capi_utils::path_to_cstring("b/c/file"); let path2 = capi_utils::path_to_cstring("abc"); @@ -337,15 +351,19 @@ fn procfs_open_v1() -> Result<(), Error> { path.as_ptr(), oflags.bits() as _, ) + }) + .with_context(|| { + format!("__pathrs_proc_open_v1(PATHRS_PROC_THREAD_SELF, {path:?}, {oflags:?})") })?; - tests_common::check_oflags(&file, oflags)?; + tests_common::check_oflags(&file, oflags) + .with_context(|| format!("check procfs {path:?} {oflags:?} oflags"))?; Ok(()) } #[test] fn procfs_openat_v1() -> Result<(), Error> { - let proc_rootfd = ProcfsHandle::new()?; + let proc_rootfd = ProcfsHandle::new().context("ProcfsHandle::new")?; let path = capi_utils::path_to_cstring("stat"); let oflags = OpenFlags::O_RDONLY | OpenFlags::O_NOFOLLOW; // SAFETY: Called with valid C-like arguments. @@ -356,8 +374,12 @@ fn procfs_openat_v1() -> Result<(), Error> { path.as_ptr(), oflags.bits() as _, ) + }) + .with_context(|| { + format!("__pathrs_proc_openat_v1(PATHRS_PROC_THREAD_SELF, {path:?}, {oflags:?})") })?; - tests_common::check_oflags(&file, oflags)?; + tests_common::check_oflags(&file, oflags) + .with_context(|| format!("check procfs {path:?} {oflags:?} oflags"))?; Ok(()) } diff --git a/src/tests/common/handle.rs b/src/tests/common/handle.rs index 356c9c21..77f7d4af 100644 --- a/src/tests/common/handle.rs +++ b/src/tests/common/handle.rs @@ -112,7 +112,7 @@ pub(in crate::tests) fn check_mode(fd: impl AsFd, create_mode: u32) -> Result<() umask.bits() }; - let got_mode = fd.metadata()?.mode() + let got_mode = fd.metadata().context("fstat fd to check mode")?.mode() // Strip type bits from mode if the caller didn't include them. & !if create_mode & libc::S_IFMT == 0 { libc::S_IFMT @@ -124,7 +124,7 @@ pub(in crate::tests) fn check_mode(fd: impl AsFd, create_mode: u32) -> Result<() create_mode & !umask, got_mode, "created fd {:?} should have mode {} ({create_mode} &^ {umask})", - fd.as_unsafe_path_unchecked()?, + fd.as_unsafe_path_unchecked().expect("get real path of fd"), create_mode & !umask ); @@ -194,7 +194,9 @@ pub(in crate::tests) fn check_reopen( (Ok(f), Ok(_)) => f, (result, expected) => { let result = match result { - Ok(file) => Ok(file.as_unsafe_path_unchecked()?), + Ok(file) => Ok(file + .as_unsafe_path_unchecked() + .context("get real path of reopened file")?), Err(err) => Err(err), }; @@ -209,16 +211,22 @@ pub(in crate::tests) fn check_reopen( } }; - let real_handle_path = handle.as_unsafe_path_unchecked()?; - let real_reopen_path = file.as_unsafe_path_unchecked()?; + let real_handle_path = handle + .as_unsafe_path_unchecked() + .context("get real path of handle")?; + let real_reopen_path = file + .as_unsafe_path_unchecked() + .context("get real path of reopened file")?; assert_eq!( real_handle_path, real_reopen_path, "reopened handle should be equivalent to old handle", ); - let clone_handle = handle.try_clone()?; - let clone_handle_path = clone_handle.as_unsafe_path_unchecked()?; + let clone_handle = handle.try_clone().context("clone handle")?; + let clone_handle_path = clone_handle + .as_unsafe_path_unchecked() + .context("get real path of cloned handle")?; assert_eq!( real_handle_path, clone_handle_path, @@ -229,7 +237,8 @@ pub(in crate::tests) fn check_reopen( &file, // NOTE: Handle::reopen() drops O_NOFOLLOW, so we shouldn't see it. flags.difference(OpenFlags::O_NOFOLLOW), - )?; + ) + .context("check reopened file flags")?; Ok(()) } diff --git a/src/tests/common/mntns.rs b/src/tests/common/mntns.rs index 5e1a1d25..380ef41b 100644 --- a/src/tests/common/mntns.rs +++ b/src/tests/common/mntns.rs @@ -78,7 +78,8 @@ pub(in crate::tests) fn mount(dst: impl AsRef, ty: MountType) -> Result<() dst, OpenFlags::O_NOFOLLOW | OpenFlags::O_PATH, 0, - )?; + ) + .with_context(|| format!("open mount destination {dst:?}"))?; let dst_path = format!("/proc/self/fd/{}", dst_file.as_raw_fd()); match ty { @@ -94,10 +95,11 @@ pub(in crate::tests) fn mount(dst: impl AsRef, ty: MountType) -> Result<() MountType::Bind { src } => { let src_file = syscalls::openat( syscalls::AT_FDCWD, - src, + &src, OpenFlags::O_NOFOLLOW | OpenFlags::O_PATH, 0, - )?; + ) + .with_context(|| format!("open bind-mount source {src:?}"))?; let src_path = format!("/proc/self/fd/{}", src_file.as_raw_fd()); rustix_mount::mount_bind(&src_path, &dst_path).with_context(|| { format!( @@ -135,7 +137,8 @@ pub(in crate::tests) fn mount(dst: impl AsRef, ty: MountType) -> Result<() dst, OpenFlags::O_NOFOLLOW | OpenFlags::O_PATH, 0, - )?; + ) + .with_context(|| format!("re-open mount destination {dst:?} for remount"))?; let dst_path = format!("/proc/self/fd/{}", dst_file.as_raw_fd()); // Then apply our mount flags. @@ -157,7 +160,7 @@ pub(in crate::tests) fn in_mnt_ns(func: F) -> Result where F: FnOnce() -> Result, { - let old_ns = File::open("/proc/self/ns/mnt")?; + let old_ns = File::open("/proc/self/ns/mnt").context("open current mount namespace")?; // TODO: Run this in a subprocess. @@ -171,7 +174,8 @@ where rustix_mount::mount_change( "/", MountPropagationFlags::DOWNSTREAM | MountPropagationFlags::REC, - )?; + ) + .context("mark / as MS_SLAVE")?; let ret = func(); diff --git a/src/tests/common/root.rs b/src/tests/common/root.rs index c7c70471..9157a53c 100644 --- a/src/tests/common/root.rs +++ b/src/tests/common/root.rs @@ -149,7 +149,7 @@ macro_rules! create_tree { // } ($($subpath:expr => $(#[$meta:meta])* ($($inner:tt)*));+ $(;)*) => { { - let root = TempDir::new()?; + let root = TempDir::new().context("create temporary dir for test tree")?; $( $(#[$meta])* { diff --git a/src/tests/test_race_resolve_partial.rs b/src/tests/test_race_resolve_partial.rs index fd0b69f8..50473dd4 100644 --- a/src/tests/test_race_resolve_partial.rs +++ b/src/tests/test_race_resolve_partial.rs @@ -41,7 +41,7 @@ use crate::{ use std::{os::unix::io::AsFd, sync::mpsc, thread}; -use anyhow::Error; +use anyhow::{Context, Error}; macro_rules! resolve_race_tests { // resolve_race_tests! { @@ -54,7 +54,7 @@ macro_rules! resolve_race_tests { #[cfg_attr(not(feature = "_test_race"), ignore)] fn []() -> Result<(), Error> { let (tmpdir, root_dir) = $root_dir; - let mut $root_var = Root::open(&root_dir)?; + let mut $root_var = Root::open(&root_dir).context("Root::open")?; assert_eq!( $root_var.resolver_backend(), ResolverBackend::default(), @@ -74,7 +74,7 @@ macro_rules! resolve_race_tests { #[cfg_attr(not(feature = "_test_race"), ignore)] fn []() -> Result<(), Error> { let (tmpdir, root_dir) = $root_dir; - let mut $root_var = Root::open(&root_dir)?; + let mut $root_var = Root::open(&root_dir).context("Root::open")?; $root_var.set_resolver_backend(ResolverBackend::KernelOpenat2); assert_eq!( $root_var.resolver_backend(), @@ -107,7 +107,7 @@ macro_rules! resolve_race_tests { } let (tmpdir, root_dir) = $root_dir; - let mut $root_var = Root::open(&root_dir)?; + let mut $root_var = Root::open(&root_dir).context("Root::open")?; $root_var.set_resolver_backend(ResolverBackend::EmulatedOpath); assert_eq!( $root_var.resolver_backend(), @@ -133,7 +133,7 @@ macro_rules! resolve_race_tests { (@impl [$rename_a:literal <=> $rename_b:literal] $test_name:ident $op_name:ident ($path:expr, $rflags:expr, $no_follow_trailing:expr) => { $($expected:tt)* }) => { paste::paste! { resolve_race_tests! { - [tests_common::create_race_tree()?] + [tests_common::create_race_tree().context("create race tree")?] fn [<$op_name _ $test_name>](mut root: Root) { root.set_resolver_flags($rflags); @@ -822,7 +822,7 @@ mod utils { sync::mpsc::{Receiver, RecvError, SyncSender, TryRecvError}, }; - use anyhow::Error; + use anyhow::{Context, Error}; use path_clean::PathClean; pub(super) enum RenameStateMsg { @@ -904,7 +904,9 @@ mod utils { .send(RenameStateMsg::Pause) .expect("should be able to pause rename attack"); - let root_dir = root.as_unsafe_path_unchecked()?; + let root_dir = root + .as_unsafe_path_unchecked() + .context("get real path of root")?; // Convert the handle to something useful for our tests. let result = result.map(|lookup_result| { diff --git a/src/tests/test_resolve.rs b/src/tests/test_resolve.rs index b00b4f95..7da44a7d 100644 --- a/src/tests/test_resolve.rs +++ b/src/tests/test_resolve.rs @@ -36,7 +36,7 @@ use crate::{error::ErrorKind, flags::ResolverFlags, resolvers::ResolverBackend, use std::path::Path; -use anyhow::Error; +use anyhow::{Context, Error}; macro_rules! resolve_tests { // resolve_tests! { @@ -51,7 +51,7 @@ macro_rules! resolve_tests { $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { - let mut $root_var = Root::open(root_dir)?; + let mut $root_var = Root::open(root_dir).context("Root::open")?; assert_eq!( $root_var.resolver_backend(), ResolverBackend::default(), @@ -70,7 +70,7 @@ macro_rules! resolve_tests { $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { - let root = Root::open(root_dir)?; + let root = Root::open(root_dir).context("Root::open")?; let mut $root_var = root.as_ref(); assert_eq!( $root_var.resolver_backend(), @@ -90,7 +90,7 @@ macro_rules! resolve_tests { $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { - let mut $root_var = Root::open(root_dir)?; + let mut $root_var = Root::open(root_dir).context("Root::open")?; $root_var.set_resolver_backend(ResolverBackend::KernelOpenat2); assert_eq!( $root_var.resolver_backend(), @@ -113,7 +113,7 @@ macro_rules! resolve_tests { $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { - let root = Root::open(root_dir)?; + let root = Root::open(root_dir).context("Root::open")?; let mut $root_var = root.as_ref(); $root_var.set_resolver_backend(ResolverBackend::KernelOpenat2); assert_eq!( @@ -145,7 +145,7 @@ macro_rules! resolve_tests { } utils::$with_root_fn(|root_dir: &Path| { - let mut $root_var = Root::open(root_dir)?; + let mut $root_var = Root::open(root_dir).context("Root::open")?; $root_var.set_resolver_backend(ResolverBackend::EmulatedOpath); assert_eq!( $root_var.resolver_backend(), @@ -177,7 +177,7 @@ macro_rules! resolve_tests { } utils::$with_root_fn(|root_dir: &Path| { - let root = Root::open(root_dir)?; + let root = Root::open(root_dir).context("Root::open")?; let mut $root_var = root .as_ref(); $root_var.set_resolver_backend(ResolverBackend::EmulatedOpath); @@ -209,7 +209,7 @@ macro_rules! resolve_tests { $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { - let $root_var = CapiRoot::open(root_dir)?; + let $root_var = CapiRoot::open(root_dir).context("CapiRoot::open")?; { $body } @@ -548,7 +548,7 @@ mod utils { where F: FnOnce(&Path) -> Result<(), Error>, { - let root_dir = tests_common::create_basic_tree()?; + let root_dir = tests_common::create_basic_tree().context("create_basic_tree")?; let res = func(root_dir.path()); @@ -568,9 +568,10 @@ mod utils { F: FnOnce(&Path) -> Result<(), Error>, { tests_common::in_mnt_ns(|| { - let root_dir = tests_common::create_basic_tree()?; + let root_dir = tests_common::create_basic_tree().context("create_basic_tree")?; - tests_common::mask_nosymfollow(root_dir.path())?; + tests_common::mask_nosymfollow(root_dir.path()) + .with_context(|| format!("could not mask {root_dir:?} with MS_NOSYMFOLLOW"))?; let res = func(root_dir.path()); @@ -590,7 +591,9 @@ mod utils { R: RootImpl, for<'a> &'a R::Handle: HandleImpl, { - let root_dir = root.as_unsafe_path_unchecked()?; + let root_dir = root + .as_unsafe_path_unchecked() + .context("get real path of root")?; let unsafe_path = unsafe_path.as_ref(); let result = if no_follow_trailing { @@ -607,7 +610,9 @@ mod utils { ), (result, expected) => { let result = match result { - Ok(handle) => Ok(handle.as_unsafe_path_unchecked()?), + Ok(handle) => Ok(handle + .as_unsafe_path_unchecked() + .context("get real path of resolved handle")?), Err(err) => Err(err), }; @@ -623,14 +628,16 @@ mod utils { }; let expected_path = expected_path.trim_start_matches('/'); - let real_handle_path = handle.as_unsafe_path_unchecked()?; + let real_handle_path = handle + .as_unsafe_path_unchecked() + .context("get real path of resolved handle")?; assert_eq!( real_handle_path, root_dir.join(expected_path), "resolve({unsafe_path:?}, {no_follow_trailing}) path mismatch", ); - let meta = handle.metadata()?; + let meta = handle.metadata().context("fstat resolved handle")?; let real_file_type = meta.mode() & libc::S_IFMT; assert_eq!(real_file_type, expected_file_type, "file type mismatch",); diff --git a/src/tests/test_resolve_partial.rs b/src/tests/test_resolve_partial.rs index 65e07ed0..a13b7111 100644 --- a/src/tests/test_resolve_partial.rs +++ b/src/tests/test_resolve_partial.rs @@ -740,8 +740,8 @@ mod utils { where F: FnOnce(Root) -> Result<(), Error>, { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(&root_dir)?; + let root_dir = tests_common::create_basic_tree().context("create_basic_tree")?; + let root = Root::open(&root_dir).context("Root::open")?; let res = func(root); @@ -754,10 +754,11 @@ mod utils { F: FnOnce(Root) -> Result<(), Error>, { tests_common::in_mnt_ns(|| { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(&root_dir)?; + let root_dir = tests_common::create_basic_tree().context("create_basic_tree")?; + let root = Root::open(&root_dir).context("Root::open")?; - tests_common::mask_nosymfollow(root_dir.path())?; + tests_common::mask_nosymfollow(root_dir.path()) + .with_context(|| format!("could not mask {root_dir:?} with MS_NOSYMFOLLOW"))?; let res = func(root); @@ -772,7 +773,9 @@ mod utils { no_follow_trailing: bool, expected: Result, ErrorKind>, ) -> Result<(), Error> { - let root_dir = root.as_unsafe_path_unchecked()?; + let root_dir = root + .as_unsafe_path_unchecked() + .context("get real path of root")?; let unsafe_path = unsafe_path.as_ref(); let result = root diff --git a/src/tests/test_root_ops.rs b/src/tests/test_root_ops.rs index c494da14..67215be4 100644 --- a/src/tests/test_root_ops.rs +++ b/src/tests/test_root_ops.rs @@ -43,7 +43,7 @@ use crate::{ use std::{fs::Permissions, os::unix::fs::PermissionsExt}; -use anyhow::Error; +use anyhow::{Context, Error}; macro_rules! root_op_tests { ($(#[$meta:meta])* fn $test_name:ident ($root_var:ident) $body:block) => { @@ -51,8 +51,8 @@ macro_rules! root_op_tests { #[test] $(#[$meta])* fn []() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let $root_var = Root::open(&root_dir)?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let $root_var = Root::open(&root_dir).context("Root::open basic tree")?; $body } @@ -60,8 +60,8 @@ macro_rules! root_op_tests { #[test] $(#[$meta])* fn []() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(&root_dir)?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(&root_dir).context("Root::open basic tree")?; let $root_var = root.as_ref(); $body @@ -70,8 +70,9 @@ macro_rules! root_op_tests { #[test] $(#[$meta])* fn []() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let $root_var = Root::open(&root_dir)? + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let $root_var = Root::open(&root_dir) + .context("Root::open basic tree")? .with_resolver_backend(ResolverBackend::KernelOpenat2); if !$root_var.resolver_backend().supported() { // Skip if not supported. @@ -84,8 +85,8 @@ macro_rules! root_op_tests { #[test] $(#[$meta])* fn []() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(&root_dir)?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(&root_dir).context("Root::open basic tree")?; let $root_var = root .as_ref() .with_resolver_backend(ResolverBackend::KernelOpenat2); @@ -109,8 +110,9 @@ macro_rules! root_op_tests { return Ok(()); } - let root_dir = tests_common::create_basic_tree()?; - let $root_var = Root::open(&root_dir)? + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let $root_var = Root::open(&root_dir) + .context("Root::open basic tree")? .with_resolver_backend(ResolverBackend::EmulatedOpath); // EmulatedOpath is always supported. assert!( @@ -133,8 +135,8 @@ macro_rules! root_op_tests { return Ok(()); } - let root_dir = tests_common::create_basic_tree()?; - let root = Root::open(&root_dir)?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let root = Root::open(&root_dir).context("Root::open basic tree")?; let $root_var = root .as_ref() .with_resolver_backend(ResolverBackend::EmulatedOpath); @@ -151,8 +153,8 @@ macro_rules! root_op_tests { #[cfg(feature = "capi")] $(#[$meta])* fn []() -> Result<(), Error> { - let root_dir = tests_common::create_basic_tree()?; - let $root_var = capi::CapiRoot::open(&root_dir)?; + let root_dir = tests_common::create_basic_tree().context("create basic tree")?; + let $root_var = capi::CapiRoot::open(&root_dir).context("CapiRoot::open basic tree")?; $body } @@ -500,42 +502,78 @@ root_op_tests! { root_dotdot: mkfifo("..", 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: mkfifo("../", 0o755) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] plain: mkblk("abc", 0o001, 123, 456) => Ok(("abc", libc::S_IFBLK | 0o001)); + #[cfg(feature = "_test_can_mknod")] exist_file: mkblk("b/c/file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] exist_dir: mkblk("a", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] exist_symlink: mkblk("b-file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] exist_dangling_symlink: mkblk("a-fake1", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] parentdir_trailing_slash: mkblk("b/c//foobar", 0o123, 123, 456) => Ok(("b/c/foobar", libc::S_IFBLK | 0o123)); + #[cfg(feature = "_test_can_mknod")] parentdir_trailing_dot: mkblk("b/c/./foobar", 0o456, 123, 456) => Ok(("b/c/foobar", libc::S_IFBLK | 0o456)); + #[cfg(feature = "_test_can_mknod")] parentdir_trailing_dotdot: mkblk("b/c/../foobar", 0o321, 123, 456) => Ok(("b/foobar", libc::S_IFBLK | 0o321)); + #[cfg(feature = "_test_can_mknod")] trailing_slash1: mkblk("foobar/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] trailing_slash2: mkblk("foobar///", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] trailing_dot: mkblk("foobar/.", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] trailing_dotdot: mkblk("foobar/..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_slash: mkblk("/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_slash2: mkblk("//", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dot: mkblk(".", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dot_trailing_slash: mkblk("./", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dotdot: mkblk("..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dotdot_trailing_slash: mkblk("../", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] plain: mkchar("abc", 0o010, 111, 222) => Ok(("abc", libc::S_IFCHR | 0o010)); + #[cfg(feature = "_test_can_mknod")] exist_file: mkchar("b/c/file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] exist_dir: mkchar("a", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] exist_symlink: mkchar("b-file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] exist_dangling_symlink: mkchar("a-fake1", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] parentdir_trailing_slash: mkchar("b/c//foobar", 0o123, 123, 456) => Ok(("b/c/foobar", libc::S_IFCHR | 0o123)); + #[cfg(feature = "_test_can_mknod")] parentdir_trailing_dot: mkchar("b/c/./foobar", 0o456, 123, 456) => Ok(("b/c/foobar", libc::S_IFCHR | 0o456)); + #[cfg(feature = "_test_can_mknod")] parentdir_trailing_dotdot: mkchar("b/c/../foobar", 0o321, 123, 456) => Ok(("b/foobar", libc::S_IFCHR | 0o321)); + #[cfg(feature = "_test_can_mknod")] trailing_slash1: mkchar("foobar/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] trailing_slash2: mkchar("foobar///", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] trailing_dot: mkchar("foobar/.", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] trailing_dotdot: mkchar("foobar/..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_slash: mkchar("/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_slash2: mkchar("//", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dot: mkchar(".", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dot_trailing_slash: mkchar("./", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dotdot: mkchar("..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); + #[cfg(feature = "_test_can_mknod")] root_dotdot_trailing_slash: mkchar("../", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); plain: create_file("abc", O_RDONLY, 0o100) => Ok("abc"); @@ -783,7 +821,9 @@ root_op_tests! { noreplace_symlink: rename("a", "b-file", RenameFlags::RENAME_NOREPLACE) => Err(ErrorKind::OsError(Some(libc::EEXIST))); noreplace_dangling_symlink: rename("a", "a-fake1", RenameFlags::RENAME_NOREPLACE) => Err(ErrorKind::OsError(Some(libc::EEXIST))); noreplace_eexist: rename("a", "e", RenameFlags::RENAME_NOREPLACE) => Err(ErrorKind::OsError(Some(libc::EEXIST))); + #[cfg(feature = "_test_can_mknod")] whiteout_dir: rename("a", "aa", RenameFlags::RENAME_WHITEOUT) => Ok(()); + #[cfg(feature = "_test_can_mknod")] whiteout_file: rename("b/c/file", "b/c/newfile", RenameFlags::RENAME_WHITEOUT) => Ok(()); exchange_dir: rename("a", "b", RenameFlags::RENAME_EXCHANGE) => Ok(()); exchange_dir_trailing_slash_from: rename("a/", "b", RenameFlags::RENAME_EXCHANGE) => Ok(()); @@ -920,7 +960,7 @@ mod utils { }; fn root_roundtrip(root: R) -> Result { - let root_clone = root.try_clone()?; + let root_clone = root.try_clone().context("clone root")?; assert_eq!( root.resolver(), root_clone.resolver(), @@ -943,7 +983,10 @@ mod utils { let _ = rustix_process::umask(Mode::empty()); // Update the expected path to have the rootdir as a prefix. - let root_dir = root.as_fd().as_unsafe_path_unchecked()?; + let root_dir = root + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of root")?; let expected_result = expected_result.map(|(path, mode)| (root_dir.join(path), mode)); match root.create(path, &inode_type) { @@ -953,10 +996,15 @@ mod utils { } Ok(_) => { let root = root_roundtrip(root)?; - let created = root.resolve_nofollow(path)?; - let meta = created.metadata()?; + let created = root + .resolve_nofollow(path) + .with_context(|| format!("resolve created {path:?}"))?; + let meta = created.metadata().context("fstat created inode")?; - let actual_path = created.as_fd().as_unsafe_path_unchecked()?; + let actual_path = created + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of created inode")?; let actual_mode = meta.mode(); assert_eq!( Ok((actual_path.clone(), actual_mode)), @@ -973,7 +1021,12 @@ mod utils { } // Check hardlink is the same inode. InodeType::Hardlink(target) => { - let target_meta = root.resolve_nofollow(target)?.as_fd().metadata()?; + let target_meta = root + .resolve_nofollow(&target) + .with_context(|| format!("resolve hardlink target {target:?}"))? + .as_fd() + .metadata() + .context("fstat hardlink target")?; assert_eq!( meta.ino(), target_meta.ino(), @@ -983,13 +1036,16 @@ mod utils { // Check symlink is correct. InodeType::Symlink(target) => { // Check using the a resolved handle. - let actual_target = syscalls::readlinkat(&created, "")?; + let actual_target = syscalls::readlinkat(&created, "") + .context("readlink created symlink")?; assert_eq!( target, actual_target, "readlinkat(handle) link target mismatch" ); // Double-check with Root::readlink. - let actual_target = root.readlink(path)?; + let actual_target = root + .readlink(path) + .with_context(|| format!("root readlink {path:?}"))?; assert_eq!( target, actual_target, "root.readlink(path) link target mismatch" @@ -1017,7 +1073,10 @@ mod utils { let pre_create_handle = root.resolve_nofollow(path); // do not unwrap // Update the expected path to have the rootdir as a prefix. - let root_dir = root.as_fd().as_unsafe_path_unchecked()?; + let root_dir = root + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of root")?; let expected_result = expected_result.map(|path| root_dir.join(path)); match root.create_file(path, oflags, perm) { @@ -1026,7 +1085,10 @@ mod utils { .with_context(|| format!("root create file {path:?}"))?; } Ok(file) => { - let actual_path = file.as_fd().as_unsafe_path_unchecked()?; + let actual_path = file + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of created file")?; assert_eq!( Ok(actual_path.clone()), expected_result, @@ -1039,18 +1101,27 @@ mod utils { .wrap("re-open created file using original path")?; assert_eq!( - new_lookup.as_fd().as_unsafe_path_unchecked()?, - file.as_fd().as_unsafe_path_unchecked()?, + new_lookup + .as_fd() + .as_unsafe_path_unchecked() + .expect("get real path of re-opened file"), + file.as_fd() + .as_unsafe_path_unchecked() + .expect("get real path of created file"), "expected real path of {path:?} handles to be the same", ); let expect_mode = if let Ok(handle) = pre_create_handle { - handle.as_fd().metadata()?.mode() + handle + .as_fd() + .metadata() + .context("fstat pre-existing file")? + .mode() } else { libc::S_IFREG | perm.mode() }; - let orig_meta = file.as_fd().metadata()?; + let orig_meta = file.as_fd().metadata().context("fstat created file")?; assert_eq!( orig_meta.mode(), expect_mode, @@ -1058,7 +1129,10 @@ mod utils { orig_meta.mode(), ); - let new_meta = new_lookup.as_fd().metadata()?; + let new_meta = new_lookup + .as_fd() + .metadata() + .context("fstat re-opened file")?; assert_eq!( orig_meta.ino(), new_meta.ino(), @@ -1068,7 +1142,8 @@ mod utils { // Note that create_file is always implemented as a two-step // process (open the parent, create the file) with O_NOFOLLOW // always being applied to the created handle (to avoid races). - tests_common::check_oflags(&file, oflags | OpenFlags::O_NOFOLLOW)?; + tests_common::check_oflags(&file, oflags | OpenFlags::O_NOFOLLOW) + .context("check created file flags")?; } } Ok(()) @@ -1083,7 +1158,10 @@ mod utils { let path = path.as_ref(); // Update the expected path to have the rootdir as a prefix. - let root_dir = root.as_fd().as_unsafe_path_unchecked()?; + let root_dir = root + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of root")?; let expected_result = expected_result.map(|path| root_dir.join(path)); match root.open_subpath(path, oflags) { @@ -1092,7 +1170,10 @@ mod utils { .with_context(|| format!("root open subpath {path:?}"))?; } Ok(file) => { - let actual_path = file.as_fd().as_unsafe_path_unchecked()?; + let actual_path = file + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of opened subpath")?; assert_eq!( Ok(actual_path.clone()), expected_result, @@ -1108,12 +1189,17 @@ mod utils { .wrap("re-open created file using original path")?; assert_eq!( - new_lookup.as_fd().as_unsafe_path_unchecked()?, - file.as_fd().as_unsafe_path_unchecked()?, + new_lookup + .as_fd() + .as_unsafe_path_unchecked() + .expect("get real path of re-opened file"), + file.as_fd() + .as_unsafe_path_unchecked() + .expect("get real path of opened subpath"), "expected real path of {path:?} handles to be the same", ); - tests_common::check_oflags(&file, oflags)?; + tests_common::check_oflags(&file, oflags).context("check opened subpath flags")?; } } Ok(()) @@ -1166,7 +1252,7 @@ mod utils { // It's possible that the path didn't exist for remove_all, but if // it did check that it was unlinked. if let Ok(handle) = handle { - let meta = handle.as_fd().metadata()?; + let meta = handle.as_fd().metadata().context("fstat removed inode")?; assert_eq!(meta.nlink(), 0, "deleted file should have a 0 nlink"); } @@ -1244,12 +1330,22 @@ mod utils { // Keep track of the original paths, pre-rename. let src_real_path = if let Ok(ref handle) = src_handle { - Some(handle.as_fd().as_unsafe_path_unchecked()?) + Some( + handle + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of rename source")?, + ) } else { None }; let dst_real_path = if let Ok(ref handle) = dst_handle { - Some(handle.as_fd().as_unsafe_path_unchecked()?) + Some( + handle + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of rename destination")?, + ) } else { None }; @@ -1265,7 +1361,10 @@ mod utils { let src_real_path = src_real_path.unwrap(); // Confirm that the handle was moved. - let moved_src_real_path = src_handle.as_fd().as_unsafe_path_unchecked()?; + let moved_src_real_path = src_handle + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of moved source")?; assert_ne!( src_real_path, moved_src_real_path, "expected real path of handle to move after rename" @@ -1285,7 +1384,10 @@ mod utils { ); // Confirm that the destination was also moved. - let moved_dst_real_path = dst_handle.as_fd().as_unsafe_path_unchecked()?; + let moved_dst_real_path = dst_handle + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of moved destination")?; assert_eq!( src_real_path, moved_dst_real_path, "expected real path of destination to move to source with RENAME_EXCHANGE" @@ -1298,7 +1400,10 @@ mod utils { .resolve_nofollow(src_path) .wrap("expected source to exist with RENAME_WHITEOUT")?; - let meta = new_lookup.as_fd().metadata()?; + let meta = new_lookup + .as_fd() + .metadata() + .context("fstat whiteout entry")?; assert_eq!( syscalls::devmajorminor(meta.rdev()), (0, 0), @@ -1317,7 +1422,10 @@ mod utils { let src_real_path = src_real_path.unwrap(); // Confirm the handle was not moved. - let nonmoved_src_real_path = src_handle.as_fd().as_unsafe_path_unchecked()?; + let nonmoved_src_real_path = src_handle + .as_fd() + .as_unsafe_path_unchecked() + .context("get real path of unmoved source")?; assert_eq!( src_real_path, nonmoved_src_real_path, "expected real path of handle to not change after failed rename" @@ -1337,7 +1445,10 @@ mod utils { // Before trying to create the directory tree, figure out what // components don't exist yet so we can check them later. - let before_partial_lookup = root.resolver().resolve_partial(root, unsafe_path, false)?; + let before_partial_lookup = root + .resolver() + .resolve_partial(root, unsafe_path, false) + .with_context(|| format!("resolve_partial {unsafe_path:?} before mkdir_all"))?; let expected_subdir_state: Option<((_, _), _)> = match expected_result { Err(_) => None, @@ -1350,7 +1461,7 @@ mod utils { let mut expected_mode = libc::S_IFDIR | (perm.mode() & !0o022); let handle: &Handle = before_partial_lookup.as_ref(); - let dir_meta = handle.metadata()?; + let dir_meta = handle.metadata().context("fstat partial lookup handle")?; if dir_meta.mode() & libc::S_ISGID == libc::S_ISGID { expected_gid = dir_meta.gid(); expected_mode |= libc::S_ISGID; @@ -1433,7 +1544,8 @@ mod utils { let mut limit = rustix_process::getrlimit(Resource::Nofile); limit.current = limit.maximum; limit - })?; + }) + .context("raise NOFILE rlimit")?; // Do lots of runs to try to catch any possible races. let num_retries = 100 + 1_000 / (1 + (num_threads >> 5)); @@ -1471,7 +1583,8 @@ mod utils { let mut limit = rustix_process::getrlimit(Resource::Nofile); limit.current = limit.maximum; limit - })?; + }) + .context("raise NOFILE rlimit")?; // Do lots of runs to try to catch any possible races. let num_retries = 100 + 1_000 / (1 + (num_threads >> 5));