pathrs-0.2.1/.cargo_vcs_info.json0000644000000001360000000000100123350ustar { "git": { "sha1": "f900452f21ff9fa092ea5250de5634beaae0f082" }, "path_in_vcs": "" }pathrs-0.2.1/.git-blame-ignore-revs000064400000000000000000000002411046102023000152220ustar 00000000000000# python bindings: reformat with black c2924e9e77d258587ed9aa160525f3d434780686 # examples: python: reformat with black 9abf966f98e675925613fa7d692a9673e6b5a1fc pathrs-0.2.1/.github/dependabot.yml000064400000000000000000000013071046102023000153160ustar 00000000000000# Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: # Dependencies list in Cargo.{toml,lock}. - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" # Transitive dependencies that violate our MSRV. ignore: # MSRV(1.65) - dependency-name: "textwrap" versions: [ ">=0.16.2" ] # MSRV(1.65) - dependency-name: "once_cell" versions: [ ">=1.21.0" ] # Dependencies listed in .github/workflows/*.yml - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" pathrs-0.2.1/.github/workflows/bindings-c.yml000064400000000000000000000020731046102023000172640ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. on: push: branches: [ main ] tags: - 'v*' pull_request: branches: [ main ] release: types: [ published ] schedule: - cron: '0 0 * * *' name: bindings-c jobs: smoke-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Build and install libpathrs.so. - uses: dtolnay/rust-toolchain@stable - name: build libpathrs run: make release - name: install libpathrs run: sudo ./install.sh --libdir=/usr/lib # Run smoke-tests. - run: make -C examples/c smoke-test c-complete: needs: - smoke-test runs-on: ubuntu-latest steps: - run: echo "C CI jobs completed successfully." pathrs-0.2.1/.github/workflows/bindings-go.yml000064400000000000000000000051061046102023000174470ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. on: push: branches: [ main ] tags: - 'v*' pull_request: branches: [ main ] release: types: [ published ] schedule: - cron: '0 0 * * *' name: bindings-go env: GO_VERSION: 1.25.x jobs: lint: permissions: contents: read pull-requests: read checks: write # to allow the action to annotate code in the pr. runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Build and install libpathrs.so. - uses: dtolnay/rust-toolchain@stable - name: build libpathrs run: make release - name: install libpathrs run: sudo ./install.sh --libdir=/usr/lib # Run golangci-lint. - uses: golangci/golangci-lint-action@v8 with: version: v2.5 working-directory: go-pathrs go-fix: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 with: fetch-depth: 2 # Build and install libpathrs.so. - uses: dtolnay/rust-toolchain@stable - name: build libpathrs run: make release - name: install libpathrs run: sudo ./install.sh --libdir=/usr/lib # Run go-fix. - uses: actions/setup-go@v6 with: go-version: "${{ env.GO_VERSION }}" - name: run go fix run: | cd go-pathrs go fix ./... git diff --exit-code smoke-test: strategy: fail-fast: false matrix: go-version: - "1.18" - "oldstable" - "stable" runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Build and install libpathrs.so. - uses: dtolnay/rust-toolchain@stable - name: build libpathrs run: make release - name: install libpathrs run: sudo ./install.sh --libdir=/usr/lib # Setup go. - name: install go ${{ matrix.go-version }} uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} check-latest: true # Run smoke-tests. - run: make -C examples/go smoke-test go-complete: needs: - lint - go-fix - smoke-test runs-on: ubuntu-latest steps: - run: echo "Go CI jobs completed successfully." pathrs-0.2.1/.github/workflows/bindings-python.yml000064400000000000000000000127011046102023000203620ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. on: push: branches: [ main ] tags: - 'v*' pull_request: branches: [ main ] release: types: [ published ] schedule: - cron: '0 0 * * *' env: PYTHON_DIST: ${{ github.workspace }}/.tmp/python3-pathrs-${{ github.run_id }}-${{ github.run_attempt }} name: bindings-python jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: astral-sh/ruff-action@v3 with: args: "--version" - run: ruff check - run: ruff format --check --diff mypy: permissions: contents: read pull-requests: read checks: write # allow the action to annotate code runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Set up python venv. - uses: actions/setup-python@v6 - name: install mypy run: >- python3 -m pip install --user mypy - uses: tsuyoshicho/action-mypy@v5 with: github_token: ${{ secrets.github_token }} reporter: github-check workdir: contrib/bindings/python/pathrs fail_on_error: true build-pyproject: strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.x"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Build and install libpathrs.so. - uses: dtolnay/rust-toolchain@stable - name: build libpathrs run: make release - name: install libpathrs run: sudo ./install.sh --libdir=/usr/lib # Set up python venv. - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: install pypa/build run: >- python3 -m pip install --user build twine # Build and install our bindings. - name: build python-pathrs bindings run: make -C contrib/bindings/python dist - run: twine check contrib/bindings/python/dist/* - name: install python-pathrs bindings run: make -C contrib/bindings/python install # Verify that the crate and python bindings have the same version. # TODO: Move this to a "make check" we can run locally as well. - name: check crate and python binding versions match run: | CRATE_VERSION="$(cargo metadata --no-deps --format-version=1 | jq -rM '.packages[] | select(.name == "pathrs") | "\(.name)-\(.version)"')" PY_VERSION="$(python3 -c 'import importlib.metadata; print("pathrs-" + importlib.metadata.version("pathrs"))')" echo "rust crate version: $CRATE_VERSION"; echo "python module version: $PY_VERSION"; [[ "$CRATE_VERSION" == "$PY_VERSION" ]] || exit 1 # Include the dist/ in our artefacts. - name: upload python-pathrs bindings dist/ uses: actions/upload-artifact@v5 with: name: python-${{ matrix.python-version }}-pathrs-dist path: contrib/bindings/python/dist/ smoke-test: strategy: fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.x"] needs: - build-pyproject runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Build and install libpathrs.so. - uses: dtolnay/rust-toolchain@stable - name: build libpathrs run: make release - name: install libpathrs run: sudo ./install.sh --libdir=/usr/lib # Set up python venv. - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} # Download the pre-built python dist. - name: download built python-pathrs uses: actions/download-artifact@v6 with: name: python-${{ matrix.python-version }}-pathrs-dist path: ${{ env.PYTHON_DIST }} - name: install python-pathrs run: |- python3 -m pip install ${{ env.PYTHON_DIST }}/*.whl # Run smoke-tests. - run: make -C examples/python smoke-test python-complete: needs: - ruff - mypy - build-pyproject - smoke-test runs-on: ubuntu-latest steps: - run: echo "Python CI jobs completed successfully." # TODO: Should we move this to a separate workflow? release-pypi: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') needs: - build-pyproject - python-complete runs-on: ubuntu-latest environment: name: release-pypi url: "https://pypi.org/p/pathrs" permissions: id-token: write steps: - name: download built python-pathrs uses: actions/download-artifact@v6 with: name: python-3.x-pathrs-dist path: ${{ env.PYTHON_DIST }} # PyPI doesn't let us upload our native wheel because we aren't building # using the restrictive manylinux set of libraries (because we depend on # libpathrs.so). - name: remove wheel from python-pathrs run: rm -fv ${{ env.PYTHON_DIST }}/*.whl - name: upload python-pathrs to pypi uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: ${{ env.PYTHON_DIST }} pathrs-0.2.1/.github/workflows/e2e-tests.yml000064400000000000000000000064161046102023000170670ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. on: push: branches: [ main ] tags: - 'v*' pull_request: branches: [ main ] release: types: [ published ] schedule: - cron: '0 0 * * *' name: e2e-tests env: BATS_VERSION: "1.11.1" jobs: e2e-test: strategy: fail-fast: false matrix: lang: - go - rust - python runas: - "" - "root" lang-desc: [""] include: # Test minimum python version. # TODO: Switch to python 3.9 (pathrs bindings version). # typing.Self: python >= 3.11 # match: python >= 3.10 - lang: python lang-desc: python3.11 python-version: "3.11" # Test minimum Go version. # TODO: Switch to Go 1.18 (go-pathrs bindings version). # strings.FieldsFuncSeq: go >= 1.24 # urfave/cli/v3: go >= 1.22 - lang: go lang-desc: go1.24 go-version: "1.24" # FIXME: This is horrific. name: >- run e2e-tests ${{ format('({0}{1})', matrix.lang-desc || matrix.lang, matrix.runas && format(', {0}', matrix.runas) || '', ) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - name: install package run: |- sudo apt-get update -y sudo apt-get install -y moreutils # Build and install libpathrs.so. - if: ${{ matrix.lang != 'rust' }} uses: dtolnay/rust-toolchain@stable - if: ${{ matrix.lang != 'rust' }} name: build libpathrs run: make release - if: ${{ matrix.lang != 'rust' }} name: install libpathrs run: sudo ./install.sh --prefix=/usr --libdir=/usr/lib # Setup go. - if: ${{ matrix.lang == 'go' }} name: install go uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version || 'stable' }} check-latest: true # Setup python. - if: ${{ matrix.lang == 'python' }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - if: ${{ matrix.lang == 'python' }} name: install pypa/build run: >- python3 -m pip install --user build twine # Install bats. - name: install bats uses: bats-core/bats-action@3.0.1 with: bats-version: ${{ env.BATS_VERSION }} support-install: false assert-install: false detik-install: false file-install: false # Run tests. - name: make -C e2e-tests test-${{ matrix.lang }} run: |- export BATS=$(which bats) make -C e2e-tests RUN_AS=${{ matrix.runas }} test-${{ matrix.lang }} e2e-complete: needs: - e2e-test runs-on: ubuntu-latest steps: - run: echo "End-to-end test CI jobs completed successfully." pathrs-0.2.1/.github/workflows/rust.yml000064400000000000000000000353501046102023000162500ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. on: push: branches: [ main ] tags: - 'v*' pull_request: branches: [ main ] release: types: [ published ] schedule: - cron: '0 0 * * *' name: rust-ci env: RUST_MSRV: "1.63" jobs: codespell: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - run: pip install codespell==v2.3.0 - run: codespell -L crate check: name: cargo check (stable) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@cargo-hack - name: cargo check run: >- cargo hack --workspace --each-feature --keep-going \ check --all-targets check-msrv: name: cargo check (msrv) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.RUST_MSRV }} - uses: taiki-e/install-action@cargo-hack - name: cargo check run: >- cargo hack --each-feature --keep-going \ check --all-targets check-cross: strategy: fail-fast: false matrix: target: - x86_64-unknown-linux-gnu - x86_64-unknown-linux-musl - aarch64-unknown-linux-musl - arm-unknown-linux-gnueabi - arm-unknown-linux-gnueabihf - armv7-unknown-linux-gnueabihf - i686-unknown-linux-gnu - loongarch64-unknown-linux-gnu - loongarch64-unknown-linux-musl - powerpc-unknown-linux-gnu - powerpc64-unknown-linux-gnu - powerpc64le-unknown-linux-gnu - riscv64gc-unknown-linux-gnu - sparc64-unknown-linux-gnu - s390x-unknown-linux-gnu name: cargo check (${{ matrix.target }}) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: # TODO: Should we use MSRV for this? targets: ${{ matrix.target }} - uses: taiki-e/install-action@cargo-hack - name: cargo check --target=${{ matrix.target }} run: >- cargo hack --each-feature --keep-going \ check --target=${{ matrix.target }} --all-targets - name: cargo build --target=${{ matrix.target }} run: >- cargo hack --each-feature --keep-going \ build --target=${{ matrix.target }} --release fmt: name: rustfmt runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # We need to use nightly Rust to check the formatting. - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt - run: cargo fmt --all -- --check clippy: name: clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # Pin the Rust version to avoid Rust updates breaking our clippy lints. - uses: dtolnay/rust-toolchain@1.88 with: components: clippy - uses: taiki-e/install-action@cargo-hack - name: cargo clippy run: >- cargo hack --workspace --each-feature --keep-going \ clippy --all-targets check-lint-nohack: name: make lint (no cargo-hack) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: components: rustfmt,clippy - name: install cbindgen run: cargo install --force cbindgen - name: make lint run: make CARGO_NIGHTLY=cargo lint validate-cbindgen: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - name: install cbindgen run: cargo install --force cbindgen - run: make validate-cbindgen rustdoc: name: cargo doc runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - run: cargo doc --document-private-items --workspace --all-features - name: upload docs uses: actions/upload-artifact@v5 with: name: rustdoc path: target/doc nextest-archive: strategy: fail-fast: false matrix: run-as: - unpriv - root name: cargo nextest archive (${{ matrix.run-as }}) runs-on: ubuntu-latest env: FEATURES: capi ${{ matrix.run-as == 'root' && '_test_as_root' || '' }} steps: - uses: actions/checkout@v5 - uses: actions/checkout@v5 # 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: cargo nextest archive run: >- cargo llvm-cov \ nextest-archive \ --workspace \ -F "${{ env.FEATURES }}" \ --archive-file nextest-pathrs-${{ matrix.run-as }}.tar.zst - name: upload nextest archive uses: actions/upload-artifact@v5 with: name: nextest-archive-${{ matrix.run-as }} path: nextest-pathrs-${{ matrix.run-as }}.tar.zst retention-days: 7 # no need to waste disk space doctest: name: cargo test --doc runs-on: ubuntu-latest env: CARGO_NIGHTLY: cargo steps: - uses: actions/checkout@v5 # 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 - run: make test-rust-doctest - name: upload rust coverage uses: actions/upload-artifact@v5 with: name: profraw-${{ github.job }}-${{ strategy.job-index }} path: "target/llvm-cov-target/*.profraw" retention-days: 7 # no need to waste disk space compute-test-partitions: name: compute test partitions runs-on: ubuntu-latest outputs: tests: ${{ steps.test-partitions.outputs.data }} steps: - uses: actions/checkout@v5 - name: compute test partitions id: test-partitions run: |- # Compute the default test set then convert each object to a string # so that we can double-fromJSON it (once as a list for # test.strategy, and once again for test.name and the actual test). partitions="$(./hack/ci-compute-test-partition.jq <<<"null")" jq -CS <<<"$partitions" # for debugging echo "data=$(jq -ScM 'map("\(.)")' <<<"$partitions")" >>"$GITHUB_OUTPUT" test: needs: - compute-test-partitions - nextest-archive strategy: fail-fast: false matrix: tests: ${{ fromJSON(needs.compute-test-partitions.outputs.tests) }} run-as: - unpriv - root enosys: - "" - openat2 - statx exclude: # The statx tests are quite slow with statx disabled, and there is no # real benefit to including them since the fallback code is tested # elsewhere and our race tests don't try even to attack fdinfo. - enosys: statx tests: >- {"name":"race","pattern":"test(#tests::test_race*)"} env: NEXTEST_PATTERN_SPEC: ${{ fromJSON(matrix.tests).pattern }} name: >- cargo test ${{ format('({0}, {1}{2})', fromJSON(matrix.tests).name, matrix.run-as, matrix.enosys && format(', {0}=enosys', matrix.enosys) || '', ) }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # 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 - name: pull nextest archive uses: actions/download-artifact@v5 with: name: nextest-archive-${{ matrix.run-as }} path: . - name: rust unit tests (${{ matrix.run-as }}) run: >- ./hack/rust-tests.sh \ --cargo=cargo \ ${{ matrix.run-as == 'root' && '--sudo' || '' }} \ --enosys="${{ matrix.enosys }}" \ --archive-file="nextest-pathrs-${{ matrix.run-as }}.tar.zst" \ "${{ env.NEXTEST_PATTERN_SPEC }}" - name: upload rust coverage uses: actions/upload-artifact@v5 with: name: profraw-${{ github.job }}-${{ strategy.job-index }} path: "target/llvm-cov-target/*.profraw" retention-days: 7 # no need to waste disk space coverage: needs: - doctest - test name: compute coverage runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 # 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 - name: install llvm-tools wrappers uses: taiki-e/install-action@v2 with: tool: cargo-binutils - name: pull rust coverage id: rust-coverage uses: actions/download-artifact@v5 with: pattern: "profraw-*" path: profraw - name: merge coverage run: |- mkdir -p target/llvm-cov-target profraw_list="$(mktemp --tmpdir libpathrs-profraw.XXXXXXXX)" find "${{ steps.rust-coverage.outputs.download-path }}" -name '*.profraw' -type f >"$profraw_list" rust-profdata merge --sparse -f "$profraw_list" -o ./target/llvm-cov-target/libpathrs-combined.profraw - name: upload merged rust coverage uses: actions/upload-artifact@v5 with: name: libpathrs-combined-profraw path: target/llvm-cov-target/libpathrs-combined.profraw retention-days: 7 # no need to waste disk space # FIXME: We just pull one version of the archive and use it for # generating coverage profiles, but this really is not correct because # the "root" and "unpriv" binaries are different and so the coverage data # is a little off. See . - name: pull nextest archive uses: actions/download-artifact@v5 with: name: nextest-archive-root path: . # FIXME: llvm-cov appears to have some kind of bug with # --nextest-archive-file as they do not strip the "target" prefix from # the nextest archive. As a workaround, we just extract it ourselves. - name: extract nextest archive run: >- tar xv -f nextest-pathrs-root.tar.zst -C target/llvm-cov-target/ --strip-components=1 - name: calculate coverage run: cargo llvm-cov report - name: generate coverage html run: cargo llvm-cov report --html - name: upload coverage html uses: actions/upload-artifact@v5 with: name: coverage-report path: target/llvm-cov/html examples: name: smoke-test examples runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - run: cargo build --examples - run: make -C examples smoke-test-rust size: permissions: contents: read statuses: write strategy: fail-fast: false matrix: libtype: [ "cdylib", "staticlib" ] name: check ${{ matrix.libtype }} size runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - run: make release - name: compute ${{ matrix.libtype }} file name run: |- case "${{ matrix.libtype }}" in cdylib) libfile=libpathrs.so ;; staticlib) libfile=libpathrs.a ;; *) exit 1 ;; esac echo "LIB_FILENAME=$libfile" >>"$GITHUB_ENV" - name: strip ${{ matrix.libtype }} run: |- cp target/release/$LIB_FILENAME{,.nostrip} strip target/release/$LIB_FILENAME - name: compute ${{ matrix.libtype }} binary size run: |- LIB_SIZE="$(stat -c "%s" "target/release/$LIB_FILENAME" | numfmt --to=si --suffix=B)" LIB_NOSTRIP_SIZE="$(stat -c "%s" "target/release/$LIB_FILENAME.nostrip" | numfmt --to=si --suffix=B)" cat >&2 <<-EOF === binary sizes === $LIB_FILENAME Size: $LIB_SIZE Unstripped: $LIB_NOSTRIP_SIZE EOF echo "LIB_SIZE=$LIB_SIZE" >>"$GITHUB_ENV" echo "LIB_NOSTRIP_SIZE=$LIB_NOSTRIP_SIZE" >>"$GITHUB_ENV" # At the moment, we can only attach the commit status for push operations # because pull requests don't get the right permissions in the default # GITHUB_TOKEN. It's not really clear to me how we should work around # this (secrets like access tokens are not provided for PRs from forked # repos) -- we probably need to switch to status checks? - if: github.event_name == 'push' name: update commit status uses: octokit/request-action@v2.x with: route: POST /repos/{owner_repo}/statuses/{sha} owner_repo: ${{ github.repository }} sha: ${{ github.sha }} state: success description: ${{ env.LIB_FILENAME }} (${{ matrix.libtype }}) is ${{ env.LIB_SIZE }} (${{ env.LIB_NOSTRIP_SIZE }} unstripped) context: rust-ci / ${{ matrix.libtype }} size env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} rust-complete: needs: - codespell - check - check-msrv - check-cross - fmt - clippy - check-lint-nohack - validate-cbindgen - rustdoc - doctest - test - coverage - examples - size runs-on: ubuntu-latest steps: - run: echo "Rust CI jobs completed successfully." release-crate: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') needs: - rust-complete runs-on: ubuntu-latest environment: name: release-crate url: "https://crates.io/crates/pathrs" permissions: id-token: write steps: - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable - run: cargo publish env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} pathrs-0.2.1/.gitignore000064400000000000000000000002131046102023000131110ustar 00000000000000# Rust. /target **/*.rs.bk # pkg-config generated by install.sh. /pathrs.pc # nextest archives generated by CI. /nextest-pathrs*.tar.zst pathrs-0.2.1/.rustfmt.toml000064400000000000000000000060731046102023000136120ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # == 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 . max_width = 100 hard_tabs = false tab_spaces = 4 newline_style = "Auto" use_small_heuristics = "Default" fn_call_width = 60 attr_fn_like_width = 70 struct_lit_width = 18 struct_variant_width = 35 array_width = 60 chain_width = 60 single_line_if_else_max_width = 50 single_line_let_else_max_width = 50 indent_style = "Block" wrap_comments = true format_code_in_doc_comments = false doc_comment_code_block_width = 80 comment_width = 80 normalize_comments = false normalize_doc_attributes = false format_strings = false format_macro_matchers = false format_macro_bodies = true empty_item_single_line = true struct_lit_single_line = true fn_single_line = false where_single_line = false imports_indent = "Block" imports_layout = "Mixed" imports_granularity = "Crate" reorder_imports = true reorder_modules = true reorder_impl_items = false type_punctuation_density = "Wide" space_before_colon = false space_after_colon = true spaces_around_ranges = false binop_separator = "Front" remove_nested_parens = true combine_control_expr = true overflow_delimited_expr = false struct_field_align_threshold = 0 enum_discrim_align_threshold = 0 match_arm_blocks = true force_multiline_blocks = false fn_params_layout = "Tall" brace_style = "SameLineWhere" control_brace_style = "AlwaysSameLine" trailing_semicolon = true trailing_comma = "Vertical" match_block_trailing_comma = false blank_lines_upper_bound = 1 blank_lines_lower_bound = 0 edition = "2021" version = "One" inline_attribute_width = 0 merge_derives = true use_try_shorthand = false use_field_init_shorthand = false force_explicit_abi = true condense_wildcard_suffixes = false color = "Auto" unstable_features = false disable_all_formatting = false skip_children = false show_parse_errors = true error_on_line_overflow = false error_on_unformatted = false ignore = [] emit_mode = "Files" make_backup = false pathrs-0.2.1/CHANGELOG.md000064400000000000000000000770261046102023000127520ustar 00000000000000# Changelog # All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] ## ## [0.2.1] - 2025-11-03 ## > やられたらやり返す。倍返しだ! ### Security ### - When using `ProcfsHandle::open_follow` on **non**-magic-links, libpathrs could fall victim to an overmount attack because we had incorrectly assumed that opening a symlink as a final component would be "atomic" (this is only true for magic-links, which was the primary usecase we had in mind for this API). We now try to use the safe procfs resolver even on symlinks to handle the "regular symlink case". Note that (due to a separate bug in `ProcfsHandle::open_follow` that has also been fixed), privileged users would likely still get an error in this case. ### Added ### - `Error` and `ErrorKind` now have a `can_retry` helper that can be used to make retry loops easier for callers. - libpathrs now has fairly comprehensive end-to-end tests for all our bindings (written in a language-agnostic way), to ensure correctness and uniformty when you use libpathrs, no matter which language you use. ### Fixed ### - python bindings: fix `pathrs.procfs` examples in README. - go bindings: fix the internal `os.FileMode` to `S_IF*` conversion to not auto-include `S_IFREG` for non-`Mknod` operations (previously this would cause `MkdirAll` to error out). - `Root::create_file` now supports `O_TMPFILE`. - Previously, trying to use `ProcfsHandle::open_follow` with a path whose penultimate component was a symlink (i.e., `ProcfsHandle::open_follow(ProcfsBase::ProcRoot, "self/status")`) would result in an error due to a mistake in how we handled looking up parent directories. This has been fixed, and this will now work the way you expect (though you should still use `ProcfsBase::ProcSelf` instead in the above example). - `ProcfsHandle::open_follow` was missing the logic to temporarily allocate a non-`subset=pid` if the target does not exit. This ended up accidentally mitigating the `ProcfsHandle::open_follow` security issue mentioned above (for `fsopen(2)` users trying to open symlinks in `ProcfsBase::ProcRoot` -- note that only `ProcfsBase::ProcRoot` contains such symlinks in the first place). - Quite a few `Root` operations that required resolving the parent directory of the user-provided path could crash if passed `/` or return an unhelpful error when passed `.`. We now return a proper error in these cases. ### Changed ### - The `openat2` resolver will now return `-EAGAIN` if the number of `openat2` retries is exceeded -- this allows higher-level users easily detect if an error is an indication they should retry (based on their own retry policy). In addition, the number of retries done has been bumped from `32` to `128` based on some benchmarking which showed that `32` could fail up to 3% of the time but `128` would only fail ~0.1% of the time in the worst case scenario of an attacker that can saturate all cores with `rename(2)` operations. Users that need stronger resiliency guarantees can do their own additional retry loop on top of `libpathrs` by checking the return value for `EAGAIN`. Please note that we would strongly recommend having some restriction to avoid denial-of-service attacks (such as a deadline -- for reference, our testing showed that even with >50k trials containing >200k operations a deadline of 1ms was never exceeded even in the most pessimistic attack scenario). - The `O_PATH` resolver for `ProcfsHandle` will now return `ELOOP` for magic-links that look like `foo:[bar]` in order to better match `openat2(2)` (examples include `anon_inode`, `nsfs`, `pipe`, and other such special inodes). Previously we would just return `ENOENT`. ## [0.2.0] - 2025-10-17 ## > You're gonna need a bigger boat. > [!NOTE] > As of this release, the libpathrs repository has been moved to > https://github.com/cyphar/libpathrs. Please update any references you have > (though GitHub will redirect the old repository name to the new one). > > In addition, the `go-pathrs` package has been moved to a vanity URL of > `cyphar.com/go-pathrs`. Please update your Go import paths accordingly. > [!IMPORTANT] > The license of this project has changed. Now, the entire project (including > all language bindings and examples) is licensed under the terms of the > Mozilla Public License version 2.0. Additionally, the Rust crate (and > `cdylib`) may also be used (at your option) under the terms of the GNU Lesser > General Public License version 3 (or later). > > For more information, see [`COPYING.md`](./COPYING.md) and the "License" > section of the [README](./README.md). > > The long-term plan is to restructure the project so that `src/capi` is a > separate crate that is only licensed as GNU LGPLv3+ and the Rust crate is > only licensed under as MPLv2, but for technical reasons this is difficult to > achieve at the moment. The primary purpose for dual-licensing is to try to > assuage possible concerns around the GNU LGPLv3 requirements to be able to > "recombine or relink the Application with a modified version of the Linked > Version to produce a modified Combined Work" in the context of the Rust build > system, while also allowing us to license the `cdylib` portion under the GNU > LGPLv3+. ### Breaking ### - python bindings: `Root.creat` has had its `filemode` and `flags` arguments swapped to match the argument order of `openat2` (and `Root.creat_raw`). This also now makes `filemode` have a default value of `0o644` if unspecified. - Most of the C FFI functions have been renamed: - Operations on a `Root` have been renamed to have a `pathrs_inroot_` prefix. - `pathrs_root_open` has been renamed to `pathrs_open_root`, to avoid confusion with `pathrs_inroot_*` functions and clarify what it is opening. - However, `libpathrs.so` now uses symbol versioning and so (mostly as a proof-of-concept) programs compiled against libpathrs 0.1 will continue to function with libpathrs 0.2. - python bindings: `Root.open` has been changed to be a wrapper of `pathrs_inroot_open` instead of being a wrapper around the `Root` constructor. - All C FFI functions that return a file descriptor now set `O_CLOEXEC` by default. Previously some functions that took `O_*` flags would only set `O_CLOEXEC` if the user explicitly requested it, but `O_CLOEXEC` is easy to unset on file descriptors and having it enabled is a more sane default. - The C API values of the `pathrs_proc_base_t` enum (`PATHRS_PROC_BASE_*`) have different values, in order to support `ProcfsBase::ProcPid` passing from C callers. Any binaries compiled with the old headers will need to be recompiled to avoid spurious behaviour. - This required a breaking change in the Go bindings for libpathrs. `ProcfsBase` is now an opaque `struct` type rather than a simple `int` wrapper -- this was necessary in order to add support for `ProcfsBase::ProcPid` in the form of the `ProcBasePid` helper function. - go bindings: the Go module name for the libpathrs Go bindings has been renamed to `cyphar.com/go-pathrs`. This will cause build errors for existing users which used the old repository path, but can easily be fixed by updating `go.mod` and `go.sum` to use the new name. - go bindings: the procfs APIs have been moved to a `procfs` subpackage, and several of the exported types and functions have changed names. We have not provided any compatibility aliases. - python bindings: the procfs APIs have been moved to a `procfs` submodule. We have not provided any compatibility aliases. ### Added ### - python bindings: add `Root.creat_raw` to create a new file and wrap it in a raw `WrappedFd` (os opposed to `Root.creat` which returns an `os.fdopen`). - Root: it is now possible to open a file in one shot without having to do an intermediate `resolve` step with `Root::open_subpath`. This can be more efficient in some scenarios (especially with the openat2-based resolver or for C FFI users where function calls are expensive) as it saves one file descriptor allocation and extra function calls. - Error: `ErrorKind` is now exported, allowing you to programmatically handle errors returned by libpathrs. This interface may change in the future (in particular, `ErrorKind::OsError` might change its representation of `errno` values). - capi: errors that are returned by libpathrs itself (such as invalid arguments being passed by users) will now contain a `saved_errno` value that makes sense for the error type (so `ErrorKind::InvalidArgument` will result in an `EINVAL` value for `saved_errno`). This will allow C users to have a nicer time handling errors programmatically. - tests: we now have a large array of tests for verifying that the core lookup logic of libpathrs is race-safe against various attacks. This is no big surprise, given libpathrs's design, but we now have more extensive tests than `github.com/cyphar/filepath-securejoin`. - procfs: added `ProcfsBase::ProcPid(n)` which is just shorthand when operating on a operating on a different process. This is also now supported by the C API (by just passing the `pid_t` instead of a special `pathrs_proc_base_t` value). - procfs: we now make use of `/proc/thread-self/fdinfo`'s `mnt_id` field to try to thwart bind-mount attacks on systems without `STATX_MNT_ID` support. On systems with `openat2(2)`, this protection is effectively just as safe as `STATX_MNT_ID` (which lets us lower the minimum recommended kernel version from Linux 5.8 to Linux 5.6). For older systems, this protection is not perfect, but is designed to be difficult for an attacker to bypass as consistently and easily as it would be without these protections. Note that it is still the case that post-6.8 kernels (`STATX_MNT_ID_UNIQUE`) are still the most strongly recommended kernels to use. - procfs: `ProcfsHandle` is now `ProcfsHandleRef<'static>`, and it is now possible to construct borrowed versions of `ProcfsHandleRef<'fd>` and still use them. This is primarily intended for our C API, but Rust users can make use of it if you wish. It is possible we will move away from a type alias for `ProcfsHandle` in the future. - capi: All of that `pathrs_proc_*` methods now have a `pathrs_proc_*at` variant which allows users to pass a file descriptor to use as the `/proc` handle (effectively acting as a C version of `ProcfsHandleRef<'fd>`). Only users that operate heavily on global procfs files are expected to make use of this API -- the regular API still lets you operate on global procfs files. Users can pass `PATHRS_PROC_DEFAULT_ROOTFD` (`-EBADF`) as a file descriptor to use the cached API (the old API methods just do this internally). - procfs: a new `ProcfsHandleBuilder` builder has been added to the API, which allows users to construct an unmasked (i.e., no-`subset=pid`) `ProcfsHandle`. This should only be used sparingly and with great care to avoid leaks, but it allows some programs to amortise the cost of constructing a `procfs` handle when doing a series of operations on global procfs files (such as configuring a large number of sysctls). We plan to add a few more configuration options to `ProcfsHandleBuilder` in the future, but `ProcfsHandleBuilder::unmasked` will always give you an unmasked version of `/proc` regardless of any new features. - procfs: `ProcfsHandleRef` can now be converted to `OwnedFd` with `.into_owned_fd()` (if it is internally an `OwnedFd`) and borrowed as `BorrowedFd` with `AsFd::as_fd`. Users should take great care when using the underlying file descriptor directly, as using it opens you up to all of the attacks that libpathrs protects you against. - capi: add `pathrs_procfs_open` method to create a new `ProcfsHandle` with a custom configuration (a-la `ProcfsHandleBuilder`). As with `ProcfsHandleBuilder`, most users do not need to use this. - python bindings: `ProcfsHandle` wraps this new API, and you can construct custom `ProcfsHandle`s with `ProcfsHandle.new(...)`. `ProcfsHandle.cached()` returns the cached global `ProcfsHandle`. The top-level `proc_*` functions (which may be removed in future versions) are now just bound methods of `ProcfsHandle.cached()` and have been renamed to remove the `proc_` prefix (now that the procfs API lives in a separate `pathrs.procfs` module). - go bindings: `ProcfsHandle` wraps this new API, and you can construct a custom `ProcfsHandle`s with `OpenProcRoot` (calling this with no arguments will produce the global cached handle if the handle is being cached). The old `Proc*` functions have been removed entirely. - capi: We now use symbol versioning for `libpathrs.so`, which should avoid concerns about future API breakages. I have tested all of the key aspects of this new symbol versioning setup and it seems Rust provides everything necessary (when testing this last year, I was unable to get backward-compatibity working). ### Changed ### - procfs: the caching strategy for the internal procfs handle has been adjusted, and the public `GLOBAL_PROCFS_HANDLE` has been removed. The initial plan was to remove caching entirely, as there is a risk of leaked long-lived file descriptors leading to attacks like [CVE-2024-21626][]. However, we found that the performance impact could be quite noticeable (`fsconfig(2)` in particular is somewhat heavy in practice if you do it for every VFS operation very frequently). (#203) The current approach is for `ProcfsHandle::new` to opportunistically cache the underlying file descriptor if it is considered relatively safe to cache (i.e., it is `subset=pid` and is a detached mount object, which should stop host breakouts even if a privileged container attacker snoops on the file descriptor). Programs need not be aware of the caching behaviour, though programs which need to change security contexts should still use common-sense protections like `PR_SET_DUMPABLE`. (#249) - api: many of the generic type parameters have been replaced with `impl Trait` arguments, in order to make using libpathrs a bit more ergonomic. Unless you were specifically setting the generic types with `::<>` syntax, this change should not affect you. - syscalls: switch to rustix for most of our syscall wrappers to simplify how much code we have for wrapper raw syscalls. This also lets us build on musl-based targets because musl doesn't support some of the syscalls we need. There are some outstanding issues with rustix that make this switch a little uglier than necessary ([rustix#1186][], [rustix#1187][]), but this is a net improvement overall. ### Fixes ### - multiarch: we now build correctly on 32-bit architectures as well as architectures that have unsigned char. We also have CI jobs that verify that builds work on a fairly large number of architectures (all relevant tier-1 and tier-2-with-host-tools architectures). If there is an architecture you would like us to add to the build matrix and it is well-supported by `rustc`, feel free to open an issue or PR! - `Handle::reopen` will now return an error if you attempt to reopen a handle to a symlink (such as one created with `Root::resolve_nofollow`). Previously, you would get various errors and unexpected behaviour. If you wish to make an `O_PATH|O_NOFOLLOW` copy of a symlink handle, you can simply use `try_clone` (i.e. `dup(2)` the file descriptor). - `Handle::reopen(O_NOFOLLOW)` will now return reasonable results. Previously, it would return `-ELOOP` in most cases and in other cases it would return unexpected results because the `O_NOFOLLOW` would have an effect on the magic-link used internally by `Handle::reopen`. - `Root::mkdir_all` will no longer return `-EEXIST` if another process tried to do `Root::mkdir_all` at the same time, instead the race winner's directory will be used by both processes. See [opencontainers/runc#4543][] for more details. - `Root::remove_all` will now handle missing paths that disappear from underneath it more gracefully, ensuring that multiple `Root::remove_all` operations run on the same directory tree will all succeed without errors. The need for this is similar to the need for `Root::mkdir_all` to handle such cases. - opath resolver: in some cases with trailing symlinks in the symlink stack (i.e. for partial lookups caused by `Root::mkdir_all`) we would not correctly handle leading `..` components, leading to safety errors when libpathrs thought that the symlink stack had been corrupted. - openat2 resolver: always return a hard `SafetyViolation` if we encounter one during partial lookups to match the opath resolver behaviour and to avoid confusion by users (it is theoretically safe to fall back from a `SafetyViolation` during a partial lookup, but it's better to be safe here). - The error handling for `Root::*` operations that require splitting paths into a parent directory and single basename component (such as `Root::create`) has now been unified and cases like trailing `/.` and `/..` will now always result in `ErrorKind::InvalidArgument`. - Trailing slash behaviour (i.e. where a user specifies a trailing slash in a path passed to libpathrs) throughout libpathrs has been improved to better match the kernel APIs (where possible) or otherwise has been made consistent and intentional: - `Root::create` will always error out with an `InvalidArgument` for the target path unless the inode being created is an `InodeType::Directory`, in which case the trailing slash will be ignored (to match the behaviour of `mkdir(2)` on Linux). Hard links with a trailing slash will also produce an error, as hard-links to directories are also forbidden on Unix. - `Root::create_file` will always error out with an `InvalidArgument`. - `Root::remove_all` and `Root::remove_dir` will ignore trailing slashes, while `Root::remove_file` will always fail with `ENOTDIR`. The reason for `Root::remove_all` always succeeding is that it matches the behaviour of Go's `os.RemoveAll` and `rm -rf`, as well as being impractical for us to determine if the target to be deleted is a directory in a race-free way. - `Root::rename` matches `renameat2(2)`'s behaviour to the best of our ability. * Trailing slashes on the source path are only allowed if the source is actually a directory (otherwise you get `ENOTDIR`). * For `RENAME_EXCHANGE`, the target path may only have trailing slashes if it is actually a directory (same as the source path). Otherwise, if the *target* path has a trailing slash then the *source* path must be a directory (otherwise you get `ENOTDIR`). - opath resolver: we now return `ELOOP` when we run into a symlink that came from mount with the `MS_NOSYMFOLLOW` set, to match the behaviour of `openat2`. - openat2: we now set `O_NOCTTY` and `O_NOFOLLOW` more aggressively when doing `openat2` operations, to avoid theoretical DoS attacks (these were set for `openat` but we missed including them for `openat2`). [CVE-2024-21626]: https://github.com/opencontainers/runc/security/advisories/GHSA-xr7r-f8xq-vfvv [rustix#1186]: https://github.com/bytecodealliance/rustix/issues/1186 [rustix#1187]: https://github.com/bytecodealliance/rustix/issues/1187 [opencontainers/runc#4543]: https://github.com/opencontainers/runc/issues/4543 ## [0.1.3] - 2024-10-10 ## > 自動化って物は試しとすればいい物だ ### Changed ### - gha: our Rust crate and Python bindings are now uploaded automatically from a GitHub action when a tag is pushed. ### Fixes ### - syscalls: the pretty-printing of `openat2` errors now gives a string description of the flags passed rather that just a hex value (to match other syscalls). - python bindings: restrict how our methods and functions can be called using `/` and `*` to reduce the possibility of future breakages if we rename or re-order some of our arguments. ## [0.1.2] - 2024-10-09 ## > 蛇のように賢く、鳩のように素直でありなさい ### Fixes ### - python bindings: add a minimal README for PyPI. - python bindings: actually export `PROC_ROOT`. - python bindings: add type annotations and `py.typed` to allow for downstream users to get proper type annotations for the API. ## [0.1.1] - 2024-10-01 ## > 頒布と聞いたら蛇に睨まれた蛙になるよ ### Added ### - procfs: add support for operating on files in the `/proc` root (or other processes) with `ProcfsBase::ProcRoot`. While the cached file descriptor shouldn't leak into containers (container runtimes know to set `PR_SET_DUMPABLE`, and our cached file descriptor is `O_CLOEXEC`), I felt a little uncomfortable about having a global unmasked procfs handle sitting around in `libpathrs`. So, in order to avoid making a file descriptor leak by a `libpathrs` user catastrophic, `libpathrs` will always try to use a "limited" procfs handle as the global cached handle (which is much safer to leak into a container) and for operations on `ProcfsBase::ProcRoot`, a temporary new "unrestricted" procfs handle is created just for that operartion. This is more expensive, but it avoids a potential leak turning into a breakout or other nightmare scenario. - python bindings: The `cffi` build script is now a little easier to use for distributions that want to build the python bindings at the same time as the main library. After compiling the library, set the `PATHRS_SRC_ROOT` environment variable to the root of the `libpathrs` source directory. This will instruct the `cffi` build script (when called from `setup.py` or `python3 -m build`) to link against the library built in the source directory rather than using system libraries. As long as you install the same library later, this should cause no issues. Standard wheel builds still work the same way, so users that want to link against the system libraries don't need to make any changes. ### Fixed ### - `Root::mkdir_all` no longer does strict verification that directories created by `mkdir_all` "look right" after opening each component. These checks didn't protect against any practical attack (since an attacker could just get us to use a directory by creating it before `Root::mkdir_all` and we would happily use it) and just resulted in spurious errors when dealing with complicated filesystem configurations (POSIX ACLs, weird filesystem-specific mount options). (#71) - capi: Passing invalid `pathrs_proc_base_t` values to `pathrs_proc_*` will now return an error rather than resulting in Undefined Behaviour™. ## [0.1.0] - 2024-09-14 ## > 負けたくないことに理由って要る? ### Added ### - libpathrs now has an official MSRV of 1.63, which is verified by our CI. The MSRV was chosen because it's the Rust version in Debian stable and it has `io_safety` which is one of the last bits we absolutely need. - libpathrs now has a "safe procfs resolver" implementation that verifies all of our operations on `/proc` are done safely (including using `fsopen(2)` or `open_tree(2)` to create a private `/proc` to protect against race attacks). This is mainly motivated by issues like [CVE-2019-16884][] and [CVE-2019-19921][], where an attacker could configure a malicious mount table such that naively doing `/proc` operations could result in security issues. While there are limited things you can do in such a scenario, it is far more preferable to be able to detect these kinds of attacks and at least error out if there is a malicious `/proc`. This is based on similar work I did in [filepath-securejoin][]. - This API is also exposed to users through the Rust and C FFI because this is something a fair number of system tools (such as container runtimes) need. - root: new `Root` methods: - `readlink` and `resolve_nofollow` to allow users to operate on symlinks directly (though it is still unsafe to use the returned path for lookups!). - `remove_all` so that Go users can switch from `os.RemoveAll` (though [Go's `os.RemoveAll` is safe against races since Go 1.21.11 and Go 1.22.4][go-52745]). - `mkdir_all` so that Go users can switch from `os.MkdirAll`. This is based on similar work done in [filepath-securejoin][]. - root: The method for configuring the resolver has changed to be more akin to a getter-setter style. This allows for more ergonomic usage (see the `RootRef::with_resolver_flags` examples) and also lets us avoid exposing internal types needlessly. As part of this change, the ability to choose the resolver backend was removed (because the C API also no longer supports it). This will probably be re-added in the future, but for now it seems best to not add extra APIs that aren't necessary until someone asks. - opath resolver: We now emulate `fs.protected_symlinks` when resolving symlinks using the emulated opath resolver. This is only done if `fs.protected_symlinks` is enabled on the system (to mirror the behaviour of `openat2`). - tests: Add a large number of integration tests, mainly based on the test suite in [filepath-securejoin][]. This test suite tests all of the Rust code and the C FFI code from within Rust, giving us ~89% test coverage. - tests: Add some smoke tests using our bindings to ensure that you can actually build with them and run a basic `cat` program. In the future we will do proper e2e testing with all of the bindings. - packaging: Add an autoconf-like `install.sh` script that generates a `pkg-config` specification for libpathrs. This should help distributions package libpathrs. [CVE-2019-16884]: https://nvd.nist.gov/vuln/detail/CVE-2019-16884 [CVE-2019-19921]: https://nvd.nist.gov/vuln/detail/CVE-2019-19921 [filepath-securejoin]: https://github.com/cyphar/filepath-securejoin [go-52745]: https://github.com/golang/go/issues/52745 ### Fixed ### - Handling of `//` and trailing slashes has been fixed to better match what users expect and what the kernel does. - opath resolver: Use reference counting to avoid needlessly cloning files internally when doing lookups. - Remove the `try_clone_hotfix` workaround, since the Rust stdlib patch was merged several years ago. - cffi: Building the C API is now optional, so Rust crates won't contain any of the C FFI code and we only build the C FFI crate types manually in the makefile. This also lets us remove some dependencies and other annoying things in the Rust crate (since those things are only needed for the C API). - python bindings: Switch to setuptools to allow for a proper Python package install. This also includes some reworking of the layout to avoid leaking stuff to users that just do `import pathrs`. ### Changed ### - cffi: Redesign the entire API to be file descriptor based, removing the need for complicated freeing logic and matching what most kernel APIs actually look like. While there is a risk that users would operate on file descriptors themselves, the benefits of a pure-fd-based API outweigh those issues (and languages with bindings like Python and Go can easily wrap the file descriptor to provide helper methods and avoid this mistake by users). Aside from making C users happier, this makes writing bindings much simpler because every language has native support for handling the freeing of file objects (Go in particular has `*os.File` which would be too difficult to emulate outside of the stdlib because of it's unique `Close` handling). - Unfortunately, this API change also removed some information from the C API because it was too difficult to deal with: - Backtraces are no longer provided to the C API. There is no plan to re-add them because they complicate the C API a fair bit and it turns out that it's basically impossible to graft backtraces to languages that have native backtrace support (Go and Python) so providing this information has no real benefit to anyone. - The configuration API has been removed for now. In the future we will probably want to re-add it, but figuring out a nice API for this is left for a future (pre-1.0) release. In practice, the default settings are the best settings to use for most people anyway. - bindings: All of the bindings were rewritten to use the new API. - rust: Rework libpathrs to use the (stabilised in Rust 1.63) `io_safety` features. This lets us avoid possible "use after free" issues with file descriptors that were closed by accident. This required the addition of `HandleRef` and `RootRef` to wrap `BorrowedFd` (this is needed for the C API, but is almost certainly useful to other folks). Unfortunately we can't implement `Deref` so all of the methods need to be duplicated for the new types. - Split `Root::remove` into `Root::remove_file` (`unlink`) and `Root::remove_dir` (`rmdir`) so we don't need to do the retry loop anymore. Some users care about what kind of inode they're removing, and if a user really wants to nuke a path they would want to use `Root::remove_all` anyway because the old `Root::remove` would not remove non-empty directories. - Switch from `snafu` to `thiserror` for generating our error impls. One upshot of this change is that our errors are more opaque to Rust users. However, this change resulted in us removing backtraces from our errors (because `thiserror` only supports `std::backtrace::Backtrace` which was stabilised after our MSRV, and even then it is somewhat limited until some more bits of `std::backtrace::Backtrace` are stabilised). We do plan to re-add backtraces but they probably aren't strictly *needed* by most library users. In the worst case we could write our own handling of backtraces using the `backtrace` crate, but I'd like to see a user actually ask for that before sitting down to work on it. ## [0.0.2] - 2020-02-15 ## ### Added ### - bindings: Go bindings (thanks to Maxim Zhiburt for the initial version!). - bindings: Add support for converting to/from file descriptors. ### Fixed ### - Update to the newest `openat2` API (which now uses extensible structs). ### Changed ### - cffi: Make all objects thread-safe so multi-threaded programs don't hit data races. - cffi: Major rework of the CPointer locking design to split the single Mutex (used for both the inner type and errors) into two separate locks. As the inner value is almost always read, this should massively reduce lock contention in multi-threaded programs. - cffi: `pathrs_from_fd` now clones the passed file descriptor. Some GC'd languages are annoying to deal with when a file descriptor's ownership is meant to be transferred outside of the program. ## [0.0.1] - 2020-01-05 ## ### Fixed ### - docs: Fix rustdoc build errors. ## [0.0.0] - 2020-01-05 `[YANKED]` ## Initial release. (This release was yanked because the rust docs were broken.) ### Added ### - Initial implementation of libpathrs, with most of the major functionality we need implemented: - `Root`: - `openat2`- and `O_PATH`-based resolvers. - `resolve` - `create` and `create_file` - `remove` - `rename` - `Handle`: - `reopen` - C FFI. - Python bindings. [Unreleased]: https://github.com/cyphar/libpathrs/compare/v0.2.1...HEAD [0.2.1]: https://github.com/cyphar/libpathrs/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/cyphar/libpathrs/compare/v0.1.3...v0.2.0 [0.1.3]: https://github.com/cyphar/libpathrs/compare/v0.1.2...v0.1.3 [0.1.2]: https://github.com/cyphar/libpathrs/compare/v0.1.1...v0.1.2 [0.1.1]: https://github.com/cyphar/libpathrs/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/cyphar/libpathrs/compare/v0.0.2...v0.1.0 [0.0.2]: https://github.com/cyphar/libpathrs/compare/v0.0.1...v0.0.2 [0.0.1]: https://github.com/cyphar/libpathrs/compare/v0.0.0...v0.0.1 [0.0.0]: https://github.com/cyphar/libpathrs/commits/v0.0.0/ pathrs-0.2.1/COPYING.md000064400000000000000000000634611046102023000125710ustar 00000000000000## COPYING ## This project is made up of code licensed under different licenses. Note that **each file** in this project individually has a code comment at the start describing the license of that particular file -- this is the most accurate license information of this project; in case there is any conflict between this document and the comment at the start of a file, the comment shall take precedence. The only purpose of this document is to work around [a known technical limitation of pkg.go.dev's license checking tool when dealing with non-trivial project licenses][go75067]. [go75067]: https://go.dev/issue/75067 ### `MPL-2.0 OR LGPL-3.0-or-later` #### `SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later` At time of writing, the following files and directories are licensed under the terms of the [Mozilla Public License version 2.0][MPL-2.0] or the [GNU Lesser General Public License version 3][LGPL-3.0], at your option: * `Cargo.toml` * `Makefile` * `install.sh` * `hack/*.sh` * `cbindgen.toml` * `include/pathrs.h` * `build.rs` * `src/**` Unless otherwise stated, by intentionally submitting any Contribution (as defined by the Mozilla Public License version 2.0) for inclusion into the `libpathrs` project, you are agreeing to dual-license your Contribution as above, without any additional terms or conditions. #### `LGPL-3.0-or-later` #### The text of the GNU Lesser General Public License version 3 is the following (the text is also available from the [`LICENSE.LGPL-3.0`][LGPL-3.0] file, and a copy of the GNU General Public License version 3 is available from [`LICENSE.GPL-3.0`][GPL-3.0]): [LGPL-3.0]: LICENSE.LGPL-3.0 [GPL-3.0]: LICENSE.GPL-3.0 ``` GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ``` ### `MPL-2.0` ### `SPDX-License-Identifier: MPL-2.0` All other files (unless otherwise marked) are licensed under the Mozilla Public License (version 2.0). The text of the Mozilla Public License (version 2.0) is the following (the text is also available from the [`LICENSE.MPL-2.0`][MPL-2.0] file): [MPL-2.0]: LICENSE.MPL-2.0 ``` Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. ``` pathrs-0.2.1/Cargo.lock0000644000000327040000000000100103160ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", "winapi", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ "bytemuck", ] [[package]] name = "bytemuck" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" dependencies = [ "bytemuck_derive", ] [[package]] name = "bytemuck_derive" version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags 1.3.2", "clap_lex", "indexmap", "once_cell", "strsim", "termcolor", "textwrap", ] [[package]] name = "clap_lex" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", "wasip2", ] [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] [[package]] name = "indexmap" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", ] [[package]] name = "indoc" version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" dependencies = [ "rustversion", ] [[package]] name = "itertools" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "once_cell" version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "open-enum" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9807f1199cf84ec7cc801a79e5ee9aa5178e4762c6b9c7066c30b3cabdcd911e" dependencies = [ "open-enum-derive", ] [[package]] name = "open-enum-derive" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "894ae443e59fecf7173ab3b963473f44193fa71b3c8953c61a5bd5f30880bb88" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] [[package]] name = "os_str_bytes" version = "6.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "path-clean" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17359afc20d7ab31fdb42bb844c8b3bb1dabd7dcf7e68428492da7f16966fcef" [[package]] name = "pathrs" version = "0.2.1" dependencies = [ "anyhow", "bitflags 2.10.0", "bytemuck", "clap", "errno", "indoc", "itertools", "libc", "memchr", "once_cell", "open-enum", "paste", "path-clean", "pretty_assertions", "rand", "rustix", "rustversion", "static_assertions", "tempfile", "thiserror", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", ] [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", "windows-sys", ] [[package]] name = "termcolor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" dependencies = [ "winapi-util", ] [[package]] name = "textwrap" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", "syn 2.0.106", ] pathrs-0.2.1/Cargo.toml0000644000000050010000000000100103270ustar # 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 are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.63" name = "pathrs" version = "0.2.1" authors = ["Aleksa Sarai "] build = "build.rs" autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "C-friendly API to make path resolution safer on Linux." readme = "README.md" keywords = [ "file", "fs", "security", "linux", ] categories = ["filesystem"] license = "MPL-2.0 OR LGPL-3.0-or-later" repository = "https://github.com/cyphar/libpathrs" [badges.maintenance] status = "experimental" [features] _test_as_root = [] capi = [ "dep:bytemuck", "bitflags/bytemuck", "dep:rand", "dep:open-enum", ] default = [] [lib] name = "pathrs" crate-type = ["rlib"] path = "src/lib.rs" [[example]] name = "rust-cat" path = "examples/rust-cat/main.rs" [dependencies.bitflags] version = "2.2" [dependencies.bytemuck] version = "1" features = [ "extern_crate_std", "derive", ] optional = true [dependencies.itertools] version = "0.14" [dependencies.libc] version = "0.2.175" [dependencies.memchr] version = "2" [dependencies.once_cell] version = "1" [dependencies.open-enum] version = "0.3" optional = true [dependencies.rand] version = "0.9" optional = true [dependencies.rustix] version = "1.1" features = [ "fs", "process", "thread", "mount", ] [dependencies.rustversion] version = "1" [dependencies.static_assertions] version = "1.1" [dependencies.thiserror] version = "2" [dev-dependencies.anyhow] version = "1" [dev-dependencies.clap] version = "3" features = ["cargo"] [dev-dependencies.errno] version = "0.3" [dev-dependencies.indoc] version = "2" [dev-dependencies.paste] version = "1" [dev-dependencies.path-clean] version = "1" [dev-dependencies.pretty_assertions] version = "1.4.1" features = ["unstable"] [dev-dependencies.tempfile] version = "3" [build-dependencies.tempfile] version = "3" [lints.rust.unexpected_cfgs] level = "warn" priority = 0 check-cfg = [ "cfg(coverage)", "cfg(cdylib)", "cfg(staticlib)", ] [profile.release] lto = true pathrs-0.2.1/Cargo.toml.orig000064400000000000000000000064751046102023000140300ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # == 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 . [package] name = "pathrs" version = "0.2.1" license = "MPL-2.0 OR LGPL-3.0-or-later" authors = ["Aleksa Sarai "] description = "C-friendly API to make path resolution safer on Linux." repository = "https://github.com/cyphar/libpathrs" readme = "README.md" keywords = ["file", "fs", "security", "linux"] categories = ["filesystem"] edition = "2021" rust-version = "1.63" [badges] maintenance = { status = "experimental" } [lib] # When building the CAPI, our Makefile adds --crate-type={cdylib,staticlib}. crate-type = ["rlib"] [features] default = [] capi = ["dep:bytemuck", "bitflags/bytemuck", "dep:rand", "dep:open-enum"] # All of these _test_* features are only used for our own tests -- they must # 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 = [] [profile.release] # Enable link-time optimisations. lto = true [dependencies] bitflags = "2.2" bytemuck = { version = "1", features = ["extern_crate_std", "derive"], optional = true } itertools = "0.14" libc = "0.2.175" memchr = "2" # MSRV(1.80): Use LazyLock. # MSRV(1.65): Update to once_cell >= 1.21. once_cell = "1" # MSRV(1.65): Update to >=0.4.1 which uses let_else. 0.4.0 was broken. open-enum = { version = "0.3", optional = true } rand = { version = "0.9", optional = true } rustix = { version = "1.1", features = ["fs", "process", "thread", "mount"] } rustversion = "1" thiserror = "2" static_assertions = "1.1" [dev-dependencies] anyhow = "1" clap = { version = "3", features = ["cargo"] } errno = "0.3" indoc = "2" tempfile = "3" paste = "1" path-clean = "1" pretty_assertions = { version = "1.4.1", features = ["unstable"] } [build-dependencies] tempfile = "3" [lints.rust] unexpected_cfgs = { level = "warn", check-cfg = [ # We have special handling for coverage runs (which set cfg(coverage)). 'cfg(coverage)', # We set these cfgs when building with --features=capi. 'cfg(cdylib)', 'cfg(staticlib)' ] } [workspace] resolver = "2" members = [ "contrib/fake-enosys", "e2e-tests/cmd/rust", ] pathrs-0.2.1/LICENSE.GPL-3.0000064400000000000000000001045151046102023000130570ustar 00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . pathrs-0.2.1/LICENSE.LGPL-3.0000064400000000000000000000167441046102023000132010ustar 00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. pathrs-0.2.1/LICENSE.MPL-2.0000064400000000000000000000405261046102023000130650ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. pathrs-0.2.1/Makefile000064400000000000000000000121761046102023000125740ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # == 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 . CARGO ?= cargo CARGO_NIGHTLY ?= cargo +nightly # Unfortunately --all-features needs to be put after the subcommand, but # cargo-hack needs to be put before the subcommand. So make a function to make # this a little easier. ifneq (, $(shell which cargo-hack)) define cargo_hack $(1) hack --feature-powerset $(2) endef else define cargo_hack $(1) $(2) --all-features endef endif CARGO_CHECK := $(call cargo_hack,$(CARGO),check) CARGO_CLIPPY := $(call cargo_hack,$(CARGO),clippy) CARGO_LLVM_COV := $(call cargo_hack,$(CARGO_NIGHTLY),llvm-cov) RUSTC_FLAGS := --features=capi -- -C panic=abort CARGO_FLAGS ?= SRC_FILES = $(wildcard Cargo.*) $(shell find . -name '*.rs') .DEFAULT: debug .PHONY: debug debug: target/debug target/debug: $(SRC_FILES) # For some reason, --crate-types needs separate invocations. We can't use # #![crate_type] unfortunately, as using it with #![cfg_attr] has been # deprecated. $(CARGO) rustc $(CARGO_FLAGS) --crate-type=cdylib $(RUSTC_FLAGS) $(CARGO) rustc $(CARGO_FLAGS) --crate-type=staticlib $(RUSTC_FLAGS) .PHONY: release release: target/release target/release: $(SRC_FILES) # For some reason, --crate-types needs separate invocations. We can't use # #![crate_type] unfortunately, as using it with #![cfg_attr] has been # deprecated. $(CARGO) rustc $(CARGO_FLAGS) --release --crate-type=cdylib $(RUSTC_FLAGS) $(CARGO) rustc $(CARGO_FLAGS) --release --crate-type=staticlib $(RUSTC_FLAGS) .PHONY: smoke-test smoke-test: make -C examples smoke-test .PHONY: clean clean: -rm -rf target/ .PHONY: lint lint: validate-cbindgen lint-rust .PHONY: lint-rust lint-rust: $(CARGO_NIGHTLY) fmt --all -- --check $(CARGO_CLIPPY) --all-targets $(CARGO_CHECK) $(CARGO_FLAGS) --all-targets .PHONY: validate-cbindgen validate-cbindgen: $(eval TMPDIR := $(shell mktemp --tmpdir -d libpathrs-cbindgen-check.XXXXXXXX)) @ \ trap "rm -rf $(TMPDIR)" EXIT ; \ cbindgen -c cbindgen.toml -o $(TMPDIR)/pathrs.h ; \ if ! ( diff -u include/pathrs.h $(TMPDIR)/pathrs.h ); then \ echo -e \ "\n" \ "ERROR: include/pathrs.h is out of date.\n\n" \ "Changes to the C API in src/capi/ usually need to be paired with\n" \ "an update to include/pathrs.h using cbindgen. To fix this error,\n" \ "just run:\n\n" \ "\tcbindgen -c cbindgen.toml -o include/pathrs.h\n" ; \ exit 1 ; \ fi .PHONY: test-rust-doctest test-rust-doctest: $(CARGO_LLVM_COV) --no-report --branch --doc .PHONY: test-rust-unpriv test-rust-unpriv: ./hack/rust-tests.sh --cargo="$(CARGO_NIGHTLY)" ./hack/rust-tests.sh --cargo="$(CARGO_NIGHTLY)" --enosys=openat2 # In order to avoid re-running the entire test suite with just statx # disabled, we re-run the key procfs tests with statx disabled. ./hack/rust-tests.sh --cargo="$(CARGO_NIGHTLY)" --enosys=statx "test(#tests::*procfs*)" .PHONY: test-rust-root test-rust-root: ./hack/rust-tests.sh --cargo="$(CARGO_NIGHTLY)" --sudo ./hack/rust-tests.sh --cargo="$(CARGO_NIGHTLY)" --sudo --enosys=openat2 # In order to avoid re-running the entire test suite with just statx # disabled, we re-run the key procfs tests with statx disabled. ./hack/rust-tests.sh --cargo="$(CARGO_NIGHTLY)" --sudo --enosys=statx "test(#tests::*procfs*)" .PHONY: test-rust test-rust: -rm -rf target/llvm-cov* make test-rust-{doctest,unpriv,root} .PHONY: test-e2e test-e2e: make -C e2e-tests test-all make -C e2e-tests RUN_AS=root test-all .PHONY: test test: test-rust test-e2e $(CARGO_NIGHTLY) llvm-cov report $(CARGO_NIGHTLY) llvm-cov report --open .PHONY: docs docs: $(CARGO) doc --all-features --document-private-items --open .PHONY: install install: release @echo "If you want to configure the install paths, use ./install.sh directly." @echo "[Sleeping for 3 seconds.]" @sleep 3s ./install.sh pathrs-0.2.1/README.md000064400000000000000000000351171046102023000124130ustar 00000000000000## `libpathrs` ## [![rust-ci build status](https://github.com/cyphar/libpathrs/actions/workflows/rust.yml/badge.svg)](https://github.com/cyphar/libpathrs/actions/workflows/rust.yml) [![bindings-c build status](https://github.com/cyphar/libpathrs/actions/workflows/bindings-c.yml/badge.svg)](https://github.com/cyphar/libpathrs/actions/workflows/bindings-c.yml) [![bindings-go build status](https://github.com/cyphar/libpathrs/actions/workflows/bindings-go.yml/badge.svg)](https://github.com/cyphar/libpathrs/actions/workflows/bindings-go.yml) [![bindings-python build status](https://github.com/cyphar/libpathrs/actions/workflows/bindings-python.yml/badge.svg)](https://github.com/cyphar/libpathrs/actions/workflows/bindings-python.yml) [![docs](https://docs.rs/pathrs/badge.svg)](https://docs.rs/pathrs/) [![dependency status](https://deps.rs/repo/github/cyphar/libpathrs/status.svg)](https://deps.rs/repo/github/cyphar/libpathrs) [![msrv](https://shields.io/crates/msrv/pathrs)](Cargo.toml) This library implements a set of C-friendly APIs (written in Rust) to make path resolution within a potentially-untrusted directory safe on GNU/Linux. There are countless examples of security vulnerabilities caused by bad handling of paths (symlinks make the issue significantly worse). ### Example ### #### Root and Handle API #### Here is a toy example of using this library to open a path (`/etc/passwd`) inside a root filesystem (`/path/to/root`) safely. More detailed examples can be found in `examples/` and `tests/`. ```c #include int get_my_fd(void) { const char *root_path = "/path/to/root"; const char *unsafe_path = "/etc/passwd"; int liberr = 0; int root = -EBADF, handle = -EBADF, fd = -EBADF; root = pathrs_open_root(root_path); if (IS_PATHRS_ERR(root)) { liberr = root; goto err; } handle = pathrs_inroot_resolve(root, unsafe_path); if (IS_PATHRS_ERR(handle)) { liberr = handle; goto err; } fd = pathrs_reopen(handle, O_RDONLY); if (IS_PATHRS_ERR(fd)) { liberr = fd; goto err; } err: if (IS_PATHRS_ERR(liberr)) { pathrs_error_t *error = pathrs_errorinfo(liberr); fprintf(stderr, "Uh-oh: %s (errno=%d)\n", error->description, error->saved_errno); pathrs_errorinfo_free(error); } close(root); close(handle); return fd; } ``` #### Safe `procfs` API #### `libpathrs` also provides a set of primitives to safely interact with `procfs`. This is very important for some programs (such as container runtimes), because `/proc` has several key system administration purposes that make it different to other filesystems. It particular, `/proc` is used: 1. As a mechanism for doing certain filesystem operations through `/proc/self/fd/...` (and other similar magic-links) that cannot be done by other means. 1. As a source of true information about processes and the general system (such as by looking `/proc/$pid/status`). 1. As an administrative tool for managing processes (such as setting LSM labels like `/proc/self/attr/apparmor/exec`). These operations have stronger requirements than regular filesystems. For (1) we need to open the magic-link for real (magic-links are symlinks that are not resolved lexically, they are in-kernel objects that warp you to other files without doing a regular path lookup) which much harder to do safely (even with `openat2`). For (2) and (3) we have the requirement that we need to open a specific file, not just any file within `/proc` (if there are overmounts or symlinks) which is not the case `pathrs_inroot_resolve()`. As a result, it is necessary to take far more care when doing operations of `/proc` and `libpathrs` provides very useful helper to do this. Failure to do so can lead to security issues such as those in [CVE-2019-16884][cve-2019-16884] and [CVE-2019-19921][cve-2019-19921]. In addition, with the [new mount API][lwn-newmount] (`fsopen(2)` and `open_tree(2)` in particular, added in Linux 5.2), it is possible to get a totally private `procfs` handle that can be used without worrying about racing mount operations. `libpathrs` will try to use this if it can (this usually requires root). Here are a few examples of practical things you might want to do with `libpathrs`'s `procfs` API: ```c /* * Safely get an fd to /proc/self/exe. This is something runc does to re-exec * itself during the container setup process. */ int get_self_exe(void) { /* This follows the trailing magic-link! */ int fd = pathrs_proc_open(PATHRS_PROC_SELF, "exe", O_PATH); if (IS_PATHRS_ERR(fd)) { pathrs_error_t *error = pathrs_errorinfo(fd); /* ... print the error ... */ pathrs_errorinfo_free(error); return -1; } return fd; } /* * Safely set the AppArmor exec label for the current process. This is * something runc does while configuring the container process. */ int write_apparmor_label(const char *label) { int fd, err; /* * Note the usage of O_NOFOLLOW here. You should use O_NOFOLLOW except in * the very rare case where you need to open a magic-link or you really * want to follow a trailing symlink. */ fd = pathrs_proc_open(PATHRS_PROC_SELF, "attr/apparmor/exec", O_WRONLY|O_NOFOLLOW); if (IS_PATHRS_ERR(fd)) { pathrs_error_t *error = pathrs_errorinfo(fd); /* ... print the error ... */ pathrs_errorinfo_free(error); return -1; } err = write(fd, label, strlen(label)); close(fd); return err; } /* * Sometimes you need to get the "real" path of a file descriptor. This path * MUST NOT be used for actual filesystem operations, because it's possible for * an attacker to move the file or change one of the path components to a * symlink, which could lead to you operating on files you didn't expect * (including host files if you're a container runtime). * * In most cases, this kind of function would be used for diagnostic purposes * (such as in error messages, to provide context about what file the error is * in relation to). */ char *get_unsafe_path(int fd) { char *fdpath; if (asprintf(&fdpath, "fd/%d", fd) < 0) return NULL; int linkbuf_size = 128; char *linkbuf = malloc(size); if (!linkbuf) goto err; for (;;) { int len = pathrs_proc_readlink(PATHRS_PROC_THREAD_SELF, fdpath, linkbuf, linkbuf_size); if (IS_PATHRS_ERR(len)) { pathrs_error_t *error = pathrs_errorinfo(fd); /* ... print the error ... */ pathrs_errorinfo_free(error); goto err; } if (len <= linkbuf_size) break; linkbuf_size = len; linkbuf = realloc(linkbuf, linkbuf_size); if (!linkbuf) goto err; } free(fdpath); return linkbuf; err: free(fdpath); free(linkbuf); return NULL; } ``` [cve-2019-16884]: https://nvd.nist.gov/vuln/detail/CVE-2019-16884 [cve-2019-19921]: https://nvd.nist.gov/vuln/detail/CVE-2019-19921 [lwn-newmount]: https://lwn.net/Articles/759499/ ### Kernel Support ### `libpathrs` is designed to only work with Linux, as it uses several Linux-only APIs. `libpathrs` was designed alongside [`openat2(2)`][] (available since Linux 5.6) and dynamically tries to use the latest kernel features to provide the maximum possible protection against racing attackers. However, it also provides support for older kernel versions (in theory up to Linux 2.6.39 but we do not currently test this) by emulating newer kernel features in userspace. However, we strongly recommend you use at least Linux 5.6 to get a reasonable amount of protection against various attacks, and ideally at least Linux 6.8 to make use of all of the protections we have implemented. See the following table for what kernel features we optionally support and what they are used for. | Feature | Minimum Kernel Version | Description | Fallback | | --------------------- | ----------------------- | ----------- | -------- | | `/proc/thread-self` | Linux 3.17 (2014-10-05) | Used when operating on the current thread's `/proc` directory for use with `PATHRS_PROC_THREAD_SELF`. | `/proc/self/task/$tid` is used, but this might not be available in some edge cases so `/proc/self` is used as a final fallback. | | [`open_tree(2)`][] | Linux 5.2 (2019-07-07) | Used to create a private procfs handle when operating on `/proc` (this is a copy of the host `/proc` -- in most cases this will also strip any overmounts). Requires `CAP_SYS_ADMIN` privileges. | Open a regular handle to `/proc`. This can lead to certain race attacks if the attacker can dynamically create mounts. | | [`fsopen(2)`][] | Linux 5.2 (2019-07-07) | Used to create a private procfs handle when operating on `/proc` (with a completely fresh copy of `/proc` -- in some cases this operation will fail if there are locked overmounts on top of `/proc`). Requires `CAP_SYS_ADMIN` privileges. | Try to use [`open_tree(2)`] instead -- in the case of errors due to locked overmounts, [`open_tree(2)`] will be used to create a recursive copy that preserves the overmounts. This means that an attacker would not be able to actively change the mounts on top of `/proc` but there might be some overmounts that libpathrs will detect (and reject). | | [`openat2(2)`][] | Linux 5.6 (2020-03-29) | In-kernel restrictions of path lookup. This is used extensively by `libpathrs` to safely do path lookups. | Userspace emulated path lookups. | | `subset=pid` | Linux 5.8 (2020-08-02) | Allows for a `procfs` handle created with [`fsopen(2)`][] to not contain any global procfs files that would be dangerous for an attacker to write to. Detached `procfs` mounts with `subset=pid` are deemed safe(r) to leak into containers and so libpathrs will internally cache `subset=pid` `ProcfsHandle`s. | libpathrs's `ProcfsHandle`s will have global files and thus libpathrs will not cache a copy of the file descriptor for each operation (possibly causing substantially higher syscall usage as a result -- our testing found that this can have a performance impact in some cases). | | `STATX_MNT_ID` | Linux 5.8 (2020-08-02) | Used to verify whether there are bind-mounts on top of `/proc` that could result in insecure operations (on systems with `fsopen(2)` or `open_tree(2)` this protection is somewhat redundant for privileged programs -- those kinds of `procfs` handles will typically not have overmounts.) | Parse the `/proc/thread-self/fdinfo/$fd` directly -- for systems with `openat2(2)`, this is guaranteed to be safe against attacks. For systems without `openat2(2)`, we have to fallback to unsafe opens that could be fooled by bind-mounts -- however, we believe that exploitation of this would be difficult in practice (even with an attacker that has the persistent ability to mount to arbitrary paths) due to the way we verify `procfs` accesses. | | `STATX_MNT_ID_UNIQUE` | Linux 6.8 (2024-03-10) | Used for the same reason as `STATX_MNT_ID`, but allows us to protect against mount ID recycling. This is effectively a safer version of `STATX_MNT_ID`. | `STATX_MNT_ID` is used (see the `STATX_MNT_ID` fallback if it's not available either). | For more information about the work behind `openat2(2)`, you can read the following LWN articles (note that the merged version of `openat2(2)` is different to the version described by LWN): * [New AT_ flags for restricting pathname lookup][lwn-atflags] * [Restricting path name lookup with openat2()][lwn-openat2] [`openat2(2)`]: https://www.man7.org/linux/man-pages/man2/openat2.2.html [`open_tree(2)`]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md [`fsopen(2)`]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md [lwn-atflags]: https://lwn.net/Articles/767547/ [lwn-openat2]: https://lwn.net/Articles/796868/ ### License ### `SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later` `libpathrs` is licensed under the terms of the [Mozilla Public License version 2.0][MPL-2.0] or the [GNU Lesser General Public License version 3][LGPL-3.0], at your option. Unless otherwise stated, by intentionally submitting any Contribution (as defined by the Mozilla Public License version 2.0) for inclusion into the `libpathrs` project, you are agreeing to dual-license your Contribution as above, without any additional terms or conditions. ``` libpathrs: safe path resolution on Linux Copyright (C) 2019-2025 Aleksa Sarai Copyright (C) 2019-2025 SUSE LLC == 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 . ``` [MPL-2.0]: LICENSE.MPL-2.0 [LGPL-3.0]: LICENSE.LGPL-3.0 #### Bindings #### `SPDX-License-Identifier: MPL-2.0` The language-specific bindings (the code in `contrib/bindings/` and `go-pathrs/`) are licensed under the Mozilla Public License version 2.0 (available in [`LICENSE.MPL-2.0`][MPL-2.0]). **NOTE**: If you compile `libpathrs.so` into your binary statically, you still need to abide by the license terms of the main `libpathrs` project. ``` libpathrs: safe path resolution on Linux Copyright (C) 2019-2025 Aleksa Sarai Copyright (C) 2019-2025 SUSE LLC 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/. ``` [MPL-2.0]: LICENSE.MPL-2.0 #### Examples #### `SPDX-License-Identifier: MPL-2.0` The example code in `examples/` is licensed under the Mozilla Public License version 2.0 (available in [`LICENSE.MPL-2.0`][MPL-2.0]). ``` libpathrs: safe path resolution on Linux Copyright (C) 2019-2025 Aleksa Sarai Copyright (C) 2019-2025 SUSE LLC 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/. ``` pathrs-0.2.1/build.rs000064400000000000000000000062461046102023000126020ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use std::{env, io::Write}; use tempfile::NamedTempFile; fn main() { // Add DT_SONAME and other ELF metadata to our cdylibs. We can't check the // crate-type here directly, but we can at least avoid needless warnings for // "cargo build" by only emitting this when the capi feature is enabled. if cfg!(feature = "capi") { let name = "pathrs"; // TODO: Since we use symbol versioning, it seems quite unlikely that we // would ever bump the major version in the SONAME, so we should // probably hard-code this or define it elsewhere. let major = env::var("CARGO_PKG_VERSION_MAJOR").unwrap(); println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,lib{name}.so.{major}"); let (mut version_script_file, version_script_path) = NamedTempFile::with_prefix("libpathrs-version-script.") .expect("mktemp") .keep() .expect("persist mktemp"); let version_script_path = version_script_path .to_str() .expect("mktemp should be utf-8 safe string"); writeln!( version_script_file, // All of the symbol versions are done with in-line .symver entries. // This version script is only needed to define the version nodes // (and their dependencies). // FIXME: "local" doesn't appear to actually hide symbols in the // output .so. For more information about getting all of this to // work nicely, see . r#" LIBPATHRS_0.2 {{ local: *; }} LIBPATHRS_0.1; LIBPATHRS_0.1 {{ }}; "# ) .expect("write version script"); println!("cargo:rustc-cdylib-link-arg=-Wl,--version-script={version_script_path}"); println!("cargo:rustc-cdylib-link-arg=-Wl,--wrap=pathrs_inroot_open_bad"); } } pathrs-0.2.1/cbindgen.toml000064400000000000000000000133251046102023000135770ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # == 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 . language = "C" header = """ // SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif #define __CBINDGEN_ALIGNED(n) __attribute__((aligned(n))) """ trailer = """ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif """ include_guard = "LIBPATHRS_H" autogen_warning = """ /* * WARNING: This file was auto-generated by rust-cbindgen. Don't modify it. * Instead, re-generate it with: * % cbindgen -c cbindgen.toml -o include/pathrs.h */ """ sys_includes = [ # Needed for dev_t. "sys/types.h", ] after_includes = """ /* * Returns whether the given numerical value is a libpathrs error (which can be * passed to pathrs_errorinfo()). Users are recommended to use this instead of * a bare `<0` comparison because some functions may return a negative number * even in a success condition. */ #define IS_PATHRS_ERR(ret) ((ret) < __PATHRS_MAX_ERR_VALUE) /* * Used to construct pathrs_proc_base_t values for a PID (or TID). Passing * PATHRS_PROC_PID(pid) to pathrs_proc_*() as pathrs_proc_base_t will cause * libpathrs to use /proc/$pid as the base of the operation. * * This is essentially functionally equivalent to prefixing "$pid/" to the * subpath argument and using PATHRS_PROC_ROOT. * * Note that this operation is inherently racy -- the process referenced by this * PID may have died and the PID recycled with a different process. In * principle, this means that it is only really safe to use this with: * * - PID 1 (the init process), as that PID cannot ever get recycled. * - Your current PID (though you should just use PATHRS_PROC_SELF). * - Your current TID (though you should just use PATHRS_PROC_THREAD_SELF), * or _possibly_ other TIDs in your thread-group if you are absolutely sure * they have not been reaped (typically with pthread_join(3), though there * are other ways). * - PIDs of child processes (as long as you are sure that no other part of * your program incorrectly catches or ignores SIGCHLD, and that you do it * *before* you call wait(2) or any equivalent method that could reap * zombies). * * Outside of those specific uses, users should probably avoid using this. */ #define PATHRS_PROC_PID(n) (__PATHRS_PROC_TYPE_PID | (n)) /* * A sentinel value to tell `pathrs_proc_*` methods to use the default procfs * root handle (which may be globally cached). */ #define PATHRS_PROC_DEFAULT_ROOTFD -9 /* (-EBADF) */ """ # Basic kernel-style formatting (can't use tabs so just use 4-spaces). line_length = 80 tab_width = 4 usize_is_size_t = true style = "type" [layout] aligned_n = "__CBINDGEN_ALIGNED" [export] exclude = [ # Don't generate a "typedef" for CBorrowedFd -- FFI-wise, CBorrowedFd is # just an int. "CBorrowedFd", # CReturn is a rust-only typedef. "CReturn", # Don't export the RESOLVE_* definitions. "RESOLVE_NO_XDEV", "RESOLVE_NO_MAGICLINKS", "RESOLVE_NO_SYMLINKS", "RESOLVE_BENEATH", "RESOLVE_IN_ROOT", ] # Clean up the naming of structs. [export.rename] "CProcfsBase" = "pathrs_proc_base_t" "ProcfsOpenFlags" = "uint64_t" "ProcfsOpenHow" = "pathrs_procfs_open_how" # Error API. "CError" = "pathrs_error_t" # The bare return values used for "kernel-like" APIs. "RawFd" = "int" "BorrowedFd" = "int" "CBorrowedFd" = "int" pathrs-0.2.1/contrib/bindings/go/.golangci.yml000064400000000000000000000015441046102023000173570ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. version: "2" linters: enable: - bidichk - cyclop - errname - errorlint - exhaustive - goconst - godot - gomoddirectives - gosec - mirror - misspell - mnd - nilerr - nilnil - perfsprint - prealloc - reassign - revive - unconvert - unparam - usestdlibvars - wastedassign formatters: enable: - gofumpt - goimports settings: goimports: local-prefixes: - cyphar.com/go-pathrs pathrs-0.2.1/contrib/bindings/go/COPYING000064400000000000000000000405261046102023000160310ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. pathrs-0.2.1/contrib/bindings/go/doc.go000064400000000000000000000007701046102023000160670ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package pathrs provides bindings for libpathrs, a library for safe path // resolution on Linux. package pathrs pathrs-0.2.1/contrib/bindings/go/go.mod000064400000000000000000000001731046102023000160760ustar 00000000000000// Hosted at . module cyphar.com/go-pathrs go 1.18 require golang.org/x/sys v0.26.0 pathrs-0.2.1/contrib/bindings/go/go.sum000064400000000000000000000002311046102023000161160ustar 00000000000000golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= pathrs-0.2.1/contrib/bindings/go/handle_linux.go000064400000000000000000000076341046102023000200020ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package pathrs import ( "fmt" "os" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // Handle is a handle for a path within a given [Root]. This handle references // an already-resolved path which can be used for only one purpose -- to // "re-open" the handle and get an actual [os.File] which can be used for // ordinary operations. // // If you wish to open a file without having an intermediate [Handle] object, // you can try to use [Root.Open] or [Root.OpenFile]. // // It is critical that perform all relevant operations through this [Handle] // (rather than fetching the file descriptor yourself with [Handle.IntoRaw]), // because the security properties of libpathrs depend on users doing all // relevant filesystem operations through libpathrs. // // [os.File]: https://pkg.go.dev/os#File type Handle struct { inner *os.File } // HandleFromFile creates a new [Handle] from an existing file handle. The // handle will be copied by this method, so the original handle should still be // freed by the caller. // // This is effectively the inverse operation of [Handle.IntoRaw], and is used // for "deserialising" pathrs root handles. func HandleFromFile(file *os.File) (*Handle, error) { newFile, err := fdutils.DupFile(file) if err != nil { return nil, fmt.Errorf("duplicate handle fd: %w", err) } return &Handle{inner: newFile}, nil } // Open creates an "upgraded" file handle to the file referenced by the // [Handle]. Note that the original [Handle] is not consumed by this operation, // and can be opened multiple times. // // The handle returned is only usable for reading, and this is method is // shorthand for [Handle.OpenFile] with os.O_RDONLY. // // TODO: Rename these to "Reopen" or something. func (h *Handle) Open() (*os.File, error) { return h.OpenFile(os.O_RDONLY) } // OpenFile creates an "upgraded" file handle to the file referenced by the // [Handle]. Note that the original [Handle] is not consumed by this operation, // and can be opened multiple times. // // The provided flags indicate which open(2) flags are used to create the new // handle. // // TODO: Rename these to "Reopen" or something. func (h *Handle) OpenFile(flags int) (*os.File, error) { return fdutils.WithFileFd(h.inner, func(fd uintptr) (*os.File, error) { newFd, err := libpathrs.Reopen(fd, flags) if err != nil { return nil, err } return os.NewFile(newFd, h.inner.Name()), nil }) } // IntoFile unwraps the [Handle] into its underlying [os.File]. // // You almost certainly want to use [Handle.OpenFile] to get a non-O_PATH // version of this [Handle]. // // This operation returns the internal [os.File] of the [Handle] directly, so // calling [Handle.Close] will also close any copies of the returned [os.File]. // If you want to get an independent copy, use [Handle.Clone] followed by // [Handle.IntoFile] on the cloned [Handle]. // // [os.File]: https://pkg.go.dev/os#File func (h *Handle) IntoFile() *os.File { // TODO: Figure out if we really don't want to make a copy. // TODO: We almost certainly want to clear r.inner here, but we can't do // that easily atomically (we could use atomic.Value but that'll make // things quite a bit uglier). return h.inner } // Clone creates a copy of a [Handle], such that it has a separate lifetime to // the original (while referring to the same underlying file). func (h *Handle) Clone() (*Handle, error) { return HandleFromFile(h.inner) } // Close frees all of the resources used by the [Handle]. func (h *Handle) Close() error { return h.inner.Close() } pathrs-0.2.1/contrib/bindings/go/internal/fdutils/fd_linux.go000064400000000000000000000042531046102023000224200ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package fdutils contains a few helper methods when dealing with *os.File and // file descriptors. package fdutils import ( "fmt" "os" "golang.org/x/sys/unix" "cyphar.com/go-pathrs/internal/libpathrs" ) // DupFd makes a duplicate of the given fd. func DupFd(fd uintptr, name string) (*os.File, error) { newFd, err := unix.FcntlInt(fd, unix.F_DUPFD_CLOEXEC, 0) if err != nil { return nil, fmt.Errorf("fcntl(F_DUPFD_CLOEXEC): %w", err) } return os.NewFile(uintptr(newFd), name), nil } // WithFileFd is a more ergonomic wrapper around file.SyscallConn().Control(). func WithFileFd[T any](file *os.File, fn func(fd uintptr) (T, error)) (T, error) { conn, err := file.SyscallConn() if err != nil { return *new(T), err } var ( ret T innerErr error ) if err := conn.Control(func(fd uintptr) { ret, innerErr = fn(fd) }); err != nil { return *new(T), err } return ret, innerErr } // DupFile makes a duplicate of the given file. func DupFile(file *os.File) (*os.File, error) { return WithFileFd(file, func(fd uintptr) (*os.File, error) { return DupFd(fd, file.Name()) }) } // MkFile creates a new *os.File from the provided file descriptor. However, // unlike os.NewFile, the file's Name is based on the real path (provided by // /proc/self/fd/$n). func MkFile(fd uintptr) (*os.File, error) { fdPath := fmt.Sprintf("fd/%d", fd) fdName, err := libpathrs.ProcReadlinkat(libpathrs.ProcDefaultRootFd, libpathrs.ProcThreadSelf, fdPath) if err != nil { _ = unix.Close(int(fd)) return nil, fmt.Errorf("failed to fetch real name of fd %d: %w", fd, err) } // TODO: Maybe we should prefix this name with something to indicate to // users that they must not use this path as a "safe" path. Something like // "//pathrs-handle:/foo/bar"? return os.NewFile(fd, fdName), nil } pathrs-0.2.1/contrib/bindings/go/internal/libpathrs/error_unix.go000064400000000000000000000016551046102023000233250ustar 00000000000000//go:build linux // TODO: Use "go:build unix" once we bump the minimum Go version 1.19. // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package libpathrs import ( "syscall" ) // Error represents an underlying libpathrs error. type Error struct { description string errno syscall.Errno } // Error returns a textual description of the error. func (err *Error) Error() string { return err.description } // Unwrap returns the underlying error which was wrapped by this error (if // applicable). func (err *Error) Unwrap() error { if err.errno != 0 { return err.errno } return nil } pathrs-0.2.1/contrib/bindings/go/internal/libpathrs/libpathrs_linux.go000064400000000000000000000252251046102023000243370ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package libpathrs is an internal thin wrapper around the libpathrs C API. package libpathrs import ( "fmt" "syscall" "unsafe" ) /* // TODO: Figure out if we need to add support for linking against libpathrs // statically even if in dynamically linked builds in order to make // packaging a bit easier (using "-Wl,-Bstatic -lpathrs -Wl,-Bdynamic" or // "-l:pathrs.a"). #cgo pkg-config: pathrs #include // This is a workaround for unsafe.Pointer() not working for non-void pointers. char *cast_ptr(void *ptr) { return ptr; } */ import "C" func fetchError(errID C.int) error { if errID >= C.__PATHRS_MAX_ERR_VALUE { return nil } cErr := C.pathrs_errorinfo(errID) defer C.pathrs_errorinfo_free(cErr) var err error if cErr != nil { err = &Error{ errno: syscall.Errno(cErr.saved_errno), description: C.GoString(cErr.description), } } return err } // OpenRoot wraps pathrs_open_root. func OpenRoot(path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_open_root(cPath) return uintptr(fd), fetchError(fd) } // Reopen wraps pathrs_reopen. func Reopen(fd uintptr, flags int) (uintptr, error) { newFd := C.pathrs_reopen(C.int(fd), C.int(flags)) return uintptr(newFd), fetchError(newFd) } // InRootResolve wraps pathrs_inroot_resolve. func InRootResolve(rootFd uintptr, path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_resolve(C.int(rootFd), cPath) return uintptr(fd), fetchError(fd) } // InRootResolveNoFollow wraps pathrs_inroot_resolve_nofollow. func InRootResolveNoFollow(rootFd uintptr, path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_resolve_nofollow(C.int(rootFd), cPath) return uintptr(fd), fetchError(fd) } // InRootOpen wraps pathrs_inroot_open. func InRootOpen(rootFd uintptr, path string, flags int) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_open(C.int(rootFd), cPath, C.int(flags)) return uintptr(fd), fetchError(fd) } // InRootReadlink wraps pathrs_inroot_readlink. func InRootReadlink(rootFd uintptr, path string) (string, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) size := 128 for { linkBuf := make([]byte, size) n := C.pathrs_inroot_readlink(C.int(rootFd), cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf))) switch { case int(n) < C.__PATHRS_MAX_ERR_VALUE: return "", fetchError(n) case int(n) <= len(linkBuf): return string(linkBuf[:int(n)]), nil default: // The contents were truncated. Unlike readlinkat, pathrs returns // the size of the link when it checked. So use the returned size // as a basis for the reallocated size (but in order to avoid a DoS // where a magic-link is growing by a single byte each iteration, // make sure we are a fair bit larger). size += int(n) } } } // InRootRmdir wraps pathrs_inroot_rmdir. func InRootRmdir(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_rmdir(C.int(rootFd), cPath) return fetchError(err) } // InRootUnlink wraps pathrs_inroot_unlink. func InRootUnlink(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_unlink(C.int(rootFd), cPath) return fetchError(err) } // InRootRemoveAll wraps pathrs_inroot_remove_all. func InRootRemoveAll(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_remove_all(C.int(rootFd), cPath) return fetchError(err) } // InRootCreat wraps pathrs_inroot_creat. func InRootCreat(rootFd uintptr, path string, flags int, mode uint32) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_creat(C.int(rootFd), cPath, C.int(flags), C.uint(mode)) return uintptr(fd), fetchError(fd) } // InRootRename wraps pathrs_inroot_rename. func InRootRename(rootFd uintptr, src, dst string, flags uint) error { cSrc := C.CString(src) defer C.free(unsafe.Pointer(cSrc)) cDst := C.CString(dst) defer C.free(unsafe.Pointer(cDst)) err := C.pathrs_inroot_rename(C.int(rootFd), cSrc, cDst, C.uint(flags)) return fetchError(err) } // InRootMkdir wraps pathrs_inroot_mkdir. func InRootMkdir(rootFd uintptr, path string, mode uint32) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_mkdir(C.int(rootFd), cPath, C.uint(mode)) return fetchError(err) } // InRootMkdirAll wraps pathrs_inroot_mkdir_all. func InRootMkdirAll(rootFd uintptr, path string, mode uint32) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_mkdir_all(C.int(rootFd), cPath, C.uint(mode)) return uintptr(fd), fetchError(fd) } // InRootMknod wraps pathrs_inroot_mknod. func InRootMknod(rootFd uintptr, path string, mode uint32, dev uint64) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_mknod(C.int(rootFd), cPath, C.uint(mode), C.dev_t(dev)) return fetchError(err) } // InRootSymlink wraps pathrs_inroot_symlink. func InRootSymlink(rootFd uintptr, path, target string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) cTarget := C.CString(target) defer C.free(unsafe.Pointer(cTarget)) err := C.pathrs_inroot_symlink(C.int(rootFd), cPath, cTarget) return fetchError(err) } // InRootHardlink wraps pathrs_inroot_hardlink. func InRootHardlink(rootFd uintptr, path, target string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) cTarget := C.CString(target) defer C.free(unsafe.Pointer(cTarget)) err := C.pathrs_inroot_hardlink(C.int(rootFd), cPath, cTarget) return fetchError(err) } // ProcBase is pathrs_proc_base_t (uint64_t). type ProcBase C.pathrs_proc_base_t // FIXME: We need to open-code the constants because CGo unfortunately will // implicitly convert any non-literal constants (i.e. those resolved using gcc) // to signed integers. See for some // more information on the underlying issue (though. const ( // ProcRoot is PATHRS_PROC_ROOT. ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT // ProcSelf is PATHRS_PROC_SELF. ProcSelf ProcBase = 0xFFFF_FFFE_091D_5E1F // C.PATHRS_PROC_SELF // ProcThreadSelf is PATHRS_PROC_THREAD_SELF. ProcThreadSelf ProcBase = 0xFFFF_FFFE_3EAD_5E1F // C.PATHRS_PROC_THREAD_SELF // ProcBaseTypeMask is __PATHRS_PROC_TYPE_MASK. ProcBaseTypeMask ProcBase = 0xFFFF_FFFF_0000_0000 // C.__PATHRS_PROC_TYPE_MASK // ProcBaseTypePid is __PATHRS_PROC_TYPE_PID. ProcBaseTypePid ProcBase = 0x8000_0000_0000_0000 // C.__PATHRS_PROC_TYPE_PID // ProcDefaultRootFd is PATHRS_PROC_DEFAULT_ROOTFD. ProcDefaultRootFd = -int(syscall.EBADF) // C.PATHRS_PROC_DEFAULT_ROOTFD ) func assertEqual[T comparable](a, b T, msg string) { if a != b { panic(fmt.Sprintf("%s ((%T) %#v != (%T) %#v)", msg, a, a, b, b)) } } // Verify that the values above match the actual C values. Unfortunately, Go // only allows us to forcefully cast int64 to uint64 if you use a temporary // variable, which means we cannot do it in a const context and thus need to do // it at runtime (even though it is a check that fundamentally could be done at // compile-time)... func init() { var ( actualProcRoot int64 = C.PATHRS_PROC_ROOT actualProcSelf int64 = C.PATHRS_PROC_SELF actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF ) assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT") assertEqual(ProcSelf, ProcBase(actualProcSelf), "PATHRS_PROC_SELF") assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF") var ( actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID ) assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK") assertEqual(ProcBaseTypePid, ProcBase(actualProcBaseTypePid), "__PATHRS_PROC_TYPE_PID") assertEqual(ProcDefaultRootFd, int(C.PATHRS_PROC_DEFAULT_ROOTFD), "PATHRS_PROC_DEFAULT_ROOTFD") } // ProcPid reimplements the PROC_PID(x) conversion. func ProcPid(pid uint32) ProcBase { return ProcBaseTypePid | ProcBase(pid) } // ProcOpenat wraps pathrs_proc_openat. func ProcOpenat(procRootFd int, base ProcBase, path string, flags int) (uintptr, error) { cBase := C.pathrs_proc_base_t(base) cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_proc_openat(C.int(procRootFd), cBase, cPath, C.int(flags)) return uintptr(fd), fetchError(fd) } // ProcReadlinkat wraps pathrs_proc_readlinkat. func ProcReadlinkat(procRootFd int, base ProcBase, path string) (string, error) { // TODO: See if we can unify this code with InRootReadlink. cBase := C.pathrs_proc_base_t(base) cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) size := 128 for { linkBuf := make([]byte, size) n := C.pathrs_proc_readlinkat( C.int(procRootFd), cBase, cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf))) switch { case int(n) < C.__PATHRS_MAX_ERR_VALUE: return "", fetchError(n) case int(n) <= len(linkBuf): return string(linkBuf[:int(n)]), nil default: // The contents were truncated. Unlike readlinkat, pathrs returns // the size of the link when it checked. So use the returned size // as a basis for the reallocated size (but in order to avoid a DoS // where a magic-link is growing by a single byte each iteration, // make sure we are a fair bit larger). size += int(n) } } } // ProcfsOpenHow is pathrs_procfs_open_how (struct). type ProcfsOpenHow C.pathrs_procfs_open_how const ( // ProcfsNewUnmasked is PATHRS_PROCFS_NEW_UNMASKED. ProcfsNewUnmasked = C.PATHRS_PROCFS_NEW_UNMASKED ) // Flags returns a pointer to the internal flags field to allow other packages // to modify structure fields that are internal due to Go's visibility model. func (how *ProcfsOpenHow) Flags() *C.uint64_t { return &how.flags } // ProcfsOpen is pathrs_procfs_open (sizeof(*how) is passed automatically). func ProcfsOpen(how *ProcfsOpenHow) (uintptr, error) { fd := C.pathrs_procfs_open((*C.pathrs_procfs_open_how)(how), C.size_t(unsafe.Sizeof(*how))) return uintptr(fd), fetchError(fd) } pathrs-0.2.1/contrib/bindings/go/pathrs.h000064400000000000000000000771631046102023000164570ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif #define __CBINDGEN_ALIGNED(n) __attribute__((aligned(n))) #ifndef LIBPATHRS_H #define LIBPATHRS_H /* * WARNING: This file was auto-generated by rust-cbindgen. Don't modify it. * Instead, re-generate it with: * % cbindgen -c cbindgen.toml -o include/pathrs.h */ #include #include #include #include #include #include /* * Returns whether the given numerical value is a libpathrs error (which can be * passed to pathrs_errorinfo()). Users are recommended to use this instead of * a bare `<0` comparison because some functions may return a negative number * even in a success condition. */ #define IS_PATHRS_ERR(ret) ((ret) < __PATHRS_MAX_ERR_VALUE) /* * Used to construct pathrs_proc_base_t values for a PID (or TID). Passing * PATHRS_PROC_PID(pid) to pathrs_proc_*() as pathrs_proc_base_t will cause * libpathrs to use /proc/$pid as the base of the operation. * * This is essentially functionally equivalent to prefixing "$pid/" to the * subpath argument and using PATHRS_PROC_ROOT. * * Note that this operation is inherently racy -- the process referenced by this * PID may have died and the PID recycled with a different process. In * principle, this means that it is only really safe to use this with: * * - PID 1 (the init process), as that PID cannot ever get recycled. * - Your current PID (though you should just use PATHRS_PROC_SELF). * - Your current TID (though you should just use PATHRS_PROC_THREAD_SELF), * or _possibly_ other TIDs in your thread-group if you are absolutely sure * they have not been reaped (typically with pthread_join(3), though there * are other ways). * - PIDs of child processes (as long as you are sure that no other part of * your program incorrectly catches or ignores SIGCHLD, and that you do it * *before* you call wait(2) or any equivalent method that could reap * zombies). * * Outside of those specific uses, users should probably avoid using this. */ #define PATHRS_PROC_PID(n) (__PATHRS_PROC_TYPE_PID | (n)) /* * A sentinel value to tell `pathrs_proc_*` methods to use the default procfs * root handle (which may be globally cached). */ #define PATHRS_PROC_DEFAULT_ROOTFD -9 /* (-EBADF) */ /** * Bits in `pathrs_proc_base_t` that indicate the type of the base value. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_PROC_TYPE_MASK 18446744069414584320ull /** * Bits in `pathrs_proc_base_t` that must be set for `/proc/$pid` values. Don't * use this directly, instead use `PATHRS_PROC_PID(n)` to convert a PID to an * appropriate `pathrs_proc_base_t` value. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_PROC_TYPE_PID 9223372036854775808ull /** * Construct a completely unmasked procfs handle. * * This is equivalent to [`ProcfsHandleBuilder::unmasked`], and is meant as * a flag argument to [`ProcfsOpenFlags`] (the `flags` field in `struct * pathrs_procfs_open_how`) for use with pathrs_procfs_open(). */ #define PATHRS_PROCFS_NEW_UNMASKED 1 /** * Indicate what base directory should be used when doing operations with * `pathrs_proc_*`. In addition to the values defined here, the following * macros can be used for other values: * * * `PATHRS_PROC_PID(pid)` refers to the `/proc/` directory for the * process with PID (or TID) `pid`. * * Note that this operation is inherently racy and should probably avoided * for most uses -- see the block comment above `PATHRS_PROC_PID(n)` for * more details. * * Unknown values will result in an error being returned. */ enum pathrs_proc_base_t { /** * Use /proc. Note that this mode may be more expensive because we have * to take steps to try to avoid leaking unmasked procfs handles, so you * should use PATHRS_PROC_SELF if you can. */ PATHRS_PROC_ROOT = 18446744067006164835ull, /** * Use /proc/self. For most programs, this is the standard choice. */ PATHRS_PROC_SELF = 18446744065272536607ull, /** * Use /proc/thread-self. In multi-threaded programs where one thread has a * different CLONE_FS, it is possible for /proc/self to point the wrong * thread and so /proc/thread-self may be necessary. * * NOTE: Using /proc/thread-self may require care if used from languages * where your code can change threads without warning and old threads can * be killed (such as Go -- where you want to use runtime.LockOSThread). */ PATHRS_PROC_THREAD_SELF = 18446744066171166239ull, }; typedef uint64_t pathrs_proc_base_t; typedef struct { uint64_t flags; } pathrs_procfs_open_how; /** * Attempts to represent a Rust Error type in C. This structure must be freed * using pathrs_errorinfo_free(). */ typedef struct __CBINDGEN_ALIGNED(8) { /** * Raw errno(3) value of the underlying error (or 0 if the source of the * error was not due to a syscall error). */ uint64_t saved_errno; /** * Textual description of the error. */ const char *description; } pathrs_error_t; /** * The smallest return value which cannot be a libpathrs error ID. * * While all libpathrs error IDs are negative numbers, some functions may * return a negative number in a success scenario. This macro defines the high * range end of the numbers that can be used as an error ID. Don't use this * value directly, instead use `IS_PATHRS_ERR(ret)` to check if a returned * value is an error or not. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_MAX_ERR_VALUE -4096 /** * Open a root handle. * * The provided path must be an existing directory. * * Note that root handles are not special -- this function is effectively * equivalent to * * ```c * fd = open(path, O_PATH|O_DIRECTORY); * ``` * * # Return Value * * On success, this function returns a file descriptor that can be used as a * root handle in subsequent pathrs_inroot_* operations. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_open_root(const char *path); /** * "Upgrade" an O_PATH file descriptor to a usable fd, suitable for reading and * writing. This does not consume the original file descriptor. (This can be * used with non-O_PATH file descriptors as well.) * * It should be noted that the use of O_CREAT *is not* supported (and will * result in an error). Handles only refer to *existing* files. Instead you * need to use pathrs_inroot_creat(). * * In addition, O_NOCTTY is automatically set when opening the path. If you * want to use the path as a controlling terminal, you will have to do * ioctl(fd, TIOCSCTTY, 0) yourself. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_reopen(int fd, int flags); /** * Resolve the given path within the rootfs referenced by root_fd. The path * *must already exist*, otherwise an error will occur. * * All symlinks (including trailing symlinks) are followed, but they are * resolved within the rootfs. If you wish to open a handle to the symlink * itself, use pathrs_inroot_resolve_nofollow(). * * # Return Value * * On success, this function returns an O_PATH file descriptor referencing the * resolved path. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_resolve(int root_fd, const char *path); /** * pathrs_inroot_resolve_nofollow() is effectively an O_NOFOLLOW version of * pathrs_inroot_resolve(). Their behaviour is identical, except that * *trailing* symlinks will not be followed. If the final component is a * trailing symlink, an O_PATH|O_NOFOLLOW handle to the symlink itself is * returned. * * # Return Value * * On success, this function returns an O_PATH file descriptor referencing the * resolved path. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_resolve_nofollow(int root_fd, const char *path); /** * pathrs_inroot_open() is effectively shorthand for pathrs_inroot_resolve() * followed by pathrs_reopen(). If you only need to open a path and don't care * about re-opening it later, this can be slightly more efficient than the * alternative for the openat2-based resolver as it doesn't require allocating * an extra file descriptor. For languages where C FFI is expensive (such as * Go), using this also saves a function call. * * If flags contains O_NOFOLLOW, the behaviour is like that of * pathrs_inroot_resolve_nofollow() followed by pathrs_reopen(). * * In addition, O_NOCTTY is automatically set when opening the path. If you * want to use the path as a controlling terminal, you will have to do * ioctl(fd, TIOCSCTTY, 0) yourself. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_open(int root_fd, const char *path, int flags); /** * Get the target of a symlink within the rootfs referenced by root_fd. * * NOTE: The returned path is not modified to be "safe" outside of the * root. You should not use this path for doing further path lookups -- use * pathrs_inroot_resolve() instead. * * This method is just shorthand for: * * ```c * int linkfd = pathrs_inroot_resolve_nofollow(rootfd, path); * if (IS_PATHRS_ERR(linkfd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * copied = readlinkat(linkfd, "", linkbuf, linkbuf_size); * close(linkfd); * ``` * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_inroot_readlink() will return *the * number of bytes it would have copied if the buffer was large enough*. This * matches the behaviour of pathrs_proc_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_readlink(int root_fd, const char *path, char *linkbuf, size_t linkbuf_size); /** * Rename a path within the rootfs referenced by root_fd. The flags argument is * identical to the renameat2(2) flags that are supported on the system. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_rename(int root_fd, const char *src, const char *dst, uint32_t flags); /** * Remove the empty directory at path within the rootfs referenced by root_fd. * * The semantics are effectively equivalent to unlinkat(..., AT_REMOVEDIR). * This function will return an error if the path doesn't exist, was not a * directory, or was a non-empty directory. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_rmdir(int root_fd, const char *path); /** * Remove the file (a non-directory inode) at path within the rootfs referenced * by root_fd. * * The semantics are effectively equivalent to unlinkat(..., 0). This function * will return an error if the path doesn't exist or was a directory. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_unlink(int root_fd, const char *path); /** * Recursively delete the path and any children it contains if it is a * directory. The semantics are equivalent to `rm -r`. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_remove_all(int root_fd, const char *path); /** * Create a new regular file within the rootfs referenced by root_fd. This is * effectively an O_CREAT operation, and so (unlike pathrs_inroot_resolve()), * this function can be used on non-existent paths. * * If you want to ensure the creation is a new file, use O_EXCL. * * If you want to create a file without opening a handle to it, you can do * pathrs_inroot_mknod(root_fd, path, S_IFREG|mode, 0) instead. * * As with pathrs_reopen(), O_NOCTTY is automatically set when opening the * path. If you want to use the path as a controlling terminal, you will have * to do ioctl(fd, TIOCSCTTY, 0) yourself. * * NOTE: Unlike O_CREAT, pathrs_inroot_creat() will return an error if the * final component is a dangling symlink. O_CREAT will create such files, and * while openat2 does support this it would be difficult to implement this in * the emulated resolver. * * # Return Value * * On success, this function returns a file descriptor to the requested file. * The open flags are based on the provided flags. The file descriptor will * have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_creat(int root_fd, const char *path, int flags, unsigned int mode); /** * Create a new directory within the rootfs referenced by root_fd. * * This is shorthand for pathrs_inroot_mknod(root_fd, path, S_IFDIR|mode, 0). * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mkdir(int root_fd, const char *path, unsigned int mode); /** * Create a new directory (and any of its path components if they don't exist) * within the rootfs referenced by root_fd. * * # Return Value * * On success, this function returns an O_DIRECTORY file descriptor to the * newly created directory. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mkdir_all(int root_fd, const char *path, unsigned int mode); /** * Create a inode within the rootfs referenced by root_fd. The type of inode to * be created is configured using the S_IFMT bits in mode (a-la mknod(2)). * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mknod(int root_fd, const char *path, unsigned int mode, dev_t dev); /** * Create a symlink within the rootfs referenced by root_fd. Note that the * symlink target string is not modified when creating the symlink. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_symlink(int root_fd, const char *path, const char *target); /** * Create a hardlink within the rootfs referenced by root_fd. Both the hardlink * path and target are resolved within the rootfs. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_hardlink(int root_fd, const char *path, const char *target); /** * Create a new (custom) procfs root handle. * * This is effectively a C wrapper around [`ProcfsHandleBuilder`], allowing you * to create a custom procfs root handle that can be used with other * `pathrs_proc_*at` methods. * * While most users should just use `PATHRS_PROC_DEFAULT_ROOTFD` (or the * non-`at` variants of `pathrs_proc_*`), creating an unmasked procfs root * handle (using `PATHRS_PROCFS_NEW_UNMASKED`) can be useful for programs that * need to operate on a lot of global procfs files. (Note that accessing global * procfs files does not *require* creating a custom procfs handle -- * `pathrs_proc_*` will automatically create a global-friendly handle * internally when necessary but will close it immediately after operating on * it.) * * # Extensible Structs * * The [`ProcfsOpenHow`] (`struct pathrs_procfs_open_how`) argument is * designed to be extensible, modelled after the extensible structs scheme used * by Linux (for syscalls such as [clone3(2)], [openat2(2)] and other such * syscalls). Normally one would use symbol versioning to achieve this, but * unfortunately Rust's symbol versioning support is incredibly primitive (one * might even say "non-existent") and so this system is more robust, even if * the calling convention is a little strange for userspace libraries. * * In addition to a pointer argument, the caller must also provide the size of * the structure it is passing. By providing this information, it is possible * for `pathrs_procfs_open()` to provide both forwards- and * backwards-compatibility, with size acting as an implicit version number. * (Because new extension fields will always be appended, the structure size * will always increase.) * * If we let `usize` be the structure specified by the caller, and `lsize` be * the size of the structure internal to libpathrs, then there are three cases * to consider: * * * If `usize == lsize`, then there is no version mismatch and the structure * provided by the caller can be used verbatim. * * If `usize < lsize`, then there are some extension fields which libpathrs * supports that the caller does not. Because a zero value in any added * extension field signifies a no-op, libpathrs treats all of the extension * fields not provided by the caller as having zero values. This provides * backwards-compatibility. * * If `usize > lsize`, then there are some extension fields which the caller * is aware of but this version of libpathrs does not support. Because any * extension field must have its zero values signify a no-op, libpathrs can * safely ignore the unsupported extension fields if they are all-zero. If * any unsupported extension fields are nonzero, then an `E2BIG` error is * returned. This provides forwards-compatibility. * * Because the definition of `struct pathrs_procfs_open_how` may open in the * future * * Because the definition of `struct pathrs_procfs_open_how` may change in the * future (with new fields being added when headers are updated), callers * should zero-fill the structure to ensure that recompiling the program with * new headers will not result in spurious errors at run time. The simplest * way is to use a designated initialiser: * * ```c * struct pathrs_procfs_open_how how = { * .flags = PATHRS_PROCFS_NEW_UNMASKED, * }; * ``` * * or explicitly using `memset(3)` or similar: * * ```c * struct pathrs_procfs_open_how how; * memset(&how, 0, sizeof(how)); * how.flags = PATHRS_PROCFS_NEW_UNMASKED; * ``` * * # Return Value * * On success, this function returns *either* a file descriptor *or* * `PATHRS_PROC_DEFAULT_ROOTFD` (this is a negative number, equal to `-EBADF`). * The file descriptor will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). * * [clone3(2)]: https://www.man7.org/linux/man-pages/man2/clone3.2.html * [openat2(2)]: https://www.man7.org/linux/man-pages/man2/openat2.2.html */ int pathrs_procfs_open(const pathrs_procfs_open_how *args, size_t size); /** * `pathrs_proc_open` but with a caller-provided file descriptor for `/proc`. * * Internally, `pathrs_proc_open` will attempt to use a cached copy of a very * restricted `/proc` handle (a detached mount object with `subset=pid` and * `hidepid=4`). If a user requests a global `/proc` file, a temporary handle * capable of accessing global files is created and destroyed after the * operation completes. * * For most users, this is more than sufficient. However, if a user needs to * operate on many global `/proc` files, the cost of creating handles can get * quite expensive. `pathrs_proc_openat` allows a user to manually manage the * global-friendly `/proc` handle. Note that passing a `subset=pid` file * descriptor to `pathrs_proc_openat` will *not* stop the automatic creation of * a global-friendly handle internally if necessary. * * In order to get the behaviour of `pathrs_proc_open`, you can pass the * special value `PATHRS_PROC_DEFAULT_ROOTFD` (`-EBADF`) as the `proc_rootfd` * argument. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_openat(int proc_rootfd, pathrs_proc_base_t base, const char *path, int flags); /** * Safely open a path inside a `/proc` handle. * * Any bind-mounts or other over-mounts will (depending on what kernel features * are available) be detected and an error will be returned. Non-trailing * symlinks are followed but care is taken to ensure the symlinks are * legitimate. * * Unless you intend to open a magic-link, `O_NOFOLLOW` should be set in flags. * Lookups with `O_NOFOLLOW` are guaranteed to never be tricked by bind-mounts * (on new enough Linux kernels). * * If you wish to resolve a magic-link, you need to unset `O_NOFOLLOW`. * Unfortunately (if libpathrs is using the regular host `/proc` mount), this * lookup mode cannot protect you against an attacker that can modify the mount * table during this operation. * * NOTE: Instead of using paths like `/proc/thread-self/fd`, `base` is used to * indicate what "base path" inside procfs is used. For example, to re-open a * file descriptor: * * ```c * fd = pathrs_proc_open(PATHRS_PROC_THREAD_SELF, "fd/101", O_RDWR); * if (IS_PATHRS_ERR(fd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * ``` * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_open(pathrs_proc_base_t base, const char *path, int flags); /** * `pathrs_proc_readlink` but with a caller-provided file descriptor for * `/proc`. * * See the documentation of pathrs_proc_openat() for when this API might be * useful. * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_proc_readlink() will return *the number * of bytes it would have copied if the buffer was large enough*. This matches * the behaviour of pathrs_inroot_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_readlinkat(int proc_rootfd, pathrs_proc_base_t base, const char *path, char *linkbuf, size_t linkbuf_size); /** * Safely read the contents of a symlink inside `/proc`. * * As with `pathrs_proc_open`, any bind-mounts or other over-mounts will * (depending on what kernel features are available) be detected and an error * will be returned. Non-trailing symlinks are followed but care is taken to * ensure the symlinks are legitimate. * * This function is effectively shorthand for * * ```c * fd = pathrs_proc_open(base, path, O_PATH|O_NOFOLLOW); * if (IS_PATHRS_ERR(fd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * copied = readlinkat(fd, "", linkbuf, linkbuf_size); * close(fd); * ``` * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_proc_readlink() will return *the number * of bytes it would have copied if the buffer was large enough*. This matches * the behaviour of pathrs_inroot_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_readlink(pathrs_proc_base_t base, const char *path, char *linkbuf, size_t linkbuf_size); /** * Retrieve error information about an error id returned by a pathrs operation. * * Whenever an error occurs with libpathrs, a negative number describing that * error (the error id) is returned. pathrs_errorinfo() is used to retrieve * that information: * * ```c * fd = pathrs_inroot_resolve(root, "/foo/bar"); * if (IS_PATHRS_ERR(fd)) { * // fd is an error id * pathrs_error_t *error = pathrs_errorinfo(fd); * // ... print the error information ... * pathrs_errorinfo_free(error); * } * ``` * * Once pathrs_errorinfo() is called for a particular error id, that error id * is no longer valid and should not be used for subsequent pathrs_errorinfo() * calls. * * Error ids are only unique from one another until pathrs_errorinfo() is * called, at which point the id can be reused for subsequent errors. The * precise format of error ids is completely opaque and they should never be * compared directly or used for anything other than with pathrs_errorinfo(). * * Error ids are not thread-specific and thus pathrs_errorinfo() can be called * on a different thread to the thread where the operation failed (this is of * particular note to green-thread language bindings like Go, where this is * important). * * # Return Value * * If there was a saved error with the provided id, a pathrs_error_t is * returned describing the error. Use pathrs_errorinfo_free() to free the * associated memory once you are done with the error. */ pathrs_error_t *pathrs_errorinfo(int err_id); /** * Free the pathrs_error_t object returned by pathrs_errorinfo(). */ void pathrs_errorinfo_free(pathrs_error_t *ptr); #endif /* LIBPATHRS_H */ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif pathrs-0.2.1/contrib/bindings/go/procfs/procfs_linux.go000064400000000000000000000215241046102023000213310ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package procfs provides a safe API for operating on /proc on Linux. package procfs import ( "os" "runtime" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // ProcBase is used with [ProcReadlink] and related functions to indicate what // /proc subpath path operations should be done relative to. type ProcBase struct { inner libpathrs.ProcBase } var ( // ProcRoot indicates to use /proc. Note that this mode may be more // expensive because we have to take steps to try to avoid leaking unmasked // procfs handles, so you should use [ProcBaseSelf] if you can. ProcRoot = ProcBase{inner: libpathrs.ProcRoot} // ProcSelf indicates to use /proc/self. For most programs, this is the // standard choice. ProcSelf = ProcBase{inner: libpathrs.ProcSelf} // ProcThreadSelf indicates to use /proc/thread-self. In multi-threaded // programs where one thread has a different CLONE_FS, it is possible for // /proc/self to point the wrong thread and so /proc/thread-self may be // necessary. ProcThreadSelf = ProcBase{inner: libpathrs.ProcThreadSelf} ) // ProcPid returns a ProcBase which indicates to use /proc/$pid for the given // PID (or TID). Be aware that due to PID recycling, using this is generally // not safe except in certain circumstances. Namely: // // - PID 1 (the init process), as that PID cannot ever get recycled. // - Your current PID (though you should just use [ProcBaseSelf]). // - Your current TID if you have used [runtime.LockOSThread] (though you // should just use [ProcBaseThreadSelf]). // - PIDs of child processes (as long as you are sure that no other part of // your program incorrectly catches or ignores SIGCHLD, and that you do it // *before* you call wait(2)or any equivalent method that could reap // zombies). func ProcPid(pid int) ProcBase { if pid < 0 || pid >= 1<<31 { panic("invalid ProcBasePid value") // TODO: should this be an error? } return ProcBase{inner: libpathrs.ProcPid(uint32(pid))} } // ThreadCloser is a callback that needs to be called when you are done // operating on an [os.File] fetched using [Handle.OpenThreadSelf]. // // [os.File]: https://pkg.go.dev/os#File type ThreadCloser func() // Handle is a wrapper around an *os.File handle to "/proc", which can be // used to do further procfs-related operations in a safe way. type Handle struct { inner *os.File } // Close releases all internal resources for this [Handle]. // // Note that if the handle is actually the global cached handle, this operation // is a no-op. func (proc *Handle) Close() error { var err error if proc.inner != nil { err = proc.inner.Close() } return err } // OpenOption is a configuration function passed as an argument to [Open]. type OpenOption func(*libpathrs.ProcfsOpenHow) error // UnmaskedProcRoot can be passed to [Open] to request an unmasked procfs // handle be created. // // procfs, err := procfs.OpenRoot(procfs.UnmaskedProcRoot) func UnmaskedProcRoot(how *libpathrs.ProcfsOpenHow) error { *how.Flags() |= libpathrs.ProcfsNewUnmasked return nil } // Open creates a new [Handle] to a safe "/proc", based on the passed // configuration options (in the form of a series of [OpenOption]s). func Open(opts ...OpenOption) (*Handle, error) { var how libpathrs.ProcfsOpenHow for _, opt := range opts { if err := opt(&how); err != nil { return nil, err } } fd, err := libpathrs.ProcfsOpen(&how) if err != nil { return nil, err } var procFile *os.File if int(fd) >= 0 { procFile = os.NewFile(fd, "/proc") } // TODO: Check that fd == PATHRS_PROC_DEFAULT_ROOTFD in the <0 case? return &Handle{inner: procFile}, nil } // TODO: Switch to something fdutils.WithFileFd-like. func (proc *Handle) fd() int { if proc.inner != nil { return int(proc.inner.Fd()) } return libpathrs.ProcDefaultRootFd } // TODO: Should we expose open? func (proc *Handle) open(base ProcBase, path string, flags int) (_ *os.File, Closer ThreadCloser, Err error) { var closer ThreadCloser if base == ProcThreadSelf { runtime.LockOSThread() closer = runtime.UnlockOSThread } defer func() { if closer != nil && Err != nil { closer() Closer = nil } }() fd, err := libpathrs.ProcOpenat(proc.fd(), base.inner, path, flags) if err != nil { return nil, nil, err } file, err := fdutils.MkFile(fd) return file, closer, err } // OpenRoot safely opens a given path from inside /proc/. // // This function must only be used for accessing global information from procfs // (such as /proc/cpuinfo) or information about other processes (such as // /proc/1). Accessing your own process information should be done using // [Handle.OpenSelf] or [Handle.OpenThreadSelf]. func (proc *Handle) OpenRoot(path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcRoot, path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcRoot)") } return file, err } // OpenSelf safely opens a given path from inside /proc/self/. // // This method is recommend for getting process information about the current // process for almost all Go processes *except* for cases where there are // [runtime.LockOSThread] threads that have changed some aspect of their state // (such as through unshare(CLONE_FS) or changing namespaces). // // For such non-heterogeneous processes, /proc/self may reference to a task // that has different state from the current goroutine and so it may be // preferable to use [Handle.OpenThreadSelf]. The same is true if a user // really wants to inspect the current OS thread's information (such as // /proc/thread-self/stack or /proc/thread-self/status which is always uniquely // per-thread). // // Unlike [Handle.OpenThreadSelf], this method does not involve locking // the goroutine to the current OS thread and so is simpler to use and // theoretically has slightly less overhead. // // [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread func (proc *Handle) OpenSelf(path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcSelf, path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcSelf)") } return file, err } // OpenPid safely opens a given path from inside /proc/$pid/, where pid can be // either a PID or TID. // // This is effectively equivalent to calling [Handle.OpenRoot] with the // pid prefixed to the subpath. // // Be aware that due to PID recycling, using this is generally not safe except // in certain circumstances. See the documentation of [ProcPid] for more // details. func (proc *Handle) OpenPid(pid int, path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcPid(pid), path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcPidOpen)") } return file, err } // OpenThreadSelf safely opens a given path from inside /proc/thread-self/. // // Most Go processes have heterogeneous threads (all threads have most of the // same kernel state such as CLONE_FS) and so [Handle.OpenSelf] is // preferable for most users. // // For non-heterogeneous threads, or users that actually want thread-specific // information (such as /proc/thread-self/stack or /proc/thread-self/status), // this method is necessary. // // Because Go can change the running OS thread of your goroutine without notice // (and then subsequently kill the old thread), this method will lock the // current goroutine to the OS thread (with [runtime.LockOSThread]) and the // caller is responsible for unlocking the the OS thread with the // [ThreadCloser] callback once they are done using the returned file. This // callback MUST be called AFTER you have finished using the returned // [os.File]. This callback is completely separate to [os.File.Close], so it // must be called regardless of how you close the handle. // // [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread // [os.File]: https://pkg.go.dev/os#File // [os.File.Close]: https://pkg.go.dev/os#File.Close func (proc *Handle) OpenThreadSelf(path string, flags int) (*os.File, ThreadCloser, error) { return proc.open(ProcThreadSelf, path, flags) } // Readlink safely reads the contents of a symlink from the given procfs base. // // This is effectively equivalent to doing an Open*(O_PATH|O_NOFOLLOW) of the // path and then doing unix.Readlinkat(fd, ""), but with the benefit that // thread locking is not necessary for [ProcThreadSelf]. func (proc *Handle) Readlink(base ProcBase, path string) (string, error) { return libpathrs.ProcReadlinkat(proc.fd(), base.inner, path) } pathrs-0.2.1/contrib/bindings/go/root_linux.go000064400000000000000000000276731046102023000175370ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package pathrs import ( "errors" "fmt" "os" "syscall" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // Root is a handle to the root of a directory tree to resolve within. The only // purpose of this "root handle" is to perform operations within the directory // tree, or to get a [Handle] to inodes within the directory tree. // // At time of writing, it is considered a *VERY BAD IDEA* to open a [Root] // inside a possibly-attacker-controlled directory tree. While we do have // protections that should defend against it, it's far more dangerous than just // opening a directory tree which is not inside a potentially-untrusted // directory. type Root struct { inner *os.File } // OpenRoot creates a new [Root] handle to the directory at the given path. func OpenRoot(path string) (*Root, error) { fd, err := libpathrs.OpenRoot(path) if err != nil { return nil, err } file, err := fdutils.MkFile(fd) if err != nil { return nil, err } return &Root{inner: file}, nil } // RootFromFile creates a new [Root] handle from an [os.File] referencing a // directory. The provided file will be duplicated, so the original file should // still be closed by the caller. // // This is effectively the inverse operation of [Root.IntoFile]. // // [os.File]: https://pkg.go.dev/os#File func RootFromFile(file *os.File) (*Root, error) { newFile, err := fdutils.DupFile(file) if err != nil { return nil, fmt.Errorf("duplicate root fd: %w", err) } return &Root{inner: newFile}, nil } // Resolve resolves the given path within the [Root]'s directory tree, and // returns a [Handle] to the resolved path. The path must already exist, // otherwise an error will occur. // // All symlinks (including trailing symlinks) are followed, but they are // resolved within the rootfs. If you wish to open a handle to the symlink // itself, use [ResolveNoFollow]. func (r *Root) Resolve(path string) (*Handle, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootResolve(rootFd, path) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, nil }) } // ResolveNoFollow is effectively an O_NOFOLLOW version of [Resolve]. Their // behaviour is identical, except that *trailing* symlinks will not be // followed. If the final component is a trailing symlink, an O_PATH|O_NOFOLLOW // handle to the symlink itself is returned. func (r *Root) ResolveNoFollow(path string) (*Handle, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootResolveNoFollow(rootFd, path) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, nil }) } // Open is effectively shorthand for [Resolve] followed by [Handle.Open], but // can be slightly more efficient (it reduces CGo overhead and the number of // syscalls used when using the openat2-based resolver) and is arguably more // ergonomic to use. // // This is effectively equivalent to [os.Open]. // // [os.Open]: https://pkg.go.dev/os#Open func (r *Root) Open(path string) (*os.File, error) { return r.OpenFile(path, os.O_RDONLY) } // OpenFile is effectively shorthand for [Resolve] followed by // [Handle.OpenFile], but can be slightly more efficient (it reduces CGo // overhead and the number of syscalls used when using the openat2-based // resolver) and is arguably more ergonomic to use. // // However, if flags contains os.O_NOFOLLOW and the path is a symlink, then // OpenFile's behaviour will match that of openat2. In most cases an error will // be returned, but if os.O_PATH is provided along with os.O_NOFOLLOW then a // file equivalent to [ResolveNoFollow] will be returned instead. // // This is effectively equivalent to [os.OpenFile], except that os.O_CREAT is // not supported. // // [os.OpenFile]: https://pkg.go.dev/os#OpenFile func (r *Root) OpenFile(path string, flags int) (*os.File, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) { fd, err := libpathrs.InRootOpen(rootFd, path, flags) if err != nil { return nil, err } return fdutils.MkFile(fd) }) } // Create creates a file within the [Root]'s directory tree at the given path, // and returns a handle to the file. The provided mode is used for the new file // (the process's umask applies). // // Unlike [os.Create], if the file already exists an error is created rather // than the file being opened and truncated. // // [os.Create]: https://pkg.go.dev/os#Create func (r *Root) Create(path string, flags int, mode os.FileMode) (*os.File, error) { unixMode, err := toUnixMode(mode, false) if err != nil { return nil, err } return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) { handleFd, err := libpathrs.InRootCreat(rootFd, path, flags, unixMode) if err != nil { return nil, err } return fdutils.MkFile(handleFd) }) } // Rename two paths within a [Root]'s directory tree. The flags argument is // identical to the RENAME_* flags to the renameat2(2) system call. func (r *Root) Rename(src, dst string, flags uint) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRename(rootFd, src, dst, flags) return struct{}{}, err }) return err } // RemoveDir removes the named empty directory within a [Root]'s directory // tree. func (r *Root) RemoveDir(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRmdir(rootFd, path) return struct{}{}, err }) return err } // RemoveFile removes the named file within a [Root]'s directory tree. func (r *Root) RemoveFile(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootUnlink(rootFd, path) return struct{}{}, err }) return err } // Remove removes the named file or (empty) directory within a [Root]'s // directory tree. // // This is effectively equivalent to [os.Remove]. // // [os.Remove]: https://pkg.go.dev/os#Remove func (r *Root) Remove(path string) error { // In order to match os.Remove's implementation we need to also do both // syscalls unconditionally and adjust the error based on whether // pathrs_inroot_rmdir() returned ENOTDIR. unlinkErr := r.RemoveFile(path) if unlinkErr == nil { return nil } rmdirErr := r.RemoveDir(path) if rmdirErr == nil { return nil } // Both failed, adjust the error in the same way that os.Remove does. err := rmdirErr if errors.Is(err, syscall.ENOTDIR) { err = unlinkErr } return err } // RemoveAll recursively deletes a path and all of its children. // // This is effectively equivalent to [os.RemoveAll]. // // [os.RemoveAll]: https://pkg.go.dev/os#RemoveAll func (r *Root) RemoveAll(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRemoveAll(rootFd, path) return struct{}{}, err }) return err } // Mkdir creates a directory within a [Root]'s directory tree. The provided // mode is used for the new directory (the process's umask applies). // // This is effectively equivalent to [os.Mkdir]. // // [os.Mkdir]: https://pkg.go.dev/os#Mkdir func (r *Root) Mkdir(path string, mode os.FileMode) error { unixMode, err := toUnixMode(mode, false) if err != nil { return err } _, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootMkdir(rootFd, path, unixMode) return struct{}{}, err }) return err } // MkdirAll creates a directory (and any parent path components if they don't // exist) within a [Root]'s directory tree. The provided mode is used for any // directories created by this function (the process's umask applies). // // This is effectively equivalent to [os.MkdirAll]. // // [os.MkdirAll]: https://pkg.go.dev/os#MkdirAll func (r *Root) MkdirAll(path string, mode os.FileMode) (*Handle, error) { unixMode, err := toUnixMode(mode, false) if err != nil { return nil, err } return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootMkdirAll(rootFd, path, unixMode) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, err }) } // Mknod creates a new device inode of the given type within a [Root]'s // directory tree. The provided mode is used for the new directory (the // process's umask applies). // // This is effectively equivalent to [unix.Mknod]. // // [unix.Mknod]: https://pkg.go.dev/golang.org/x/sys/unix#Mknod func (r *Root) Mknod(path string, mode os.FileMode, dev uint64) error { unixMode, err := toUnixMode(mode, true) if err != nil { return err } _, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootMknod(rootFd, path, unixMode, dev) return struct{}{}, err }) return err } // Symlink creates a symlink within a [Root]'s directory tree. The symlink is // created at path and is a link to target. // // This is effectively equivalent to [os.Symlink]. // // [os.Symlink]: https://pkg.go.dev/os#Symlink func (r *Root) Symlink(path, target string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootSymlink(rootFd, path, target) return struct{}{}, err }) return err } // Hardlink creates a hardlink within a [Root]'s directory tree. The hardlink // is created at path and is a link to target. Both paths are within the // [Root]'s directory tree (you cannot hardlink to a different [Root] or the // host). // // This is effectively equivalent to [os.Link]. // // [os.Link]: https://pkg.go.dev/os#Link func (r *Root) Hardlink(path, target string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootHardlink(rootFd, path, target) return struct{}{}, err }) return err } // Readlink returns the target of a symlink with a [Root]'s directory tree. // // This is effectively equivalent to [os.Readlink]. // // [os.Readlink]: https://pkg.go.dev/os#Readlink func (r *Root) Readlink(path string) (string, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (string, error) { return libpathrs.InRootReadlink(rootFd, path) }) } // IntoFile unwraps the [Root] into its underlying [os.File]. // // It is critical that you do not operate on this file descriptor yourself, // because the security properties of libpathrs depend on users doing all // relevant filesystem operations through libpathrs. // // This operation returns the internal [os.File] of the [Root] directly, so // calling [Root.Close] will also close any copies of the returned [os.File]. // If you want to get an independent copy, use [Root.Clone] followed by // [Root.IntoFile] on the cloned [Root]. // // [os.File]: https://pkg.go.dev/os#File func (r *Root) IntoFile() *os.File { // TODO: Figure out if we really don't want to make a copy. // TODO: We almost certainly want to clear r.inner here, but we can't do // that easily atomically (we could use atomic.Value but that'll make // things quite a bit uglier). return r.inner } // Clone creates a copy of a [Root] handle, such that it has a separate // lifetime to the original (while referring to the same underlying directory). func (r *Root) Clone() (*Root, error) { return RootFromFile(r.inner) } // Close frees all of the resources used by the [Root] handle. func (r *Root) Close() error { return r.inner.Close() } pathrs-0.2.1/contrib/bindings/go/utils_linux.go000064400000000000000000000025141046102023000176770ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package pathrs import ( "fmt" "os" "golang.org/x/sys/unix" ) //nolint:cyclop // this function needs to handle a lot of cases func toUnixMode(mode os.FileMode, needsType bool) (uint32, error) { sysMode := uint32(mode.Perm()) switch mode & os.ModeType { //nolint:exhaustive // we only care about ModeType bits case 0: if needsType { sysMode |= unix.S_IFREG } case os.ModeDir: sysMode |= unix.S_IFDIR case os.ModeSymlink: sysMode |= unix.S_IFLNK case os.ModeCharDevice | os.ModeDevice: sysMode |= unix.S_IFCHR case os.ModeDevice: sysMode |= unix.S_IFBLK case os.ModeNamedPipe: sysMode |= unix.S_IFIFO case os.ModeSocket: sysMode |= unix.S_IFSOCK default: return 0, fmt.Errorf("invalid mode filetype %+o", mode) } if mode&os.ModeSetuid != 0 { sysMode |= unix.S_ISUID } if mode&os.ModeSetgid != 0 { sysMode |= unix.S_ISGID } if mode&os.ModeSticky != 0 { sysMode |= unix.S_ISVTX } return sysMode, nil } pathrs-0.2.1/contrib/bindings/python/.gitignore000064400000000000000000000000341046102023000176700ustar 00000000000000/build/ /dist/ /*.egg-info/ pathrs-0.2.1/contrib/bindings/python/COPYING000064400000000000000000000405261046102023000167450ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. pathrs-0.2.1/contrib/bindings/python/Makefile000064400000000000000000000013411046102023000173420ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. PYTHON ?= python3 PIP ?= pip3 SRC_FILES := $(wildcard *.py pathrs/*.py) dist: $(SRC_FILES) pyproject.toml $(PYTHON) -m build .PHONY: clean clean: rm -rf dist/ pathrs.*-info/ pathrs/__pycache__/ pathrs/_libpathrs_cffi.* .PHONY: lint lint: ruff format --check --diff . ruff check . mypy . .PHONY: install install: dist $(PIP) install dist/pathrs*.whl pathrs-0.2.1/contrib/bindings/python/README.md000064400000000000000000000054771046102023000171770ustar 00000000000000## python-pathrs ## This is a basic Python wrapper around [libpathrs][libpathrs], a safe path resolution library for Linux. For more details about the security protections provided by [libpathrs][libpathrs], [see the main README][libpathrs-readme]. In order to use this library, you need to have `libpathrs.so` installed on your system. Your distribution might already have a libpathrs package. If not, you can [install libpathrs from source][libpathrs]. ### Examples ### libpathrs allows you to operate on a container root filesystem safely, without worrying about an attacker swapping components and tricking you into operating on host files. ```python import pathrs # Get a handle to the root filesystem. with pathrs.Root("/path/to/rootfs") as root: # Get an O_PATH handle to a path we want to operate on. with root.resolve("/etc/passwd") as passwd: # Upgrade the handle to one you can do regular IO on. with root.reopen("r") as f: for line in f: print(line.rstrip("\n")) ``` Aside from just opening files, libpathrs also allows you to do most common filesystem operations: ```python import pathrs # RENAME_EXCHANGE = 0x2 with pathrs.Root("/path/to/rootfs") as root: # symlink root.symlink("foo", "bar") # foo -> bar # link root.hardlink("a", "b") # a -> b # rename(at2) root.rename("foo", "b", flags=RENAME_EXCHANGE) # foo <-> b # open(O_CREAT) with root.creat("newfile", "w+") as f: f.write("Some contents.") ``` It also supports operations like `mkdir -p` and `rm -f`, which are a little tricky to implement safely. ```python import pathrs with pathrs.Root("/path/to/rootfs") as root: # rm -r root.remove_all("/tmp/foo") # mkdir -p root.mkdir_all("/tmp/foo/bar/baz/bing/boop", 0o755) ``` In addition, libpathrs provides a safe `procfs` API, to allow for privileged programs to operate on `/proc` in a way that detects a maliciously-configured mount table. This is a somewhat esoteric requirement, but privileged processes that have to operate in untrusted mount namespaces need to handle this properly or risk serious security issues. ```python from pathrs import procfs # readlink("/proc/thread-self/fd/0") stdin_path = procfs.readlink(procfs.PROC_THREAD_SELF, "fd/0") # readlink("/proc/self/exe") exe_path = procfs.readlink(procfs.PROC_SELF, "exe") # Read data from /proc/cpuinfo. with procfs.open(procfs.PROC_ROOT, "cpuinfo", "r") as cpuinfo: for line in cpuinfo: print(line.rstrip("\n")) ``` For more information about the libpathrs API and considerations you should have when using libpathrs, please see [the Rust documentation][libpathrs-rustdoc]. [libpathrs]: https://github.com/cyphar/libpathrs [libpathrs-readme]: https://github.com/cyphar/libpathrs/blob/main/README.md [libpathrs-rustdoc]: https://docs.rs/pathrs pathrs-0.2.1/contrib/bindings/python/mypy.ini000064400000000000000000000000251046102023000173770ustar 00000000000000[mypy] strict = true pathrs-0.2.1/contrib/bindings/python/pathrs/.gitignore000064400000000000000000000000411046102023000211670ustar 00000000000000/__pycache__/ /_libpathrs_cffi.* pathrs-0.2.1/contrib/bindings/python/pathrs/__init__.py000064400000000000000000000023401046102023000213140ustar 00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. import importlib import importlib.metadata from . import _pathrs from ._pathrs import * # noqa: F403 # We just re-export everything. # In order get pydoc to include the documentation for the re-exported code from # _pathrs, we need to include all of the members in __all__. Rather than # duplicating the member list here explicitly, just re-export __all__. __all__ = [] __all__ += _pathrs.__all__ # pyright doesn't support "=" here. try: # In order to avoid drift between this version and the dist-info/ version # information, just fill __version__ with the dist-info/ information. __version__ = importlib.metadata.version("pathrs") except importlib.metadata.PackageNotFoundError: # We're being run from a local directory without an installed version of # pathrs, so just fill in a dummy version. __version__ = "" pathrs-0.2.1/contrib/bindings/python/pathrs/_internal.py000064400000000000000000000243371046102023000215420ustar 00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. import io import os import sys import copy import errno import fcntl import typing from types import TracebackType from typing import Any, Dict, IO, Optional, TextIO, Type, TypeVar, Union # TODO: Remove this once we only support Python >= 3.11. from typing_extensions import Self, TypeAlias from ._libpathrs_cffi import lib as libpathrs_so if typing.TYPE_CHECKING: # mypy apparently cannot handle the "ffi: cffi.api.FFI" definition in # _libpathrs_cffi/__init__.pyi so we need to explicitly reference the type # from cffi here. import cffi ffi = cffi.FFI() CString: TypeAlias = cffi.FFI.CData CBuffer: TypeAlias = cffi.FFI.CData else: from ._libpathrs_cffi import ffi CString: TypeAlias = ffi.CData CBuffer: TypeAlias = ffi.CData def _cstr(pystr: str) -> CString: return ffi.new("char[]", pystr.encode("utf8")) def _pystr(cstr: CString) -> str: s = ffi.string(cstr) assert isinstance(s, bytes) # typing return s.decode("utf8") def _cbuffer(size: int) -> CBuffer: return ffi.new("char[%d]" % (size,)) def _is_pathrs_err(ret: int) -> bool: return ret < libpathrs_so.__PATHRS_MAX_ERR_VALUE class PathrsError(Exception): """ Represents a libpathrs error. All libpathrs errors have a description (PathrsError.message) and errors that were caused by an underlying OS error (or can be translated to an OS error) also include the errno value (PathrsError.errno). """ message: str errno: Optional[int] strerror: Optional[str] def __init__(self, message: str, /, *, errno: Optional[int] = None): # Construct Exception. super().__init__(message) # Basic arguments. self.message = message self.errno = errno # Pre-format the errno. self.strerror = None if errno is not None: try: self.strerror = os.strerror(errno) except ValueError: self.strerror = str(errno) @classmethod def _fetch(cls, err_id: int, /) -> Optional[Self]: if err_id >= 0: return None err = libpathrs_so.pathrs_errorinfo(err_id) if err == ffi.NULL: # type: ignore # TODO: Make this check nicer... return None description = _pystr(err.description) errno = err.saved_errno or None # TODO: Should we use ffi.gc()? mypy doesn't seem to like our types... libpathrs_so.pathrs_errorinfo_free(err) del err return cls(description, errno=errno) def __str__(self) -> str: if self.errno is None: return self.message else: return "%s (%s)" % (self.message, self.strerror) def __repr__(self) -> str: return "Error(%r, errno=%r)" % (self.message, self.errno) def pprint(self, out: TextIO = sys.stdout) -> None: "Pretty-print the error to the given @out file." # Basic error information. if self.errno is None: print("pathrs error:", file=out) else: print("pathrs error [%s]:" % (self.strerror,), file=out) print(" %s" % (self.message,), file=out) INTERNAL_ERROR = PathrsError("tried to fetch libpathrs error but no error found") class FilenoFile(typing.Protocol): def fileno(self) -> int: ... FileLike = Union[FilenoFile, int] def _fileno(file: FileLike) -> int: if isinstance(file, int): # file is a plain fd return file else: # Assume there is a fileno method. return file.fileno() def _clonefile(file: FileLike) -> int: return fcntl.fcntl(_fileno(file), fcntl.F_DUPFD_CLOEXEC) # TODO: Switch to def foo[T](...): ... syntax with Python >= 3.12. Fd = TypeVar("Fd", bound="WrappedFd") class WrappedFd(object): """ Represents a file descriptor that allows for manual lifetime management, unlike os.fdopen() which are tracked by the GC with no way of "leaking" the file descriptor for FFI purposes. pathrs will return WrappedFds for most operations that return an fd. """ _fd: Optional[int] def __init__(self, file: FileLike, /): """ Construct a WrappedFd from any file-like object. For most cases, the WrappedFd will take ownership of the lifetime of the file handle. This means you should So a raw file descriptor must only be turned into a WrappedFd *once* (unless you make sure to use WrappedFd.leak() to ensure there is only ever one owner of the handle at a given time). However, for os.fdopen() (or similar Pythonic file objects that are tracked by the GC), we have to create a clone and so the WrappedFd is a copy. """ # TODO: Maybe we should always clone to make these semantics less # confusing...? fd = _fileno(file) if isinstance(file, io.IOBase): # If this is a regular open file, we need to make a copy because # you cannot leak files and so the GC might close it from # underneath us. fd = _clonefile(fd) self._fd = fd def fileno(self) -> int: """ Return the file descriptor number of this WrappedFd. Note that the file can still be garbage collected by Python after this call, so the file descriptor number might become invalid (or worse, be reused for an unrelated file). If you want to convert a WrappedFd to a file descriptor number and stop the GC from the closing the file, use WrappedFd.into_raw_fd(). """ if self._fd is None: raise OSError(errno.EBADF, "Closed file descriptor") return self._fd def leak(self) -> None: """ Clears this WrappedFd without closing the underlying file, to stop GC from closing the file. Note that after this operation, all operations on this WrappedFd will return an error. If you want to get the underlying file handle and then leak the WrappedFd, just use WrappedFd.into_raw_fd() which does both for you. """ if self._fd is not None and self._fd >= 0: self._fd = None def fdopen(self, mode: str = "r") -> IO[Any]: """ Convert this WrappedFd into an os.fileopen() handle. This operation implicitly calls WrappedFd.leak(), so the WrappedFd will no longer be useful and you should instead use the returned os.fdopen() handle. """ fd = self.fileno() try: file = os.fdopen(fd, mode) self.leak() return file except: # "Unleak" the file if there was an error. self._fd = fd raise @classmethod def from_raw_fd(cls: Type[Fd], fd: int, /) -> Fd: "Shorthand for WrappedFd(fd)." return cls(fd) @classmethod def from_file(cls: Type[Fd], file: FileLike, /) -> Fd: "Shorthand for WrappedFd(file)." return cls(file) def into_raw_fd(self) -> int: """ Convert this WrappedFd into a raw file descriptor that GC won't touch. This is just shorthand for WrappedFd.fileno() to get the fileno, followed by WrappedFd.leak(). """ fd = self.fileno() self.leak() return fd def isclosed(self) -> bool: """ Returns whether the underlying file descriptor is closed or the WrappedFd has been leaked. """ return self._fd is None def close(self) -> None: """ Manually close the underlying file descriptor for this WrappedFd. WrappedFds are garbage collected, so this is usually unnecessary unless you really care about the point where a file is closed. """ if not self.isclosed(): assert self._fd is not None # typing if self._fd >= 0: os.close(self._fd) self._fd = None def clone(self) -> Self: "Create a clone of this WrappedFd that has a separate lifetime." if self.isclosed(): raise ValueError("cannot clone closed file") assert self._fd is not None # typing newfd = self._fd if self._fd >= 0: newfd = _clonefile(self._fd) return self.__class__.from_raw_fd(newfd) def __copy__(self) -> Self: "Identical to WrappedFd.clone()" # A "shallow copy" of a file is the same as a deep copy. return copy.deepcopy(self) def __deepcopy__(self, memo: Dict[int, Any]) -> Self: "Identical to WrappedFd.clone()" return self.clone() def __del__(self) -> None: "Identical to WrappedFd.close()" self.close() def __enter__(self) -> Self: return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], exc_traceback: Optional[TracebackType], ) -> None: self.close() # XXX: This is _super_ ugly but so is the one in CPython. def _convert_mode(mode: str) -> int: mode_set = set(mode) flags = os.O_CLOEXEC # We don't support O_CREAT or O_EXCL with libpathrs -- use creat(). if "x" in mode_set: raise ValueError("pathrs doesn't support mode='x', use Root.creat()") # Basic sanity-check to make sure we don't accept garbage modes. if len(mode_set & {"r", "w", "a"}) > 1: raise ValueError("must have exactly one of read/write/append mode") read = False write = False if "+" in mode_set: read = True write = True if "r" in mode_set: read = True if "w" in mode_set: write = True flags |= os.O_TRUNC if "a" in mode_set: write = True flags |= os.O_APPEND if read and write: flags |= os.O_RDWR elif write: flags |= os.O_WRONLY else: flags |= os.O_RDONLY # We don't care about "b" or "t" since that's just a Python thing. return flags pathrs-0.2.1/contrib/bindings/python/pathrs/_libpathrs_cffi/__init__.pyi000064400000000000000000000006241046102023000246060ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. import cffi ffi: cffi.FFI pathrs-0.2.1/contrib/bindings/python/pathrs/_libpathrs_cffi/lib.pyi000064400000000000000000000071251046102023000236200ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. from typing import type_check_only, Union # TODO: Remove this once we only support Python >= 3.10. from typing_extensions import TypeAlias, Literal from .._pathrs import CBuffer, CString from ..procfs import ProcfsBase RawFd: TypeAlias = int # pathrs_errorinfo_t * @type_check_only class CError: saved_errno: int description: CString ErrorId: TypeAlias = int __PATHRS_MAX_ERR_VALUE: ErrorId # TODO: We actually return Union[CError, cffi.FFI.NULL] but we can't express # this using the typing stubs for CFFI... def pathrs_errorinfo(err_id: Union[ErrorId, int]) -> CError: ... def pathrs_errorinfo_free(err: CError) -> None: ... # uint64_t ProcfsOpenFlags: TypeAlias = int PATHRS_PROCFS_NEW_UNMASKED: ProcfsOpenFlags # pathrs_procfs_open_how * @type_check_only class ProcfsOpenHow: flags: ProcfsOpenFlags PATHRS_PROC_ROOT: ProcfsBase PATHRS_PROC_SELF: ProcfsBase PATHRS_PROC_THREAD_SELF: ProcfsBase __PATHRS_PROC_TYPE_MASK: ProcfsBase __PATHRS_PROC_TYPE_PID: ProcfsBase PATHRS_PROC_DEFAULT_ROOTFD: RawFd # procfs API def pathrs_procfs_open(how: ProcfsOpenHow, size: int) -> Union[RawFd, ErrorId]: ... def pathrs_proc_open( base: ProcfsBase, path: CString, flags: int ) -> Union[RawFd, ErrorId]: ... def pathrs_proc_openat( proc_root_fd: RawFd, base: ProcfsBase, path: CString, flags: int ) -> Union[RawFd, ErrorId]: ... def pathrs_proc_readlink( base: ProcfsBase, path: CString, linkbuf: CBuffer, linkbuf_size: int ) -> Union[int, ErrorId]: ... def pathrs_proc_readlinkat( proc_root_fd: RawFd, base: ProcfsBase, path: CString, linkbuf: CBuffer, linkbuf_size: int, ) -> Union[int, ErrorId]: ... # core API def pathrs_open_root(path: CString) -> Union[RawFd, ErrorId]: ... def pathrs_reopen(fd: RawFd, flags: int) -> Union[RawFd, ErrorId]: ... def pathrs_inroot_resolve(rootfd: RawFd, path: CString) -> Union[RawFd, ErrorId]: ... def pathrs_inroot_resolve_nofollow( rootfd: RawFd, path: CString ) -> Union[RawFd, ErrorId]: ... def pathrs_inroot_open( rootfd: RawFd, path: CString, flags: int ) -> Union[RawFd, ErrorId]: ... def pathrs_inroot_creat( rootfd: RawFd, path: CString, flags: int, filemode: int ) -> Union[RawFd, ErrorId]: ... def pathrs_inroot_rename( rootfd: RawFd, src: CString, dst: CString, flags: int ) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_rmdir(rootfd: RawFd, path: CString) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_unlink( rootfd: RawFd, path: CString ) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_remove_all(rootfd: RawFd, path: CString) -> Union[RawFd, ErrorId]: ... def pathrs_inroot_mkdir( rootfd: RawFd, path: CString, mode: int ) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_mkdir_all( rootfd: RawFd, path: CString, mode: int ) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_mknod( rootfd: RawFd, path: CString, mode: int, dev: int ) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_hardlink( rootfd: RawFd, path: CString, target: CString ) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_symlink( rootfd: RawFd, path: CString, target: CString ) -> Union[Literal[0], ErrorId]: ... def pathrs_inroot_readlink( rootfd: RawFd, path: CString, linkbuf: CBuffer, linkbuf_size: int ) -> Union[int, ErrorId]: ... pathrs-0.2.1/contrib/bindings/python/pathrs/_pathrs.py000064400000000000000000000354301046102023000212230ustar 00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. import os import typing from typing import Any, IO, Union # TODO: Remove this once we only support Python >= 3.11. from typing_extensions import TypeAlias from ._internal import ( # File type helpers. FileLike, WrappedFd, _convert_mode, # Error API. PathrsError, _is_pathrs_err, INTERNAL_ERROR, # CFFI helpers. _cstr, _cbuffer, ) from ._libpathrs_cffi import lib as libpathrs_so if typing.TYPE_CHECKING: # mypy apparently cannot handle the "ffi: cffi.api.FFI" definition in # _libpathrs_cffi/__init__.pyi so we need to explicitly reference the type # from cffi here. import cffi ffi = cffi.FFI() CString: TypeAlias = cffi.FFI.CData CBuffer: TypeAlias = cffi.FFI.CData else: from ._libpathrs_cffi import ffi CString: TypeAlias = ffi.CData CBuffer: TypeAlias = ffi.CData __all__ = [ # Core api. "Root", "Handle", # Error api (re-export). "PathrsError", ] class Handle(WrappedFd): "A handle to a filesystem object, usually resolved using Root.resolve()." def reopen(self, mode: str = "r", /, *, extra_flags: int = 0) -> IO[Any]: """ Upgrade a Handle to a os.fdopen() file handle. mode is a Python mode string, and extra_flags can be used to indicate extra O_* flags you wish to pass to the reopen operation. The returned file handle is independent to the original Handle, and you can freely call Handle.reopen() on the same Handle multiple times. """ flags = _convert_mode(mode) | extra_flags with self.reopen_raw(flags) as file: return file.fdopen(mode) def reopen_raw(self, flags: int, /) -> WrappedFd: """ Upgrade a Handle to a WrappedFd file handle. flags is the set of O_* flags you wish to pass to the open operation. The returned file handle is independent to the original Handle, and you can freely call Handle.reopen() on the same Handle multiple times. """ fd = libpathrs_so.pathrs_reopen(self.fileno(), flags) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return WrappedFd(fd) class Root(WrappedFd): """ A handle to a filesystem root, which filesystem operations are all done relative to. """ def __init__(self, file_or_path: Union[FileLike, str], /): """ Create a handle from a file-like object or a path to a directory. Note that creating a Root in an attacker-controlled directory can allow for an attacker to trick you into allowing breakouts. If file_or_path is a path string, be aware there are no protections against rename race attacks when opening the Root directory handle itself. """ if isinstance(file_or_path, str): path = _cstr(file_or_path) fd = libpathrs_so.pathrs_open_root(path) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR file: FileLike = fd else: file = file_or_path # XXX: Is this necessary? super().__init__(file) def resolve(self, path: str, /, *, follow_trailing: bool = True) -> Handle: """ Resolve the given path inside the Root and return a Handle. follow_trailing indicates what resolve should do if the final component of the path is a symlink. The default is to continue resolving it, if follow_trailing=False then a handle to the symlink itself is returned. This has some limited uses, but most users should use the default. A pathrs.Error is raised if the path doesn't exist. """ if follow_trailing: fd = libpathrs_so.pathrs_inroot_resolve(self.fileno(), _cstr(path)) else: fd = libpathrs_so.pathrs_inroot_resolve_nofollow(self.fileno(), _cstr(path)) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return Handle(fd) def open( self, path: str, mode: str = "r", /, *, follow_trailing: bool = True, extra_flags: int = 0, ) -> IO[Any]: """ Resolve and open a path inside the Root and return an os.fdopen() file handle. This is effectively shorthand for Root.resolve(path).reopen(...), but for the openat2-based resolver this is slightly more efficient if you just want to open a file and don't need to do multiple reopens of the Handle. mode is a Python mode string, and extra_flags can be used to indicate extra O_* flags you wish to pass to the open operation. follow_trailing has the same behaviour as Root.resolve(), and is equivalent to passing os.O_NOFOLLOW to extra_flags. """ flags = _convert_mode(mode) | extra_flags if not follow_trailing: flags |= os.O_NOFOLLOW with self.open_raw(path, flags) as file: return file.fdopen(mode) def open_raw(self, path: str, flags: int, /) -> WrappedFd: """ Resolve and open a path inside the Root and return a WrappedFd file handle. This is effectively shorthand for Root.resolve(path).reopen_raw(flags), but for the openat2-based resolver this is slightly more efficient if you just want to open a file and don't need to do multiple reopens of the Handle. If flags contains os.O_NOFOLLOW, then the resolution is done as if you passed follow_trailing=False to Root.resolve(). """ fd = libpathrs_so.pathrs_inroot_open(self.fileno(), _cstr(path), flags) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return WrappedFd(fd) def readlink(self, path: str, /) -> str: """ Fetch the target of a symlink at the given path in the Root. A pathrs.Error is raised if the path is not a symlink or doesn't exist. """ cpath = _cstr(path) linkbuf_size = 128 while True: linkbuf = _cbuffer(linkbuf_size) n = libpathrs_so.pathrs_inroot_readlink( self.fileno(), cpath, linkbuf, linkbuf_size ) if _is_pathrs_err(n): raise PathrsError._fetch(n) or INTERNAL_ERROR elif n <= linkbuf_size: buf = typing.cast(bytes, ffi.buffer(linkbuf, linkbuf_size)[:n]) return buf.decode("latin1") else: # The contents were truncated. Unlike readlinkat, pathrs returns # the size of the link when it checked. So use the returned size # as a basis for the reallocated size (but in order to avoid a DoS # where a magic-link is growing by a single byte each iteration, # make sure we are a fair bit larger). linkbuf_size += n def creat( self, path: str, mode: str = "r", filemode: int = 0o644, /, extra_flags: int = 0 ) -> IO[Any]: """ Atomically create-and-open a new file at the given path in the Root, a-la O_CREAT. This method returns an os.fdopen() file handle. filemode is the Unix DAC mode you wish the new file to be created with. This mode might not be the actual mode of the created file due to a variety of external factors (umask, setgid bits, POSIX ACLs). mode is a Python mode string, and extra_flags can be used to indicate extra O_* flags you wish to pass to the reopen operation. If you wish to ensure the new file was created *by you* then you may wish to add O_EXCL to extra_flags. """ flags = _convert_mode(mode) | extra_flags fd = libpathrs_so.pathrs_inroot_creat( self.fileno(), _cstr(path), flags, filemode ) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return os.fdopen(fd, mode) def creat_raw(self, path: str, flags: int, filemode: int = 0o644, /) -> WrappedFd: """ Atomically create-and-open a new file at the given path in the Root, a-la O_CREAT. This method returns a WrappedFd handle. filemode is the Unix DAC mode you wish the new file to be created with. This mode might not be the actual mode of the created file due to a variety of external factors (umask, setgid bits, POSIX ACLs). flags is the set of O_* flags you wish to pass to the open operation. If you do not intend to open a symlink, you should pass O_NOFOLLOW to flags to let libpathrs know that it can be more strict when opening the path. """ fd = libpathrs_so.pathrs_inroot_creat( self.fileno(), _cstr(path), flags, filemode ) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return WrappedFd(fd) def rename(self, src: str, dst: str, flags: int = 0, /) -> None: """ Rename a path from src to dst within the Root. flags can be any renameat2(2) flags you wish to use, which can change the behaviour of this method substantially. For instance, RENAME_EXCHANGE will turn this into an atomic swap operation. """ # TODO: Should we have a separate Root.swap() operation? err = libpathrs_so.pathrs_inroot_rename( self.fileno(), _cstr(src), _cstr(dst), flags ) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR def rmdir(self, path: str, /) -> None: """ Remove an empty directory at the given path within the Root. To remove non-empty directories recursively, you can use Root.remove_all(). """ err = libpathrs_so.pathrs_inroot_rmdir(self.fileno(), _cstr(path)) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR def unlink(self, path: str, /) -> None: """ Remove a non-directory inode at the given path within the Root. To remove empty directories, you can use Root.remove_all(). To remove files and non-empty directories recursively, you can use Root.remove_all(). """ err = libpathrs_so.pathrs_inroot_unlink(self.fileno(), _cstr(path)) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR def remove_all(self, path: str, /) -> None: """ Remove the file or directory (empty or non-empty) at the given path within the Root. """ err = libpathrs_so.pathrs_inroot_remove_all(self.fileno(), _cstr(path)) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR def mkdir(self, path: str, mode: int, /) -> None: """ Create a directory at the given path within the Root. mode is the Unix DAC mode you wish the new directory to be created with. This mode might not be the actual mode of the created file due to a variety of external factors (umask, setgid bits, POSIX ACLs). A pathrs.Error will be raised if the parent directory doesn't exist, or the path already exists. To create a directory and all of its parent directories (or just reuse an existing directory) you can use Root.mkdir_all(). """ err = libpathrs_so.pathrs_inroot_mkdir(self.fileno(), _cstr(path), mode) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR def mkdir_all(self, path: str, mode: int, /) -> Handle: """ Recursively create a directory and all of its parents at the given path within the Root (or reuse an existing directory if the path already exists). This method returns a Handle to the created directory. mode is the Unix DAC mode you wish any new directories to be created with. This mode might not be the actual mode of the created file due to a variety of external factors (umask, setgid bits, POSIX ACLs). If the full path already exists, this mode is ignored and the existing directory mode is kept. """ fd = libpathrs_so.pathrs_inroot_mkdir_all(self.fileno(), _cstr(path), mode) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return Handle(fd) def mknod(self, path: str, mode: int, device: int = 0, /) -> None: """ Create a new inode at the given path within the Root. mode both indicates the file type (it must contain a valid bit from S_IFMT to indicate what kind of file to create) and what the mode of the newly created file should have. This mode might not be the actual mode of the created file due to a variety of external factors (umask, setgid bits, POSIX ACLs). dev is the the (major, minor) device number used for the new inode if the mode contains S_IFCHR or S_IFBLK. You can construct the device number from a (major, minor) using os.makedev(). A pathrs.Error is raised if the path already exists. """ err = libpathrs_so.pathrs_inroot_mknod(self.fileno(), _cstr(path), mode, device) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR def hardlink(self, path: str, target: str, /) -> None: """ Create a hardlink between two paths inside the Root. path is the path to the *new* hardlink, and target is a path to the *existing* file. A pathrs.Error is raised if the path for the new hardlink already exists. """ err = libpathrs_so.pathrs_inroot_hardlink( self.fileno(), _cstr(path), _cstr(target) ) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR def symlink(self, path: str, target: str, /) -> None: """ Create a symlink at the given path in the Root. path is the path to the *new* symlink, and target is what the symink will point to. Note that symlinks contents are not verified on Linux, so there are no restrictions on what target you put. A pathrs.Error is raised if the path for the new symlink already exists. """ err = libpathrs_so.pathrs_inroot_symlink( self.fileno(), _cstr(path), _cstr(target) ) if _is_pathrs_err(err): raise PathrsError._fetch(err) or INTERNAL_ERROR pathrs-0.2.1/contrib/bindings/python/pathrs/pathrs_build.py000075500000000000000000000126451046102023000222510ustar 00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. # This builds the _pathrs module (only needs to be done during the initial # build of libpathrs, and can be redistributed alongside the pathrs.py wrapping # library). It's much better than the ABI-mode of CFFI. import re import os import sys from typing import Any, Optional from collections.abc import Iterable import cffi def load_hdr(ffi: cffi.FFI, hdr_path: str) -> None: with open(hdr_path) as f: hdr = f.read() # We need to first filter out all the bits of that are not # supported by cffi. Ideally this wouldn't be necessary, but the macro # support in cffi is very limited, and we make use of basic features that # are unsupported by cffi. # Drop all non-#define lines (directives are not supported). hdr = re.sub(r"^#\s*(?!define\b).*$", "", hdr, flags=re.MULTILINE) # Drop all: # * "#define FOO(n) ..." lines (function-style macros are not supported). # * Empty-value "#define FOO" lines (empty macros are not supported) # TODO: We probably should support multi-line macros. hdr = re.sub(r"^#\s*define\b\s*\w*(\(.*|)$", "", hdr, flags=re.MULTILINE) # Replace each struct-like body that has __CBINDGEN_ALIGNED before it, # remove the __CBINDGEN_ALIGNED and add "...;" as the last field in the # struct. This is how you tell cffi to get the proper alignment from the # compiler (__attribute__((aligned(n))) is not supported by cdef). hdr = re.sub( r"__CBINDGEN_ALIGNED\(\d+\)([^{;]*){([^}]+)}", r"\1 {\2 ...;}", hdr, flags=re.MULTILINE, ) # Load the header. ffi.cdef(hdr) def create_ffibuilder(**kwargs: Any) -> cffi.FFI: ffibuilder = cffi.FFI() ffibuilder.cdef("typedef uint32_t dev_t;") # We need to use cdef to tell cffi what functions we need to FFI to. But we # don't need the structs (I hope). for include_dir in kwargs.get("include_dirs", []): pathrs_hdr = os.path.join(include_dir, "pathrs.h") if os.path.exists(pathrs_hdr): load_hdr(ffibuilder, pathrs_hdr) # Add a source and link to libpathrs. ffibuilder.set_source( "_libpathrs_cffi", "#include ", libraries=["pathrs"], **kwargs ) return ffibuilder def find_rootdir() -> str: # Figure out where the libpathrs source dir is. root_dir = None candidate = os.path.dirname(sys.path[0] or os.getcwd()) while candidate != "/": try: # Look for a Cargo.toml which says it's pathrs. candidate_toml = os.path.join(candidate, "Cargo.toml") with open(candidate_toml, "r") as f: content = f.read() if re.findall(r'^name = "pathrs"$', content, re.MULTILINE): root_dir = candidate break except FileNotFoundError: pass candidate = os.path.dirname(candidate) if not root_dir: raise FileNotFoundError("Could not find pathrs source-dir root.") return root_dir def srcdir_ffibuilder(root_dir: Optional[str] = None) -> cffi.FFI: """ Build the CFFI bindings using the provided root_dir as the root of a pathrs source tree which has compiled cdylibs ready in target/*. """ if root_dir is None: root_dir = find_rootdir() # Figure out which libs are usable. library_dirs: Iterable[str] = ( os.path.join(root_dir, "target/%s/libpathrs.so" % (mode,)) for mode in ("debug", "release") ) library_dirs = (so_path for so_path in library_dirs if os.path.exists(so_path)) library_dirs = sorted(library_dirs, key=lambda path: -os.path.getmtime(path)) library_dirs = [os.path.dirname(path) for path in library_dirs] # Compile the libpathrs module. return create_ffibuilder( include_dirs=[os.path.join(root_dir, "include")], library_dirs=library_dirs, ) def system_ffibuilder() -> cffi.FFI: """ Build the CFFI bindings using the installed libpathrs system libraries. """ return create_ffibuilder( include_dirs=[ "/usr/include", "/usr/local/include", ] ) if __name__ == "__main__": try: # Search for the compiled libraries to link to from our libpathrs # source if running outside of setuptools as a regular program. ffibuilder = srcdir_ffibuilder(root_dir=find_rootdir()) except FileNotFoundError: # If we couldn't find a valid library in the source dir, just fallback # to using the system libraries. ffibuilder = system_ffibuilder() ffibuilder.compile(verbose=True) elif os.environ.get("PATHRS_SRC_ROOT", "") != "": # If we're running in setup tools, we can't easily find the source dir. # However, distributions can set PATHRS_SRC_ROOT to the path of the # libpathrs source directory to make it easier to build the python modules # in the same %build script as the main library. ffibuilder = srcdir_ffibuilder(root_dir=os.environ.get("PATHRS_SRC_ROOT")) else: # Use the system libraries if running inside standard setuptools. ffibuilder = system_ffibuilder() pathrs-0.2.1/contrib/bindings/python/pathrs/procfs.py000064400000000000000000000200561046102023000210550ustar 00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. import typing from typing import Any, IO, cast # TODO: Remove this once we only support Python >= 3.11. from typing_extensions import Self, TypeAlias from ._internal import ( # File type helpers. WrappedFd, _convert_mode, # Error API. PathrsError, _is_pathrs_err, INTERNAL_ERROR, # CFFI helpers. _cstr, _cbuffer, ) from ._libpathrs_cffi import lib as libpathrs_so if typing.TYPE_CHECKING: # mypy apparently cannot handle the "ffi: cffi.api.FFI" definition in # _libpathrs_cffi/__init__.pyi so we need to explicitly reference the type # from cffi here. import cffi ffi = cffi.FFI() CString: TypeAlias = cffi.FFI.CData CBuffer: TypeAlias = cffi.FFI.CData else: from ._libpathrs_cffi import ffi CString: TypeAlias = ffi.CData CBuffer: TypeAlias = ffi.CData __all__ = [ "PROC_ROOT", "PROC_SELF", "PROC_THREAD_SELF", "PROC_PID", "ProcfsHandle", # Shorthand for ProcfsHandle.cached().. "open", "open_raw", "readlink", ] # TODO: Switch to "type ..." syntax once we switch to Python >= 3.12...? ProcfsBase: TypeAlias = int #: Resolve proc_* operations relative to the /proc root. Note that this mode #: may be more expensive because we have to take steps to try to avoid leaking #: unmasked procfs handles, so you should use PROC_SELF if you can. PROC_ROOT: ProcfsBase = libpathrs_so.PATHRS_PROC_ROOT #: Resolve proc_* operations relative to /proc/self. For most programs, this is #: the standard choice. PROC_SELF: ProcfsBase = libpathrs_so.PATHRS_PROC_SELF #: Resolve proc_* operations relative to /proc/thread-self. In multi-threaded #: programs where one thread has a different CLONE_FS, it is possible for #: /proc/self to point the wrong thread and so /proc/thread-self may be #: necessary. PROC_THREAD_SELF: ProcfsBase = libpathrs_so.PATHRS_PROC_THREAD_SELF def PROC_PID(pid: int) -> ProcfsBase: """ Resolve proc_* operations relative to /proc/. Be aware that due to PID recycling, using this is generally not safe except in certain circumstances. Namely: * PID 1 (the init process), as that PID cannot ever get recycled. * Your current PID (though you should just use PROC_SELF). * PIDs of child processes (as long as you are sure that no other part of your program incorrectly catches or ignores SIGCHLD, and that you do it *before* you call wait(2)or any equivalent method that could reap zombies). """ if pid & libpathrs_so.__PATHRS_PROC_TYPE_MASK: raise ValueError(f"invalid PROC_PID value {pid}") return libpathrs_so.__PATHRS_PROC_TYPE_PID | pid class ProcfsHandle(WrappedFd): """ """ _PROCFS_OPEN_HOW_TYPE = "pathrs_procfs_open_how *" @classmethod def cached(cls) -> Self: """ Returns a cached version of ProcfsHandle that will always remain valid and cannot be closed. This is the recommended usage of ProcfsHandle. """ return cls.from_raw_fd(libpathrs_so.PATHRS_PROC_DEFAULT_ROOTFD) @classmethod def new(cls, /, *, unmasked: bool = False) -> Self: """ Create a new procfs handle with the requested configuration settings. Note that the requested configuration might be eligible for caching, in which case the ProcfsHandle.fileno() will contain a special sentinel value that cannot be used like a regular file descriptor. """ # TODO: Is there a way to have ProcfsOpenHow actually subclass CData so # that we don't need to do any of these ugly casts? how = cast("libpathrs_so.ProcfsOpenHow", ffi.new(cls._PROCFS_OPEN_HOW_TYPE)) how_size = ffi.sizeof(cast("Any", how)) if unmasked: how.flags = libpathrs_so.PATHRS_PROCFS_NEW_UNMASKED fd = libpathrs_so.pathrs_procfs_open(how, how_size) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return cls.from_raw_fd(fd) def open_raw(self, base: ProcfsBase, path: str, flags: int, /) -> WrappedFd: """ Open a procfs file using Unix open flags. This function returns a WrappedFd file handle. base indicates what the path should be relative to. Valid values include PROC_{ROOT,SELF,THREAD_SELF}. path is a relative path to base indicating which procfs file you wish to open. flags is the set of O_* flags you wish to pass to the open operation. If you do not intend to open a symlink, you should pass O_NOFOLLOW to flags to let libpathrs know that it can be more strict when opening the path. """ # TODO: Should we default to O_NOFOLLOW or put a separate argument for it? fd = libpathrs_so.pathrs_proc_openat(self.fileno(), base, _cstr(path), flags) if _is_pathrs_err(fd): raise PathrsError._fetch(fd) or INTERNAL_ERROR return WrappedFd(fd) def open( self, base: ProcfsBase, path: str, mode: str = "r", /, *, extra_flags: int = 0 ) -> IO[Any]: """ Open a procfs file using Pythonic mode strings. This function returns an os.fdopen() file handle. base indicates what the path should be relative to. Valid values include PROC_{ROOT,SELF,THREAD_SELF}. path is a relative path to base indicating which procfs file you wish to open. mode is a Python mode string, and extra_flags can be used to indicate extra O_* flags you wish to pass to the open operation. If you do not intend to open a symlink, you should pass O_NOFOLLOW to extra_flags to let libpathrs know that it can be more strict when opening the path. """ flags = _convert_mode(mode) | extra_flags with self.open_raw(base, path, flags) as file: return file.fdopen(mode) def readlink(self, base: ProcfsBase, path: str, /) -> str: """ Fetch the target of a procfs symlink. Note that some procfs symlinks are "magic-links" where the returned string from readlink() is not how they are actually resolved. base indicates what the path should be relative to. Valid values include PROC_{ROOT,SELF,THREAD_SELF}. path is a relative path to base indicating which procfs file you wish to open. """ # TODO: See if we can merge this with Root.readlink. cpath = _cstr(path) linkbuf_size = 128 while True: linkbuf = _cbuffer(linkbuf_size) n = libpathrs_so.pathrs_proc_readlinkat( self.fileno(), base, cpath, linkbuf, linkbuf_size ) if _is_pathrs_err(n): raise PathrsError._fetch(n) or INTERNAL_ERROR elif n <= linkbuf_size: buf = typing.cast(bytes, ffi.buffer(linkbuf, linkbuf_size)[:n]) return buf.decode("latin1") else: # The contents were truncated. Unlike readlinkat, pathrs # returns the size of the link when it checked. So use the # returned size as a basis for the reallocated size (but in # order to avoid a DoS where a magic-link is growing by a # single byte each iteration, make sure we are a fair bit # larger). linkbuf_size += n #: Open a procfs file (with unix open flags). #: Shorthand for ProcfsHandle.cached().open(...). open = ProcfsHandle.cached().open #: Open a procfs file (with Pythonic mode strings). #: Shorthand for ProcfsHandle.cached().open_raw(...). open_raw = ProcfsHandle.cached().open_raw #: Fetch the target of a procfs symlink. #: Shorthand for ProcfsHandle.cached().readlink(...). readlink = ProcfsHandle.cached().readlink pathrs-0.2.1/contrib/bindings/python/pathrs/py.typed000064400000000000000000000000001046102023000206710ustar 00000000000000pathrs-0.2.1/contrib/bindings/python/pyproject.toml000064400000000000000000000033441046102023000206230ustar 00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. [build-system] requires = [ "cffi>=1.10.0", "setuptools>=77.0.3", "toml>=0.10", # TODO: Remove this once we only support Python >= 3.11. "wheel", ] build-backend = "setuptools.build_meta" [project] name = "pathrs" # TODO: Figure out a way to keep this version up-to-date with Cargo.toml. version = "0.2.1" description = "Python bindings for libpathrs, a safe path resolution library for Linux." readme = "README.md" keywords = ["libpathrs", "pathrs"] license = "MPL-2.0" license-files = [ "COPYING" ] authors = [ {name = "Aleksa Sarai", email = "cyphar@cyphar.com"}, ] maintainers = [ {name = "Aleksa Sarai", email = "cyphar@cyphar.com"}, ] classifiers = [ "Topic :: Security", "Topic :: System :: Filesystems", "Intended Audience :: Developers", "Natural Language :: English", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Topic :: Software Development :: Libraries :: Python Modules", ] requires-python = ">= 3.9" dependencies = [ "cffi>=1.10.0", "typing_extensions>=4.0.0", # TODO: Remove this once we only support Python >= 3.11. ] [project.urls] Homepage = "https://github.com/cyphar/libpathrs" Repository = "https://github.com/cyphar/libpathrs" Documentation = "https://docs.rs/pathrs" Changelog = "https://github.com/cyphar/libpathrs/blob/main/CHANGELOG.md" pathrs-0.2.1/contrib/bindings/python/setup.py000075500000000000000000000023461046102023000174250ustar 00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. import setuptools from typing import Any, Dict # This is only needed for backwards compatibility with older versions. def parse_pyproject() -> Dict[str, Any]: try: import tomllib openmode = "rb" except ImportError: # TODO: Remove this once we only support Python >= 3.11. import toml as tomllib # type: ignore openmode = "r" with open("pyproject.toml", openmode) as f: return tomllib.load(f) pyproject = parse_pyproject() setuptools.setup( # For backwards-compatibility with pre-pyproject setuptools. name=pyproject["project"]["name"], version=pyproject["project"]["version"], install_requires=pyproject["project"]["dependencies"], # Configure cffi building. ext_package="pathrs", platforms=["Linux"], cffi_modules=["pathrs/pathrs_build.py:ffibuilder"], ) pathrs-0.2.1/e2e-tests/Makefile000064400000000000000000000016611046102023000144040ustar 00000000000000#!/bin/bash # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. ifneq ($(RUN_AS),) SUDO := sudo -u $(RUN_AS) endif BATS ?= bats ALL_TESTS := $(patsubst cmd/%/Makefile,test-%,$(wildcard cmd/*/Makefile)) .DEFAULT: test-all .PHONY: test-all test-all: $(ALL_TESTS) test-%: cmd/%/pathrs-cmd FORCE $(SUDO) PATHRS_CMD=$< $(BATS) -t tests/ cmd/%/pathrs-cmd: FORCE make -C $(@D) $(@F) # .PHONY doesn't work with patterns so we need to create a dummy rule. # See # and . FORCE: pathrs-0.2.1/e2e-tests/README.md000064400000000000000000000010531046102023000142160ustar 00000000000000## `libpathrs` End-to-End Binding Tests ## In addition to [the extensive Rust-based tests][rust-tests] we have (which test our Rust implementation as well as the C binding wrappers from Rust), we also have verification tests to ensure that our language bindings have the correct behaviour. These tests are not designed to be as extensive as our Rust tests (which test the behaviour of `libpathrs` itself), but instead are just intended to verify that the language bindings are correctly wrapping the right `libpathrs` APIs. [rust-tests]: ../src/tests/ pathrs-0.2.1/e2e-tests/cmd/.gitignore000064400000000000000000000000161046102023000154700ustar 00000000000000/*/pathrs-cmd pathrs-0.2.1/e2e-tests/cmd/README.md000064400000000000000000000215121046102023000147630ustar 00000000000000## `libpathrs` Binding-Agnostic Test Binary API ## In order to allow us to test the full functionality of the `libpathrs` bindings in a uniform way, we need have some kind of binary-agnostic API that the tests can use so we can test the same functionality for all bindings easily. This is accomplished by implementing a test binary for each binding that implements a fairly simple CLI interface that tests can be written against. The basic usage of the tool looks like this: ``` pathrs-cmd ``` The following operations are all part of the API used by our tests in [`e2e-tests/tests`](../tests). Any change in this API should result in changes in the test suite. #### Error Output #### Each section below describes the `pathrs-cmd` output in successful cases, but if an operation fails the output will be consistent: ``` ERRNO () ERROR-DESCRIPTION ``` Note that `` can have different formatting based on the language, so tests should only ever test against the numerical ``. Tests against `` are acceptable but should be used carefully as such tests could be brittle. ### `Root` Operations ### All of the following operations are subcommands of the `root` subcommand. #### `resolve` #### ``` pathrs-cmd root --root resolve [--[no-]follow] [--reopen=] ``` Calls `Root::resolve` (or `Root::resolve_nofollow`) with the given `subpath` argument. * `--no-follow` indicates to use `Root::resolve_nofollow` (i.e., do not follow trailing symlinks) rather than `Root::resolve`. The default is `--follow`. * `--reopen` indicates that we should re-open the handle with the given set of `O_*` flags. ##### Output ##### ``` HANDLE-PATH FILE-PATH ``` **TODO**: We currently do not do an I/O for the re-opened file. #### `open` #### ``` pathrs-cmd root --root open [--[no-]follow] [--oflags=] ``` Calls `Root::open_subpath` with the given `subpath` argument. * `--oflags` is the set of `O_*` flags to use when opening the subpath. * `--no-follow` is equivalent to `--oflags O_NOFOLLOW`. The default is `--follow`. ##### Output ##### ``` FILE-PATH ``` **TODO**: We currently do not do an I/O for the opened file. #### `mkfile` #### ``` pathrs-cmd root --root mkfile [--oflags=] [--mode ] ``` Calls `Root::create_file` with the given `subpath` argument. This is effectively a safe `O_CREAT|O_EXCL|O_NOFOLLOW`. * `--oflags` is the set of `O_*` flags to use when creating the file. (`O_CREAT` is not required and is actually an invalid argument.) * `--mode` is the mode of the created file (umask still applies). ##### Output ##### ``` FILE-PATH ``` **TODO**: We currently do not do an I/O for the newly-created file. #### `mkdir` #### ``` pathrs-cmd root --root mkdir [--mode ] ``` Calls `pathrs_inroot_mkdir` (`Root::create(InodeType::Directory)`) with the given `subpath` argument. * `--mode` is the mode of the created directory (umask still applies). #### `mkdir-all` #### ``` pathrs-cmd root --root mkdir-all [--mode ] ``` Calls `Root::mkdir_all` with the given `subpath` argument. * `--mode` is the mode of the created directories (umask still applies). Existing directories do not have their modes changed. ##### Output ##### ``` HANDLE-PATH ``` #### `mknod` #### ``` pathrs-cmd root --root mknod [--mode ] [ ] ``` Calls `Root::create` with various inode types with the given `subpath` argument, loosely effectively equivalent to `mknod(1)`. * `--mode` is the mode of the created inode (umask still applies). * `type` is the inode type to create, and must be one of the following values: - `f`: regular **f**ile - `d`: **d**irectory - `b`: **b**lock device (`major:minor`) - `c` (or `u`): **c**character device (`major:minor`) - `p`: named **p**ipe (aka FIFO) * `major` and `minor` only have effect for `b` and `c`/`u` but `pathrs-cmd` will pass the device value to all calls if they are specified. #### `hardlink` #### ``` pathrs-cmd root --root hardlink ``` Calls `Root::create(InodeType::Hardlink)` with the given `subpath` argument. Both `target` and `linkname` are resolved inside the root. * `target` is an existing path that will be the target of the new hardlink. * `linkname` is the path to where the new hardlink will be placed. Note that the argument order is the same as `ln(1)`! #### `symlink` #### ``` pathrs-cmd root --root symlink ``` Calls `Root::create(InodeType::Symlink)` with the given `subpath` argument. * `target` is an arbitrary string that will be the content of the symlink. * `linkname` is the path to where the new symlink will be placed. Note that the argument order is the same as `ln(1)`! #### `readlink` #### ``` pathrs-cmd root --root readlink ``` Calls `Root::readlink` with the given `subpath` argument. ##### Output ##### ``` LINK-TARGET ``` #### `rmdir` #### ``` pathrs-cmd root --root rmdir ``` Calls `Root::remove_dir` with the given `subpath` argument. (`subpath` needs to be an empty directory.) #### `unlink` #### ``` pathrs-cmd root --root unlink ``` Calls `Root::remove_file` with the given `subpath` argument. (`subpath` needs to be a non-directory.) #### `remove-all` #### ``` pathrs-cmd root --root remove-all ``` Calls `Root::remove_all` with the given `subpath` argument. #### `rename` #### ``` pathrs-cmd root --root rename [--whiteout] [--exchange] [--[no-]clobber] ``` Calls `Root::rename` with the given `source` and `destination` arguments. * `--whiteout` indicates that the `RENAME_WHITEOUT` flag should be set. * `--exchange` indicates that the `RENAME_EXCHANGE` flag should be set. * `--no-clobber` indicates the the `RENAME_NOREPLACE` flag should be set. Some of these flag combinations are not permitted by Linux, however `pathrs-cmd` allows any combination to be provided and for libpathrs to return an error if appropriate. ### `ProcfsHandle` Operations ### All of the following operations are subcommands of the `procfs` subcommand. All procfs commands take the following arguments: * `--unmasked` indicates whether to use `ProcfsHandleBuilder::unmasked()` when configuring the `ProcfsHandle`. At the moment there isn't really a way to determine that this is not a no-op in our tests, but we may expand this capability in the future (such as by looking at `mnt_id` after successive calls). * `--base` indicates what `ProcfsBase` to use, and must be one of the following values: - `root`: `ProcfsBase::ProcRoot` - `self`: `ProcfsBase::ProcSelf` - `thread-self`: `ProcfsBase::ProcThreadSelf` - `pid=$n`: `ProcfsBase::ProcPid($n)` (`$n` is an integer) The default is `--base root`. **TODO**: We should probably expose `ProcfsHandleRef::try_from_fd`. #### `open` #### ``` pathrs-cmd procfs [--unmasked] [--base ] open [--[no-]follow] [--oflags ] ` ``` Calls `ProcfsHandle::open` (or `ProcfsHandle::open_follow`) with the given `subpath` argument. * `--oflags` is the set of `O_*` flags to use when opening the subpath. * `--no-follow` is generally equivalent to `--oflags O_NOFOLLOW` but some bindings might switch between using `ProcfsHandle::open` and `ProcfsHandle::open_follow` directly based on this flag instead. In general, bindings that have a distinction should treat `--[no-]follow` as calling different APIs, while `--oflags` should only affect the set of `O_*` flags passed. (libpathrs treats both equivalently, but for test purposes we keep this distinction to make sure that libpathrs bindings also treat these equivalently.) The default is `--follow`. ##### Output ##### ``` FILE-PATH ``` **TODO**: We currently do not do an I/O for the opened file. #### `readlink` #### ``` pathrs-cmd procfs [--unmasked] [--base ] readlink ` ``` Calls `ProcfsHandle::readlink` with the given `subpath` argument. ##### Output ##### ``` LINK-TARGET ``` pathrs-0.2.1/e2e-tests/cmd/go/Makefile000064400000000000000000000006531046102023000155540ustar 00000000000000#!/bin/bash # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. pathrs-cmd: *.go go.* go build -o $@ pathrs-0.2.1/e2e-tests/cmd/go/go.mod000064400000000000000000000002641046102023000152200ustar 00000000000000module pathrs-cmd go 1.24 require ( cyphar.com/go-pathrs v0.2.0 github.com/urfave/cli/v3 v3.5.0 golang.org/x/sys v0.26.0 ) replace cyphar.com/go-pathrs => ../../../go-pathrs pathrs-0.2.1/e2e-tests/cmd/go/go.sum000064400000000000000000000017421046102023000152470ustar 00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli/v3 v3.5.0 h1:qCuFMmdayTF3zmjG8TSsoBzrDqszNrklYg2x3g4MSgw= github.com/urfave/cli/v3 v3.5.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= pathrs-0.2.1/e2e-tests/cmd/go/main.go000064400000000000000000000017071046102023000153700ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package main import ( "context" "errors" "fmt" "os" "syscall" "github.com/urfave/cli/v3" ) func main() { cmd := &cli.Command{ Name: "pathrs-cmd", Usage: "helper binary for testing libpathrs", Authors: []any{ "Aleksa Sarai ", }, Commands: []*cli.Command{ rootCmd, procfsCmd, }, } if err := cmd.Run(context.Background(), os.Args); err != nil { var errno syscall.Errno if errors.As(err, &errno) { fmt.Fprintf(os.Stderr, "ERRNO %d (%s)\n", errno, errno) } fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } } pathrs-0.2.1/e2e-tests/cmd/go/procfs.go000064400000000000000000000101221046102023000157270ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package main import ( "context" "fmt" "os" "strconv" "strings" "github.com/urfave/cli/v3" "golang.org/x/sys/unix" "cyphar.com/go-pathrs/procfs" ) type procfsOpenFunc func(path string, flags int) (*os.File, error) var procfsCmd = &cli.Command{ Name: "procfs", Usage: "ProcfsHandle::* operations", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "unmasked", Usage: "use unmasked procfs handle", }, &cli.StringFlag{ Name: "base", Usage: "base path for procfs operations (root, pid=, self, thread-self)", Value: "root", }, }, Commands: []*cli.Command{ procfsOpenCmd, procfsReadlinkCmd, }, Before: func(ctx context.Context, cmd *cli.Command) (_ context.Context, Err error) { var opts []procfs.OpenOption if cmd.Bool("unmasked") { opts = append(opts, procfs.UnmaskedProcRoot) } proc, err := procfs.Open(opts...) if err != nil { return nil, err } ctx = context.WithValue(ctx, "procfs", proc) defer func() { if Err != nil { _ = proc.Close() } }() return ctx, nil }, After: func(ctx context.Context, cmd *cli.Command) error { if proc, ok := ctx.Value("procfs").(*procfs.Handle); ok { _ = proc.Close() } return nil }, } var procfsOpenCmd = cmdWithOptions(&cli.Command{ Name: "open", Usage: "open a path in procfs", Flags: []cli.Flag{ &cli.BoolWithInverseFlag{ Name: "follow", Usage: "follow trailing symlinks", Value: true, }, }, Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { proc := ctx.Value("procfs").(*procfs.Handle) follow := cmd.Bool("follow") subpath := cmd.StringArg("subpath") oflags := unix.O_RDONLY if val := ctx.Value("oflags"); val != nil { oflags = val.(int) } if !follow { oflags |= unix.O_NOFOLLOW } var ( f *os.File err error ) baseStr := cmd.String("base") if pidStr, ok := strings.CutPrefix(baseStr, "pid="); ok { var pid int pid, err = strconv.Atoi(pidStr) if err != nil { return fmt.Errorf("failed to parse --base=%q: %w", pidStr, err) } f, err = proc.OpenPid(pid, subpath, oflags) } else { switch baseStr { case "root": f, err = proc.OpenRoot(subpath, oflags) case "self": f, err = proc.OpenSelf(subpath, oflags) case "thread-self": var closer procfs.ThreadCloser f, closer, err = proc.OpenThreadSelf(subpath, oflags) if closer != nil { defer closer() } default: return fmt.Errorf("invalid --base value %q", baseStr) } } if err != nil { return err } defer f.Close() fmt.Println("FILE-PATH", f.Name()) // TODO: Input/output file data. return nil }, }, oflags("oflags", "O_* flags to use when opening the file", unix.O_RDONLY)) var procfsReadlinkCmd = &cli.Command{ Name: "readlink", Usage: "read the target path of a symbolic link in procfs", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { proc := ctx.Value("procfs").(*procfs.Handle) subpath := cmd.StringArg("subpath") var base procfs.ProcBase baseStr := cmd.String("base") if pidStr, ok := strings.CutPrefix(baseStr, "pid="); ok { pid, err := strconv.Atoi(pidStr) if err != nil { return fmt.Errorf("failed to parse --base=%q: %w", pidStr, err) } base = procfs.ProcPid(pid) } else { switch baseStr { case "root": base = procfs.ProcRoot case "self": base = procfs.ProcSelf case "thread-self": base = procfs.ProcThreadSelf default: return fmt.Errorf("invalid --base value %q", baseStr) } } target, err := proc.Readlink(base, subpath) if err != nil { return err } fmt.Println("LINK-TARGET", target) return nil }, } pathrs-0.2.1/e2e-tests/cmd/go/root.go000064400000000000000000000234621046102023000154310ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package main import ( "context" "errors" "fmt" "os" "github.com/urfave/cli/v3" "golang.org/x/sys/unix" "cyphar.com/go-pathrs" ) var rootCmd = &cli.Command{ Name: "root", Usage: "Root::* operations", Flags: []cli.Flag{ &cli.StringFlag{ Name: "root", Required: true, }, }, Commands: []*cli.Command{ rootResolveCmd, rootOpenCmd, rootMkfileCmd, rootMkdirCmd, rootMkdirAllCmd, rootMknodCmd, rootHardlinkCmd, rootSymlinkCmd, rootReadlinkCmd, rootUnlinkCmd, rootRmdirCmd, rootRmdirAllCmd, rootRenameCmd, }, Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { rootPath := cmd.String("root") // The "required" flag checks in urfave/cli happen after Before is run, // so we need to manually check this here. if rootPath == "" { return nil, errors.New(`Required flag "root" not set`) } root, err := pathrs.OpenRoot(rootPath) if err != nil { return nil, err } ctx = context.WithValue(ctx, "root", root) return ctx, nil }, After: func(ctx context.Context, cmd *cli.Command) error { if root, ok := ctx.Value("root").(*pathrs.Root); ok { _ = root.Close() } return nil }, } var rootResolveCmd = cmdWithOptions(&cli.Command{ Name: "resolve", Usage: "resolve a path inside the root", Flags: []cli.Flag{ &cli.BoolWithInverseFlag{ Name: "follow", Usage: "follow trailing symlinks", Value: true, }, }, Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) follow := cmd.Bool("follow") subpath := cmd.StringArg("subpath") var ( handle *pathrs.Handle err error ) if follow { handle, err = root.Resolve(subpath) } else { handle, err = root.ResolveNoFollow(subpath) } if err != nil { return err } defer handle.Close() fmt.Println("HANDLE-PATH", handle.IntoFile().Name()) if val := ctx.Value("reopen"); val != nil { oflags := val.(int) f, err := handle.OpenFile(oflags) if err != nil { return err } // TODO: Input/output file data. fmt.Println("FILE-PATH", f.Name()) } return nil }, }, oflags("reopen", "reopen the handle with these O_* flags", nil)) var rootOpenCmd = cmdWithOptions(&cli.Command{ Name: "open", Usage: "open a path inside the root", Flags: []cli.Flag{ &cli.BoolWithInverseFlag{ Name: "follow", Usage: "follow trailing symlinks", Value: true, }, }, Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) follow := cmd.Bool("follow") subpath := cmd.StringArg("subpath") oflags := unix.O_RDONLY if val := ctx.Value("oflags"); val != nil { oflags = val.(int) } if !follow { oflags |= unix.O_NOFOLLOW } var ( f *os.File err error ) if oflags == 0 /* O_RDONLY */ { f, err = root.Open(subpath) } else { f, err = root.OpenFile(subpath, oflags) } if err != nil { return err } defer f.Close() // TODO: Input/output file data. fmt.Println("FILE-PATH", f.Name()) return nil }, }, oflags("oflags", "O_* flags to use when opening the file", unix.O_RDONLY)) var rootMkfileCmd = cmdWithOptions(&cli.Command{ Name: "mkfile", Usage: "make an empty file inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") oflags := ctx.Value("oflags").(int) mode := ctx.Value("mode").(os.FileMode) f, err := root.Create(subpath, oflags, mode) if err != nil { return err } defer f.Close() // TODO: Input/output file data? fmt.Println("FILE-PATH", f.Name()) return nil }, }, oflags("oflags", "O_* flags to use when creating the file", unix.O_RDONLY), modeFlag("mode", "file mode for the created file", "0o644"), ) var rootMkdirCmd = cmdWithOptions(&cli.Command{ Name: "mkdir", Usage: "make an empty directory inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") mode := ctx.Value("mode").(os.FileMode) return root.Mkdir(subpath, mode) }, }, modeFlag("mode", "file mode for the created directory", "0o755"), ) var rootMkdirAllCmd = cmdWithOptions(&cli.Command{ Name: "mkdir-all", Usage: "make a directory (including parents) inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") mode := ctx.Value("mode").(os.FileMode) handle, err := root.MkdirAll(subpath, mode) if err != nil { return err } defer handle.Close() fmt.Println("HANDLE-PATH", handle.IntoFile().Name()) return nil }, }, modeFlag("mode", "file mode for the created directories", "0o755"), ) var rootMknodCmd = cmdWithOptions(&cli.Command{ Name: "mknod", Usage: "make an inode inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, &cli.StringArg{ Name: "type", }, &cli.Uint32Arg{ Name: "major", }, &cli.Uint32Arg{ Name: "minor", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") mode := ctx.Value("mode").(os.FileMode) inoType := cmd.StringArg("type") switch inoType { case "": return fmt.Errorf("type is a required positional argument") case "f": // no-op case "d": mode |= os.ModeDir case "b": mode |= os.ModeDevice case "c", "u": mode |= os.ModeCharDevice | os.ModeDevice case "p": mode |= os.ModeNamedPipe default: return fmt.Errorf("unknown type %s", inoType) } dev := unix.Mkdev(cmd.Uint32Arg("major"), cmd.Uint32Arg("minor")) return root.Mknod(subpath, mode, dev) }, }, modeFlag("mode", "file mode for the created inode", "0o644"), ) var rootHardlinkCmd = &cli.Command{ Name: "hardlink", Usage: "make a hardlink inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "target", }, &cli.StringArg{ Name: "linkname", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) target := cmd.StringArg("target") linkname := cmd.StringArg("linkname") // TODO: These arguments need to get swapped. return root.Hardlink(linkname, target) }, } var rootSymlinkCmd = &cli.Command{ Name: "symlink", Usage: "make a symbolic link inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "target", }, &cli.StringArg{ Name: "linkname", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) target := cmd.StringArg("target") linkname := cmd.StringArg("linkname") // TODO: These arguments need to get swapped. return root.Symlink(linkname, target) }, } var rootReadlinkCmd = &cli.Command{ Name: "readlink", Usage: "read the target path of a symbolic link inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") target, err := root.Readlink(subpath) if err != nil { return err } fmt.Println("LINK-TARGET", target) return nil }, } var rootUnlinkCmd = &cli.Command{ Name: "unlink", Usage: "remove a file inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") return root.RemoveFile(subpath) }, } var rootRmdirCmd = &cli.Command{ Name: "rmdir", Usage: "remove an (empty) directory inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") return root.RemoveDir(subpath) }, } var rootRmdirAllCmd = &cli.Command{ Name: "rmdir-all", Usage: "remove a path (recursively) inside the root", Arguments: []cli.Argument{ &cli.StringArg{ Name: "subpath", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) subpath := cmd.StringArg("subpath") return root.RemoveAll(subpath) }, } var rootRenameCmd = &cli.Command{ Name: "rename", Usage: "rename a path inside the root", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "exchange", }, &cli.BoolFlag{ Name: "whiteout", }, &cli.BoolWithInverseFlag{ Name: "clobber", Value: true, }, }, Arguments: []cli.Argument{ &cli.StringArg{ Name: "source", }, &cli.StringArg{ Name: "destination", }, }, Action: func(ctx context.Context, cmd *cli.Command) error { root := ctx.Value("root").(*pathrs.Root) src := cmd.StringArg("source") dst := cmd.StringArg("destination") var renameArgs uint if !cmd.Bool("clobber") { renameArgs |= unix.RENAME_NOREPLACE } if cmd.Bool("exchange") { renameArgs |= unix.RENAME_EXCHANGE } if cmd.Bool("whiteout") { renameArgs |= unix.RENAME_WHITEOUT } return root.Rename(src, dst, renameArgs) }, } pathrs-0.2.1/e2e-tests/cmd/go/ux.go000064400000000000000000000013351046102023000150750ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package main import ( "github.com/urfave/cli/v3" ) // uxOption is an option that is applied to *[cli.Command] to modify its // behaviour and add some new flag. type uxOption func(*cli.Command) *cli.Command func cmdWithOptions(cmd *cli.Command, opts ...uxOption) *cli.Command { for _, opt := range opts { cmd = opt(cmd) } return cmd } pathrs-0.2.1/e2e-tests/cmd/go/ux_mode.go000064400000000000000000000033461046102023000161050ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package main import ( "context" "fmt" "os" "strconv" "strings" "github.com/urfave/cli/v3" "golang.org/x/sys/unix" ) func parseModeFlag(modeStr string) (os.FileMode, error) { modeStr = strings.TrimPrefix(modeStr, "0o") unixMode, err := strconv.ParseUint(modeStr, 8, 32) if err != nil { return 0, fmt.Errorf("failed to parse --mode: %w", err) } if unixMode&^0o7777 != 0 { return 0, fmt.Errorf("invalid --mode %#o: must be subset of 0o7777") } mode := os.FileMode(unixMode & 0o777) if unixMode&unix.S_ISUID == unix.S_ISUID { mode |= os.ModeSetuid } if unixMode&unix.S_ISGID == unix.S_ISGID { mode |= os.ModeSetgid } if unixMode&unix.S_ISVTX == unix.S_ISVTX { mode |= os.ModeSticky } return mode, nil } func modeFlag(name, usage, dfl string) uxOption { return func(cmd *cli.Command) *cli.Command { cmd.Flags = append(cmd.Flags, &cli.StringFlag{ Name: name, Usage: usage, Value: dfl, }) // TODO: Should we wrap Action instead? oldBefore := cmd.Before cmd.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) { mode, err := parseModeFlag(cmd.String(name)) if err != nil { return nil, fmt.Errorf("error parsing --%s: %w", name, err) } ctx = context.WithValue(ctx, name, mode) if oldBefore != nil { ctx, err = oldBefore(ctx, cmd) } return ctx, err } return cmd } } pathrs-0.2.1/e2e-tests/cmd/go/ux_oflags.go000064400000000000000000000050021046102023000164230ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package main import ( "context" "fmt" "strings" "github.com/urfave/cli/v3" "golang.org/x/sys/unix" ) var oflagValues = map[string]int{ // Access modes (including O_PATH). "O_RDWR": unix.O_RDWR, "O_RDONLY": unix.O_RDONLY, "O_WRONLY": unix.O_WRONLY, "O_PATH": unix.O_PATH, // Fd flags. "O_CLOEXEC": unix.O_CLOEXEC, // Control lookups. "O_NOFOLLOW": unix.O_NOFOLLOW, "O_DIRECTORY": unix.O_DIRECTORY, "O_NOCTTY": unix.O_NOCTTY, // NOTE: This flag contains O_DIRECTORY! "O_TMPFILE": unix.O_TMPFILE, // File creation. "O_CREAT": unix.O_CREAT, "O_EXCL": unix.O_EXCL, "O_TRUNC": unix.O_TRUNC, "O_APPEND": unix.O_APPEND, // Sync. "O_SYNC": unix.O_SYNC, "O_ASYNC": unix.O_ASYNC, "O_DSYNC": unix.O_DSYNC, "O_FSYNC": unix.O_FSYNC, "O_RSYNC": unix.O_RSYNC, "O_DIRECT": unix.O_DIRECT, "O_NDELAY": unix.O_NDELAY, "O_NOATIME": unix.O_NOATIME, "O_NONBLOCK": unix.O_NONBLOCK, } func parseOflags(flags string) (int, error) { oflagFieldsFunc := func(ch rune) bool { return ch == '|' || ch == ',' } var oflags int for flag := range strings.FieldsFuncSeq(flags, oflagFieldsFunc) { // Convert any flags to -> O_*. flag = strings.ToUpper(flag) if !strings.HasPrefix(flag, "O_") { flag = "O_" + flag } val, ok := oflagValues[flag] if !ok { return 0, fmt.Errorf("unknown flag name %q", flag) } oflags |= val } return oflags, nil } func oflags(name, usage string, dfl any) uxOption { return func(cmd *cli.Command) *cli.Command { cmd.Flags = append(cmd.Flags, &cli.StringFlag{ Name: name, Usage: usage + " (comma- or |-separated)", }) // TODO: Should we wrap Action instead? oldBefore := cmd.Before cmd.Before = func(ctx context.Context, cmd *cli.Command) (context.Context, error) { if cmd.IsSet(name) { oflags, err := parseOflags(cmd.String(name)) if err != nil { return nil, fmt.Errorf("error parsing --%s: %w", name, err) } ctx = context.WithValue(ctx, name, oflags) } else { ctx = context.WithValue(ctx, name, dfl) } var err error if oldBefore != nil { ctx, err = oldBefore(ctx, cmd) } return ctx, err } return cmd } } pathrs-0.2.1/e2e-tests/cmd/python/.gitignore000064400000000000000000000000101046102023000170030ustar 00000000000000/.venv/ pathrs-0.2.1/e2e-tests/cmd/python/Makefile000064400000000000000000000022621046102023000164660ustar 00000000000000#!/bin/bash # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. define script = #!/bin/bash # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. set -Eeuo pipefail SRC_DIR="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")" exec "$SRC_DIR/.venv/bin/python3" "$SRC_DIR/pathrs-cmd.py" "$@" endef .ONESHELL: .PHONY: pathrs-cmd pathrs-cmd: .venv cat >$@ <<'EOF' $(value script) EOF chmod +x $@ .PHONY: .venv .venv: python3 -m venv $@ make -C ../../../contrib/bindings/python $@/bin/pip3 install --force ../../../contrib/bindings/python/dist/*.whl pathrs-0.2.1/e2e-tests/cmd/python/pathrs-cmd.py000075500000000000000000000431411046102023000174460ustar 00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. import os import sys import stat import argparse from typing import Optional, Self, Sequence, Protocol, Tuple sys.path.append(os.path.dirname(__file__) + "/../../../contrib/bindings/python") import pathrs from pathrs import procfs from pathrs.procfs import ProcfsHandle, ProcfsBase class FilenoFile(Protocol): def fileno(self) -> int: ... def fdpath(fd: FilenoFile) -> str: return ProcfsHandle.cached().readlink(procfs.PROC_THREAD_SELF, f"fd/{fd.fileno()}") def root_resolve(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath follow: bool = args.follow reopen: Optional[int] = args.reopen with root.resolve(subpath, follow_trailing=follow) as handle: print("HANDLE-PATH", fdpath(handle)) if reopen is not None: with handle.reopen_raw(reopen) as file: print("FILE-PATH", fdpath(file)) def root_open(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath oflags: int = args.oflags follow: bool = args.follow if not follow: oflags |= os.O_NOFOLLOW with root.open_raw(subpath, oflags) as file: print("FILE-PATH", fdpath(file)) def root_mkfile(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath oflags: int = args.oflags mode: int = args.mode with root.creat_raw(subpath, oflags, mode) as file: print("FILE-PATH", fdpath(file)) def root_mkdir(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath mode: int = args.mode root.mkdir(subpath, mode) def root_mkdir_all(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath mode: int = args.mode with root.mkdir_all(subpath, mode) as handle: print("HANDLE-PATH", fdpath(handle)) def root_mknod(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath mode: int = args.mode inode_type: int inode_dev: int (inode_type, inode_dev) = args.type mode |= inode_type root.mknod(subpath, mode, inode_dev) def root_hardlink(args: argparse.Namespace): root: pathrs.Root = args.root target: str = args.target linkname: str = args.linkname # TODO: These arguments need to get swapped. root.hardlink(linkname, target) def root_symlink(args: argparse.Namespace): root: pathrs.Root = args.root target: str = args.target linkname: str = args.linkname # TODO: These arguments need to get swapped. root.symlink(linkname, target) def root_readlink(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath target = root.readlink(subpath) print("LINK-TARGET", target) def root_unlink(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath root.unlink(subpath) def root_rmdir(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath root.rmdir(subpath) def root_rmdir_all(args: argparse.Namespace): root: pathrs.Root = args.root subpath: str = args.subpath root.remove_all(subpath) def root_rename(args: argparse.Namespace): root: pathrs.Root = args.root source: str = args.source destination: str = args.destination clobber: bool = args.clobber exchange: bool = args.exchange whiteout: bool = args.whiteout RENAME_NOREPLACE: int = 1 << 0 RENAME_EXCHANGE: int = 1 << 1 RENAME_WHITEOUT: int = 1 << 2 rename_args: int = 0 if not clobber: rename_args |= RENAME_NOREPLACE if exchange: rename_args |= RENAME_EXCHANGE if whiteout: rename_args |= RENAME_WHITEOUT root.rename(source, destination, rename_args) def procfs_open(args: argparse.Namespace): unmasked: bool = args.unmasked proc = ProcfsHandle.new(unmasked=unmasked) base: ProcfsBase = args.procfs_base subpath: str = args.subpath oflags: int = args.oflags follow: bool = args.follow if not follow: oflags |= os.O_NOFOLLOW with proc.open_raw(base, subpath, oflags) as file: print("FILE-PATH", fdpath(file)) def procfs_readlink(args: argparse.Namespace): unmasked: bool = args.unmasked proc = ProcfsHandle.new(unmasked=unmasked) base: ProcfsBase = args.procfs_base subpath: str = args.subpath target = proc.readlink(base, subpath) print("LINK-TARGET", target) def parse_args( args: tuple[str, ...], ) -> Tuple[argparse.ArgumentParser, argparse.Namespace]: parser = argparse.ArgumentParser(prog="pathrs-cmd") parser.set_defaults(func=None) top_subparser = parser.add_subparsers() def add_mode_flag( parser: argparse.ArgumentParser, name: str, default: int = 0o644, required: bool = False, help: Optional[str] = None, ) -> None: parser.add_argument( f"--{name}", type=lambda mode: int(mode, 8), default=default, required=required, help=help, ) # TODO: Should this be a class? def parse_oflags(flags: str) -> int: VALID_FLAGS = { # Access modes (including O_PATH). "O_RDWR": os.O_RDWR, "O_RDONLY": os.O_RDONLY, "O_WRONLY": os.O_WRONLY, "O_PATH": os.O_PATH, # Fd flags. "O_CLOEXEC": os.O_CLOEXEC, # Control lookups. "O_NOFOLLOW": os.O_NOFOLLOW, "O_DIRECTORY": os.O_DIRECTORY, "O_NOCTTY": os.O_NOCTTY, # NOTE: This flag contains O_DIRECTORY! "O_TMPFILE": os.O_TMPFILE, # File creation. "O_CREAT": os.O_CREAT, "O_EXCL": os.O_EXCL, "O_TRUNC": os.O_TRUNC, "O_APPEND": os.O_APPEND, # Sync. "O_SYNC": os.O_SYNC, "O_ASYNC": os.O_ASYNC, "O_DSYNC": os.O_DSYNC, "O_FSYNC": os.O_FSYNC, "O_RSYNC": os.O_RSYNC, "O_DIRECT": os.O_DIRECT, "O_NDELAY": os.O_NDELAY, "O_NOATIME": os.O_NOATIME, "O_NONBLOCK": os.O_NONBLOCK, } all_flags = 0 for flag in flags.replace(",", "|").split("|"): flag = flag.upper() if not flag.startswith("O_"): flag = "O_" + flag try: all_flags |= VALID_FLAGS[flag] except KeyError: raise ValueError(f"flag {flag:r} is not a valid O_* flag") return all_flags def add_o_flag( parser: argparse.ArgumentParser, name: str, default: Optional[int] = os.O_RDONLY, required: bool = False, help: Optional[str] = None, ) -> None: parser.add_argument( f"--{name}", metavar="O_*", type=parse_oflags, default=default, required=required, help=f"{help or name} (comma- or |-separated)", ) # root --root ... commands root_parser = top_subparser.add_parser("root", help="Root::* operations") root_parser.add_argument( "--root", type=pathrs.Root, required=True, help="root path" ) root_subparser = root_parser.add_subparsers() # root resolve [--reopen ] [--[no-]follow] subpath root_resolve_parser = root_subparser.add_parser( "resolve", help="resolve a path inside the root" ) root_resolve_parser.add_argument( "--follow", default=True, action=argparse.BooleanOptionalAction, help="follow trailing symlinks", ) add_o_flag( root_resolve_parser, "reopen", default=None, help="reopen the handle with these O_* flags", ) root_resolve_parser.add_argument("subpath", help="path inside the root") root_resolve_parser.set_defaults(func=root_resolve) del root_resolve_parser # root open [--oflags ] [--[no-]follow] subpath root_open_parser = root_subparser.add_parser( "open", help="open a path inside the root" ) root_open_parser.add_argument( "--follow", default=True, action=argparse.BooleanOptionalAction, help="follow trailing symlinks", ) add_o_flag( root_open_parser, "oflags", help="O_* flags to use when opening the file" ) root_open_parser.add_argument("subpath", help="path inside the root") root_open_parser.set_defaults(func=root_open) del root_open_parser # root mkfile [--oflags ] [--mode ] subpath root_mkfile_parser = root_subparser.add_parser( "mkfile", help="make an empty file inside the root" ) add_o_flag( root_mkfile_parser, "oflags", help="O_* flags to use when creating the file" ) add_mode_flag(root_mkfile_parser, "mode", help="created file mode") root_mkfile_parser.add_argument("subpath", help="path inside the root") root_mkfile_parser.set_defaults(func=root_mkfile) del root_mkfile_parser # root mkdir [--mode ] subpath root_mkdir_parser = root_subparser.add_parser( "mkdir", help="make an empty directory inside the root" ) add_mode_flag( root_mkdir_parser, "mode", default=0o755, help="file mode for the created directory", ) root_mkdir_parser.add_argument("subpath", help="path inside the root") root_mkdir_parser.set_defaults(func=root_mkdir) del root_mkdir_parser # root mkdir-all [--mode ] subpath root_mkdir_all_parser = root_subparser.add_parser( "mkdir-all", help="make a directory (including parents) inside the root", ) add_mode_flag( root_mkdir_all_parser, "mode", default=0o755, help="file mode for the created directories", ) root_mkdir_all_parser.add_argument("subpath", help="path inside the root") root_mkdir_all_parser.set_defaults(func=root_mkdir_all) del root_mkdir_all_parser # root mknod [--mode ] [ ] root_mknod_parser = root_subparser.add_parser( "mknod", help="make an inode inside the root" ) add_mode_flag(root_mknod_parser, "mode", help="file mode for the new inode") root_mknod_parser.add_argument("subpath", help="path inside the root") class MknodTypeAction(argparse.Action): def __call__( self: Self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, values: Optional[str | Sequence[str]], option_string: Optional[str] = None, ): inode_type: str dev: int match values: case (inode_type, major, minor): dev = os.makedev(int(major), int(minor)) case (inode_type,): dev = 0 case _: raise ValueError(f"invalid mknod type {values}") try: inode_ftype = { "f": stat.S_IFREG, "d": stat.S_IFDIR, "p": stat.S_IFIFO, "b": stat.S_IFBLK, "c": stat.S_IFCHR, "u": stat.S_IFCHR, # alias for "c" }[inode_type] except KeyError: raise ValueError(f"invalid mknod type {values}") setattr(namespace, self.dest, (inode_ftype, dev)) root_mknod_parser.add_argument( "type", nargs=argparse.PARSER, action=MknodTypeAction, help="inode type to create (like mknod(1))", ) root_mknod_parser.set_defaults(func=root_mknod) del root_mknod_parser # root hardlink root_hardlink_parser = root_subparser.add_parser( "hardlink", help="make a hardlink inside the root" ) root_hardlink_parser.add_argument( "target", help="target path of the hardlink inside the root (must already exist)", ) root_hardlink_parser.add_argument( "linkname", help="path inside the root for the new hardlink" ) root_hardlink_parser.set_defaults(func=root_hardlink) del root_hardlink_parser # root symlink root_symlink_parser = root_subparser.add_parser( "symlink", help="make a symbolic link inside the root" ) root_symlink_parser.add_argument("target", help="target path of the symlink") root_symlink_parser.add_argument( "linkname", help="path inside the root for the new symlink" ) root_symlink_parser.set_defaults(func=root_symlink) del root_symlink_parser # root readlink root_readlink_parser = root_subparser.add_parser( "readlink", help="read the target path of a symbolic link inside the root" ) root_readlink_parser.add_argument("subpath", help="path inside the root") root_readlink_parser.set_defaults(func=root_readlink) del root_readlink_parser # root unlink root_unlink_parser = root_subparser.add_parser( "unlink", help="remove a file inside the root" ) root_unlink_parser.add_argument("subpath", help="path inside the root") root_unlink_parser.set_defaults(func=root_unlink) del root_unlink_parser # root rmdir root_rmdir_parser = root_subparser.add_parser( "rmdir", help="remove an (empty) directory inside the root" ) root_rmdir_parser.add_argument("subpath", help="path inside the root") root_rmdir_parser.set_defaults(func=root_rmdir) del root_rmdir_parser # root rmdir-all root_rmdir_all_parser = root_subparser.add_parser( "rmdir-all", help="remove a path (recursively) inside the root" ) root_rmdir_all_parser.add_argument("subpath", help="path inside the root") root_rmdir_all_parser.set_defaults(func=root_rmdir_all) del root_rmdir_all_parser # root rename [--exchange] [--whiteout] [--[no-]clobber] root_rename_parser = root_subparser.add_parser( "rename", help="rename a path inside the root" ) root_rename_parser.add_argument( "--whiteout", action="store_true", help="create whiteout inode in place of source", ) root_rename_parser.add_argument( "--exchange", action="store_true", help="swap source and destination inodes", ) root_rename_parser.add_argument( "--clobber", default=True, action=argparse.BooleanOptionalAction, help="allow rename target to be clobbered", ) root_rename_parser.add_argument("source", help="source path inside the root") root_rename_parser.add_argument( "destination", help="destination path inside the root" ) root_rename_parser.set_defaults(func=root_rename) del root_rename_parser del root_subparser, root_parser def parse_procfs_base(base: str) -> ProcfsBase: if base.startswith("pid="): pid = int(base.removeprefix("pid=")) return procfs.PROC_PID(pid) else: match base: case "root": return procfs.PROC_ROOT case "self": return procfs.PROC_SELF case "thread-self": return procfs.PROC_THREAD_SELF case _: raise ValueError(f"invalid procfs base {base:r}") # procfs [--unmasked] --base ... commands procfs_parser = top_subparser.add_parser( "procfs", help="ProcfsHandle::* operations" ) procfs_parser.add_argument( "--unmasked", action="store_true", help="use unmasked procfs handle" ) procfs_parser.add_argument( "--base", dest="procfs_base", type=parse_procfs_base, metavar="PROC_*", default=procfs.PROC_ROOT, help="base path for procfs operations (root, pid=, self, thread-self)", ) procfs_subparser = procfs_parser.add_subparsers() # procfs open [--oflags ] [--[no-]follow] subpath procfs_open_parser = procfs_subparser.add_parser( "open", help="open a subpath in procfs" ) add_o_flag( procfs_open_parser, "oflags", help="O_* flags to use when opening the file" ) procfs_open_parser.add_argument( "--follow", default=True, action=argparse.BooleanOptionalAction, help="follow trailing symlinks", ) procfs_open_parser.add_argument("subpath", help="path inside procfs base") procfs_open_parser.set_defaults(func=procfs_open) del procfs_open_parser # procfs readlink procfs_readlink_parser = procfs_subparser.add_parser( "readlink", help="read the target path of a symbolic link in procfs" ) procfs_readlink_parser.add_argument("subpath", help="path inside procfs base") procfs_readlink_parser.set_defaults(func=procfs_readlink) del procfs_readlink_parser del procfs_subparser, procfs_parser return parser, parser.parse_args(args) def main(*argv: str): parser, args = parse_args(argv) if args.func is not None: args.func(args) else: # Default to help page if no subcommand was selected. parser.print_help() if __name__ == "__main__": try: main(*sys.argv[1:]) except pathrs.PathrsError as e: print(f"ERRNO {e.errno} ({os.strerror(e.errno)})") print(f"error: {e.message}") sys.exit(1) pathrs-0.2.1/e2e-tests/tests/helpers.bash000064400000000000000000000062351046102023000164110ustar 00000000000000#!/bin/bash # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. set -u function fail() { echo "FAILURE:" "$@" >&2 false } [ -v PATHRS_CMD ] || { echo "PATHRS_CMD (pathrs-cmd) must be provided" >&2 exit 2 } # Like bats's built-in run, except that we get output and status information. function sane_run() { local cmd="$1" shift run "$cmd" "$@" # Some debug information to make life easier. echo "$(basename "$cmd") $* (status=$status)" >&2 echo "$output" >&2 } # Wrapper for PATHRS_CMD. function pathrs-cmd() { sane_run "$PATHRS_CMD" "$@" } # Shorthand for checking the ERRNO value from pathrs-cmd. function check-errno() { local errno_num errno_num="$(errno "$1" | awk '{ print $2 }')" [ "$status" -ne 0 ] [[ "$output" == *"ERRNO $errno_num "* ]] } # Let's not store everything in /tmp -- that would just be messy. TESTDIR_TMPDIR="$BATS_TMPDIR/pathrs-e2e-tests" mkdir -p "$TESTDIR_TMPDIR" # Stores the set of tmpdirs that still have to be cleaned up. Calling # teardown_tmpdirs will set this to an empty array (and all the tmpdirs # contained within are removed). TESTDIR_LIST="$TESTDIR_TMPDIR/pathrs-test-tmpdirs.$$" # setup_tmpdir creates a new temporary directory and returns its name. Note # that if "$IS_ROOTLESS" is true, then removing this tmpdir might be harder # than expected -- so tests should not really attempt to clean up tmpdirs. function setup_tmpdir() { [[ -n "${PATHRS_TMPDIR:-}" ]] || PATHRS_TMPDIR="$TESTDIR_TMPDIR" mktemp -d "$PATHRS_TMPDIR/pathrs-test-tmpdir.XXXXXXXX" | tee -a "$TESTDIR_LIST" } # setup_tmpdirs just sets up the "built-in" tmpdirs. function setup_tmpdirs() { declare -g PATHRS_TMPDIR PATHRS_TMPDIR="$(setup_tmpdir)" } # teardown_tmpdirs removes all tmpdirs created with setup_tmpdir. function teardown_tmpdirs() { # Do nothing if TESTDIR_LIST doesn't exist. [ -e "$TESTDIR_LIST" ] || return # Remove all of the tmpdirs. while IFS= read -r tmpdir; do [ -e "$tmpdir" ] || continue chmod -R 0777 "$tmpdir" rm -rf "$tmpdir" done < "$TESTDIR_LIST" # Clear tmpdir list. rm -f "$TESTDIR_LIST" } # Returns whether the provided binary is compiled or is a #!-script. This is # necessary for tests that look at /proc/self/exe, because for those we cannot # just take the expected path as being $PATHRS_CMD. function is-compiled() { local bin="$1" readelf -h "$bin" >/dev/null 2>&1 } # Allows a test to specify what things it requires. If the environment can't # support it, the test is skipped with a message. function requires() { for var in "$@"; do case "$var" in root) [ "$(id -u)" -eq 0 ] || skip "test requires ${var}" ;; compiled) { is-compiled "$1"; } || skip "test requires $PATHRS_CMD be compiled" ;; can-mkwhiteout) mknod "$(setup_tmpdir)/mknod0" c 0 0 || skip "test requires the ability to 'mknod c 0 0'" ;; *) fail "BUG: Invalid requires ${var}." ;; esac done } pathrs-0.2.1/e2e-tests/tests/meta.bats000064400000000000000000000041771046102023000157140ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } # These are mainly meta-tests that ensure that pathrs-cmd supports certain # specific features. @test "pathrs-cmd [bad O_* flags]" { ROOT="$(setup_tmpdir)" touch "$ROOT/file" pathrs-cmd root --root "$ROOT" resolve --reopen O_BADFLAG file [ "$status" -ne 0 ] grep -F "O_BADFLAG" <<<"$output" # the error should mention the flag pathrs-cmd root --root "$ROOT" resolve --reopen badflag file [ "$status" -ne 0 ] grep -Fi "badflag" <<<"$output" # the error should mention the flag pathrs-cmd root --root "$ROOT" open --oflags O_BADFLAG file [ "$status" -ne 0 ] grep -F "O_BADFLAG" <<<"$output" # the error should mention the flag pathrs-cmd root --root "$ROOT" open --oflags badflag file [ "$status" -ne 0 ] grep -Fi "badflag" <<<"$output" # the error should mention the flag pathrs-cmd root --root "$ROOT" open --oflags rdonly,O_BADFLAG file [ "$status" -ne 0 ] grep -F "O_BADFLAG" <<<"$output" # the error should mention the flag pathrs-cmd root --root "$ROOT" open --oflags O_RDONLY,badflag file [ "$status" -ne 0 ] grep -Fi "badflag" <<<"$output" # the error should mention the flag } @test "pathrs-cmd [funky O_* flags]" { ROOT="$(setup_tmpdir)" echo "THIS SHOULD BE TRUNCATED" >"$ROOT/file" [ "$(stat -c '%s' "$ROOT/file")" -ne 0 ] chmod 0644 "$ROOT/file" pathrs-cmd root --root "$ROOT" open --oflags trunc file [ "$status" -eq 0 ] grep -Fx "FILE-PATH $ROOT/file" <<<"$output" sane_run stat -c '%F' "$ROOT/file" [[ "$output" == "regular empty file" ]] sane_run stat -c '%s' "$ROOT/file" [ "$output" -eq 0 ] } @test "pathrs-cmd mknod [bad type]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod file l [ "$status" -ne 0 ] } pathrs-0.2.1/e2e-tests/tests/procfs-open.bats000064400000000000000000000144671046102023000172240ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } # TODO: All of these tests are very limited because we cannot verify anything # useful about the opened files (and the path is actually not guaranteed to be # correct outside of magic-link cases). Ideally we would instead output # something simple like the fdinfo, stat, and/or contents to verify against. @test "procfs --base open --oflags O_RDONLY" { pathrs-cmd procfs open modules [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output" pathrs-cmd procfs --base root open --oflags O_RDONLY modules [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output" } @test "procfs --base pid=\$\$ --oflags O_RDONLY" { pathrs-cmd procfs open modules [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output" pathrs-cmd procfs --base root open --oflags O_RDONLY modules [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/modules$' <<<"$output" } @test "procfs --base self --oflags O_RDONLY" { pathrs-cmd procfs --base self open status [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/status$' <<<"$output" pathrs-cmd procfs --base self open stack [ "$status" -eq 0 ] # NOTE: While only admins can read from /proc/$n/stack, we can open it. grep -E '^FILE-PATH (/proc)?/[0-9]+/stack$' <<<"$output" } @test "procfs --base thread-self --oflags O_RDONLY" { pathrs-cmd procfs --base thread-self open status [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+/status$' <<<"$output" pathrs-cmd procfs --base thread-self open stack [ "$status" -eq 0 ] # NOTE: While only admins can read from /proc/$n/stack, we can open it. grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+/stack$' <<<"$output" } # Make sure that thread-self and self are actually handled differently. @test "procfs open [self != thread-self]" { pathrs-cmd procfs --base self open task [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]*/task$' <<<"$output" pathrs-cmd procfs --base thread-self open task check-errno ENOENT } @test "procfs open --follow [symlinks]" { pathrs-cmd procfs open mounts [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/mounts$' <<<"$output" pathrs-cmd procfs open --oflags O_DIRECTORY mounts check-errno ENOTDIR pathrs-cmd procfs open --oflags O_DIRECTORY net [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/net$' <<<"$output" pathrs-cmd procfs open --follow self [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+$' <<<"$output" pathrs-cmd procfs open --follow thread-self if [ -e /proc/thread-self ]; then [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+$' <<<"$output" else check-errno ENOENT fi } @test "procfs open --follow [magic-links]" { exepath="$(readlink -f "$PATHRS_CMD")" pathrs-cmd procfs --base self open --follow --oflags O_RDONLY exe [ "$status" -eq 0 ] ! grep -E '^FILE-PATH (/proc)?/[0-9]+/.*$' <<<"$output" # We can only guess /proc/self/exe for compiled pathrs-cmd binaries. if is-compiled "$PATHRS_CMD"; then grep -Fx "FILE-PATH $exepath" <<<"$output" fi realpwd="$(readlink -f "$PWD")" pathrs-cmd procfs --base self open --follow --oflags O_RDONLY cwd [ "$status" -eq 0 ] grep -Fx "FILE-PATH $realpwd" <<<"$output" dummyfile="$(setup_tmpdir)/dummyfile" echo "THIS SHOULD BE TRUNCATED" >"$dummyfile" [ "$(stat -c '%s' "$dummyfile")" -ne 0 ] pathrs-cmd procfs --base thread-self open --follow --oflags O_RDWR,O_TRUNC fd/100 100>>"$dummyfile" [ "$status" -eq 0 ] grep -Fx "FILE-PATH $dummyfile" <<<"$output" # The file should've been truncated by O_TRUNC. [ "$(stat -c '%s' "$dummyfile")" -eq 0 ] } @test "procfs open --no-follow [symlinks]" { pathrs-cmd procfs open --no-follow mounts check-errno ELOOP pathrs-cmd procfs open --no-follow net check-errno ELOOP pathrs-cmd procfs open --no-follow self check-errno ELOOP pathrs-cmd procfs open --no-follow thread-self if [ -e /proc/thread-self ]; then check-errno ELOOP else check-errno ENOENT fi } @test "procfs open --no-follow [magic-links]" { pathrs-cmd procfs --base pid=1 open --no-follow --oflags O_DIRECTORY root # O_DIRECTORY beats O_NOFOLLOW! check-errno ENOTDIR pathrs-cmd procfs --base pid=1 open --no-follow --oflags O_RDWR root # O_NOFOLLOW beats permission errors check-errno ELOOP pathrs-cmd procfs --base self open --no-follow --oflags O_RDONLY exe check-errno ELOOP pathrs-cmd procfs --base thread-self open --no-follow --oflags O_RDONLY exe check-errno ELOOP dummyfile="$(setup_tmpdir)/dummyfile" echo "THIS SHOULD NOT BE TRUNCATED" >"$dummyfile" [ "$(stat -c '%s' "$dummyfile")" -ne 0 ] pathrs-cmd procfs --base thread-self open --no-follow --oflags O_RDWR,O_TRUNC fd/100 100>>"$dummyfile" check-errno ELOOP # The file should NOT have been truncated by O_TRUNC. [ "$(stat -c '%s' "$dummyfile")" -ne 0 ] } @test "procfs open --no-follow --oflags O_PATH [symlinks]" { pathrs-cmd procfs --base self open --no-follow --oflags O_PATH exe [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/exe$' <<<"$output" pathrs-cmd procfs --base pid=$$ open --oflags O_PATH,O_NOFOLLOW exe [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/'"$$"'/exe$' <<<"$output" pathrs-cmd procfs --base thread-self open --oflags O_PATH,O_NOFOLLOW exe [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/task/[0-9]+/exe$' <<<"$output" } @test "procfs open [symlink parent component]" { pathrs-cmd procfs --base root open self/status [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/status$' <<<"$output" pathrs-cmd procfs --base root open self/fdinfo/0 [ "$status" -eq 0 ] grep -E '^FILE-PATH (/proc)?/[0-9]+/fdinfo/0$' <<<"$output" exepath="$(readlink -f "$PATHRS_CMD")" pathrs-cmd procfs --base root open self/exe [ "$status" -eq 0 ] ! grep -E '^FILE-PATH (/proc)?/[0-9]+/.*$' <<<"$output" # We can only guess /proc/self/exe for compiled pathrs-cmd binaries. if is-compiled "$PATHRS_CMD"; then grep -Fx "FILE-PATH $exepath" <<<"$output" fi } pathrs-0.2.1/e2e-tests/tests/procfs-readlink.bats000064400000000000000000000100671046102023000200440ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } @test "procfs --base root readlink" { pathrs-cmd procfs readlink net [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET self/net' <<<"$output" pathrs-cmd procfs --base root readlink mounts [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET self/mounts' <<<"$output" pathrs-cmd procfs --base root readlink self [ "$status" -eq 0 ] grep -Ex 'LINK-TARGET [0-9]+' <<<"$output" pathrs-cmd procfs --base root readlink thread-self if [ -e /proc/thread-self ]; then [ "$status" -eq 0 ] grep -Ex 'LINK-TARGET [0-9]+/task/[0-9]+' <<<"$output" else check-errno ENOENT fi } @test "procfs --base root readlink [symlink parent component]" { realpwd="$(readlink -f "$PWD")" pathrs-cmd procfs --base root readlink self/cwd [ "$status" -eq 0 ] grep -Fx "LINK-TARGET $realpwd" <<<"$output" } @test "procfs --base pid=\$\$ readlink" { realpwd="$(readlink -f "$PWD")" pathrs-cmd procfs --base pid=$$ readlink cwd [ "$status" -eq 0 ] grep -Fx "LINK-TARGET $realpwd" <<<"$output" } @test "procfs --base self readlink" { exepath="$(readlink -f "$PATHRS_CMD")" pathrs-cmd procfs --base self readlink exe [ "$status" -eq 0 ] ! grep -E '^FILE-PATH (/proc)?/[0-9]+/.*$' <<<"$output" # We can only guess /proc/self/exe for compiled pathrs-cmd binaries. if is-compiled "$PATHRS_CMD"; then grep -Fx "LINK-TARGET $exepath" <<<"$output" fi dummyfile="$(setup_tmpdir)/dummyfile" touch "$dummyfile" pathrs-cmd procfs --base self readlink fd/100 100>"$dummyfile" [ "$status" -eq 0 ] grep -Fx "LINK-TARGET $dummyfile" <<<"$output" } @test "procfs --base thread-self readlink" { exepath="$(readlink -f "$PATHRS_CMD")" pathrs-cmd procfs --base thread-self readlink exe [ "$status" -eq 0 ] ! grep -E '^FILE-PATH (/proc)?/[0-9]+/.*$' <<<"$output" # We can only guess /proc/self/exe for compiled pathrs-cmd binaries. if is-compiled "$PATHRS_CMD"; then grep -Fx "LINK-TARGET $exepath" <<<"$output" fi dummyfile="$(setup_tmpdir)/dummyfile" touch "$dummyfile" pathrs-cmd procfs --base thread-self readlink fd/100 100>"$dummyfile" [ "$status" -eq 0 ] grep -Fx "LINK-TARGET $dummyfile" <<<"$output" } # Make sure that thread-self and self are actually handled differently. @test "procfs readlink [self != thread-self]" { # This is a little ugly, but if we get ENOENT from trying to *resolve* # $base/task then we know we are in thread-self. Unfortunately, readlinkat # also returns ENOENT if the target is not a symlink so we need to check # whether the error is coming from readlinkat as well. pathrs-cmd procfs --base self readlink task check-errno ENOENT [[ "$output" == *"error:"*"readlinkat"* ]] # Make sure the error is from readlinkat(2). pathrs-cmd procfs --base thread-self readlink task check-errno ENOENT [[ "$output" != *"error:"*"readlinkat"* ]] # Make sure the error is NOT from readlinkat(2). } @test "procfs readlink [non-symlink]" { pathrs-cmd procfs readlink uptime check-errno ENOENT [[ "$output" == *"error:"*"readlinkat"* ]] # Make sure the error is from readlinkat(2). pathrs-cmd procfs --base root readlink sys/fs/overflowuid check-errno ENOENT [[ "$output" == *"error:"*"readlinkat"* ]] # Make sure the error is from readlinkat(2). pathrs-cmd procfs --base pid=1 readlink stat check-errno ENOENT [[ "$output" == *"error:"*"readlinkat"* ]] # Make sure the error is from readlinkat(2). pathrs-cmd procfs --base self readlink status check-errno ENOENT [[ "$output" == *"error:"*"readlinkat"* ]] # Make sure the error is from readlinkat(2). pathrs-cmd procfs --base thread-self readlink stack check-errno ENOENT [[ "$output" == *"error:"*"readlinkat"* ]] # Make sure the error is from readlinkat(2). } pathrs-0.2.1/e2e-tests/tests/root-create.bats000064400000000000000000000330121046102023000172000ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup_file() { export ORIGINAL_UMASK="$(umask)" } function setup() { setup_tmpdirs umask 000 } function teardown() { teardown_tmpdirs umask "$ORIGINAL_UMASK" } @test "root mkfile" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mkfile --mode 0123 file [ "$status" -eq 0 ] grep -Fx "FILE-PATH $ROOT/file" <<<"$output" [ -e "$ROOT/file" ] [ -f "$ROOT/file" ] sane_run stat -c '%#a' "$ROOT/file" [[ "$output" == "0123" ]] sane_run stat -c '%F' "$ROOT/file" [[ "$output" == "regular empty file" ]] sane_run stat -c '%s' "$ROOT/file" [ "$output" -eq 0 ] } @test "root mkfile --oflags O_EXCL" { ROOT="$(setup_tmpdir)" echo "a random file" >"$ROOT/extant-file" [ "$(stat -c '%s' "$ROOT/extant-file")" -ne 0 ] chmod 0755 "$ROOT/extant-file" pathrs-cmd root --root "$ROOT" mkfile --mode 0123 extant-file [ "$status" -eq 0 ] grep -Fx "FILE-PATH $ROOT/extant-file" <<<"$output" [ -e "$ROOT/extant-file" ] [ -f "$ROOT/extant-file" ] sane_run stat -c '%#a' "$ROOT/extant-file" # The mode is not changed if the file already existed! [[ "$output" == "0755" ]] sane_run stat -c '%F' "$ROOT/extant-file" [[ "$output" == "regular file" ]] sane_run stat -c '%s' "$ROOT/extant-file" [ "$output" -ne 0 ] pathrs-cmd root --root "$ROOT" mkfile --oflags O_EXCL --mode 0123 extant-file check-errno EEXIST sane_run stat -c '%#a' "$ROOT/extant-file" # The mode is not changed if the file already existed! [[ "$output" == "0755" ]] sane_run stat -c '%F' "$ROOT/extant-file" [[ "$output" == "regular file" ]] sane_run stat -c '%s' "$ROOT/extant-file" [ "$output" -ne 0 ] # Make sure the file is not truncated if O_EXCL is violated. pathrs-cmd root --root "$ROOT" mkfile --oflags O_EXCL,O_TRUNC --mode 0123 extant-file check-errno EEXIST sane_run stat -c '%#a' "$ROOT/extant-file" # The mode is not changed if the file already existed! [[ "$output" == "0755" ]] sane_run stat -c '%F' "$ROOT/extant-file" [[ "$output" == "regular file" ]] sane_run stat -c '%s' "$ROOT/extant-file" [ "$output" -ne 0 ] } @test "root mkfile --oflags O_TRUNC" { ROOT="$(setup_tmpdir)" file="file-$RANDOM" echo "THIS SHOULD BE TRUNCATED" >"$ROOT/$file" [ "$(stat -c '%s' "$ROOT/$file")" -ne 0 ] chmod 0644 "$ROOT/$file" pathrs-cmd root --root "$ROOT" mkfile --oflags O_TRUNC --mode 0123 "$file" [ "$status" -eq 0 ] grep -Fx "FILE-PATH $ROOT/$file" <<<"$output" [ -e "$ROOT/$file" ] [ -f "$ROOT/$file" ] sane_run stat -c '%#a' "$ROOT/$file" # The mode is not changed if the file already existed! [[ "$output" == "0644" ]] sane_run stat -c '%F' "$ROOT/$file" [[ "$output" == "regular empty file" ]] sane_run stat -c '%s' "$ROOT/$file" [ "$output" -eq 0 ] } @test "root mkfile --oflags O_TMPFILE" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/var/tmp" pathrs-cmd root --root "$ROOT" mkfile --oflags O_TMPFILE,O_RDWR --mode 0700 . [ "$status" -eq 0 ] grep -Ex "FILE-PATH $ROOT/#[0-9]+ \(deleted\)" <<<"$output" pathrs-cmd root --root "$ROOT" mkfile --oflags O_TMPFILE,O_WRONLY --mode 0700 / [ "$status" -eq 0 ] grep -Ex "FILE-PATH $ROOT/#[0-9]+ \(deleted\)" <<<"$output" pathrs-cmd root --root "$ROOT" mkfile --oflags O_TMPFILE,O_WRONLY --mode 0700 /var/tmp [ "$status" -eq 0 ] grep -Ex "FILE-PATH $ROOT/var/tmp/#[0-9]+ \(deleted\)" <<<"$output" pathrs-cmd root --root "$ROOT" mkfile --oflags O_TMPFILE,O_RDONLY --mode 0700 . check-errno EINVAL } @test "root mkfile [non-existent parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" pathrs-cmd root --root "$ROOT" mkfile --mode 0123 foo/nope/baz check-errno ENOENT ! [ -e "$ROOT/foo/nope" ] ! [ -e "$ROOT/foo/nope/baz" ] } @test "root mkfile [bad parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo" touch "$ROOT/foo/bar" pathrs-cmd root --root "$ROOT" mkfile --mode 0123 foo/bar/baz check-errno ENOTDIR [ -f "$ROOT/foo/bar" ] ! [ -e "$ROOT/foo/bar/baz" ] } @test "root mkfile [umask]" { ROOT="$(setup_tmpdir)" umask 022 pathrs-cmd root --root "$ROOT" mkfile --mode 0777 with-umask-022 [ "$status" -eq 0 ] grep -Fx "FILE-PATH $ROOT/with-umask-022" <<<"$output" [ -f "$ROOT/with-umask-022" ] umask 027 pathrs-cmd root --root "$ROOT" mkfile --mode 0777 with-umask-027 grep -Fx "FILE-PATH $ROOT/with-umask-027" <<<"$output" [ "$status" -eq 0 ] [ -f "$ROOT/with-umask-027" ] umask 117 pathrs-cmd root --root "$ROOT" mkfile --mode 0777 with-umask-117 grep -Fx "FILE-PATH $ROOT/with-umask-117" <<<"$output" [ "$status" -eq 0 ] [ -f "$ROOT/with-umask-117" ] sane_run stat -c '%#a' "$ROOT/with-umask-022" [[ "$output" == "0755" ]] sane_run stat -c '%#a' "$ROOT/with-umask-027" [[ "$output" == "0750" ]] sane_run stat -c '%#a' "$ROOT/with-umask-117" [[ "$output" == "0660" ]] } @test "root mkfile [mode=7777]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mkfile --mode 07777 all-bits [ "$status" -eq 0 ] grep -Fx "FILE-PATH $ROOT/all-bits" <<<"$output" [ -f "$ROOT/all-bits" ] sane_run stat -c '%#a' "$ROOT/all-bits" [[ "$output" == "07777" ]] } @test "root mknod [file]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod --mode 0755 file f [ "$status" -eq 0 ] [ -e "$ROOT/file" ] [ -f "$ROOT/file" ] sane_run stat -c '%#a' "$ROOT/file" [[ "$output" == "0755" ]] sane_run stat -c '%F' "$ROOT/file" [[ "$output" == "regular empty file" ]] sane_run stat -c '%s' "$ROOT/file" [ "$output" -eq 0 ] } @test "root mkdir" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mkdir --mode 0711 dir [ "$status" -eq 0 ] [ -e "$ROOT/dir" ] [ -d "$ROOT/dir" ] sane_run stat -c '%#a' "$ROOT/dir" [[ "$output" == "0711" ]] sane_run stat -c '%F' "$ROOT/dir" [[ "$output" == "directory" ]] } @test "root mkdir [non-existent parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" pathrs-cmd root --root "$ROOT" mkdir --mode 0123 foo/nope/baz check-errno ENOENT ! [ -e "$ROOT/foo/nope" ] ! [ -e "$ROOT/foo/nope/baz" ] } @test "root mkdir [bad parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo" touch "$ROOT/foo/bar" pathrs-cmd root --root "$ROOT" mkdir --mode 0123 foo/bar/baz check-errno ENOTDIR [ -f "$ROOT/foo/bar" ] ! [ -e "$ROOT/foo/bar/baz" ] } @test "root mkdir [umask]" { ROOT="$(setup_tmpdir)" umask 022 pathrs-cmd root --root "$ROOT" mkdir --mode 0777 with-umask-022 [ "$status" -eq 0 ] [ -d "$ROOT/with-umask-022" ] umask 027 pathrs-cmd root --root "$ROOT" mkdir --mode 0777 with-umask-027 [ "$status" -eq 0 ] [ -d "$ROOT/with-umask-027" ] umask 117 pathrs-cmd root --root "$ROOT" mkdir --mode 0777 with-umask-117 [ "$status" -eq 0 ] [ -d "$ROOT/with-umask-117" ] sane_run stat -c '%#a' "$ROOT/with-umask-022" [[ "$output" == "0755" ]] sane_run stat -c '%#a' "$ROOT/with-umask-027" [[ "$output" == "0750" ]] sane_run stat -c '%#a' "$ROOT/with-umask-117" [[ "$output" == "0660" ]] } @test "root mkdir [mode=7777]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mkdir --mode 07777 all-bits [ "$status" -eq 0 ] [ -d "$ROOT/all-bits" ] sane_run stat -c '%#a' "$ROOT/all-bits" # On Linux, mkdir(2) implicitly strips the setuid/setgid bits from mode. [[ "$output" == "01777" ]] } @test "root mknod [directory]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod --mode 0777 dir d [ "$status" -eq 0 ] [ -e "$ROOT/dir" ] [ -d "$ROOT/dir" ] sane_run stat -c '%#a' "$ROOT/dir" [[ "$output" == "0777" ]] sane_run stat -c '%F' "$ROOT/dir" [[ "$output" == "directory" ]] } @test "root mkdir-all" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/random" chmod -R 0777 "$ROOT/some" pathrs-cmd root --root "$ROOT" mkdir-all --mode 0711 some/random/directory [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/some/random/directory" <<<"$output" [ -d "$ROOT/some" ] [ -d "$ROOT/some/random" ] [ -d "$ROOT/some/random/directory" ] # Extant directories don't have their modes changed. sane_run stat -c '%#a' "$ROOT/some" [[ "$output" == "0777" ]] sane_run stat -c '%#a' "$ROOT/some/random" [[ "$output" == "0777" ]] # Only directories we created have their mode change. sane_run stat -c '%#a' "$ROOT/some/random/directory" [[ "$output" == "0711" ]] sane_run stat -c '%F' "$ROOT/some/random/directory" [[ "$output" == "directory" ]] } @test "root mkdir-all [non-existent parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" chmod -R 0777 "$ROOT/foo" pathrs-cmd root --root "$ROOT" mkdir-all --mode 0755 foo/nope/baz [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/foo/nope/baz" <<<"$output" [ -d "$ROOT/foo/nope" ] [ -d "$ROOT/foo/nope/baz" ] # Extant directories don't have their modes changed. sane_run stat -c '%#a' "$ROOT/foo" [[ "$output" == "0777" ]] # Only directories we created have their mode change. sane_run stat -c '%#a' "$ROOT/foo/nope" [[ "$output" == "0755" ]] sane_run stat -c '%#a' "$ROOT/foo/nope/baz" [[ "$output" == "0755" ]] sane_run stat -c '%F' "$ROOT/foo/nope/baz" [[ "$output" == "directory" ]] } @test "root mkdir-all [bad parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo" touch "$ROOT/foo/bar" pathrs-cmd root --root "$ROOT" mkdir-all --mode 0755 foo/bar/baz check-errno ENOTDIR [ -f "$ROOT/foo/bar" ] ! [ -e "$ROOT/foo/bar/baz" ] } @test "root mkdir-all [umask]" { ROOT="$(setup_tmpdir)" umask 022 pathrs-cmd root --root "$ROOT" mkdir-all --mode 0777 with-umask-022 [ "$status" -eq 0 ] [ -d "$ROOT/with-umask-022" ] umask 027 pathrs-cmd root --root "$ROOT" mkdir-all --mode 0777 with-umask-027 [ "$status" -eq 0 ] [ -d "$ROOT/with-umask-027" ] umask 117 pathrs-cmd root --root "$ROOT" mkdir-all --mode 0777 with-umask-117 [ "$status" -eq 0 ] [ -d "$ROOT/with-umask-117" ] sane_run stat -c '%#a' "$ROOT/with-umask-022" [[ "$output" == "0755" ]] sane_run stat -c '%#a' "$ROOT/with-umask-027" [[ "$output" == "0750" ]] sane_run stat -c '%#a' "$ROOT/with-umask-117" [[ "$output" == "0660" ]] } @test "root mkdir-all [mode=7777]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mkdir-all --mode 07777 all-bits # FIXME: https://github.com/cyphar/libpathrs/issues/280 check-errno EINVAL ! [ -d "$ROOT/all-bits" ] pathrs-cmd root --root "$ROOT" mkdir-all --mode 01777 mode-1777 [ "$status" -eq 0 ] ! [ -d "$ROOT/mode-1777" ] sane_run stat -c '%#a' "$ROOT/mode-1777" [[ "$output" == "01777" ]] } @test "root mknod [whiteout]" { requires can-mkwhiteout ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod --mode 0666 "c-0-0" c 0 0 [ "$status" -eq 0 ] [ -e "$ROOT/c-0-0" ] [ -c "$ROOT/c-0-0" ] sane_run stat -c '%#a' "$ROOT/c-0-0" [[ "$output" == "0666" ]] sane_run stat -c '%F:%Hr:%Lr' "$ROOT/c-0-0" [[ "$output" == "character special file:0:0" ]] } @test "root mknod [fifo]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod --mode 0664 fifo p [ "$status" -eq 0 ] [ -e "$ROOT/fifo" ] [ -p "$ROOT/fifo" ] sane_run stat -c '%#a' "$ROOT/fifo" [[ "$output" == "0664" ]] sane_run stat -c '%F' "$ROOT/fifo" [[ "$output" == "fifo" ]] } @test "root mknod [char device]" { requires root ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod --mode 0644 chr-111-222 c 111 222 [ "$status" -eq 0 ] [ -e "$ROOT/chr-111-222" ] [ -c "$ROOT/chr-111-222" ] sane_run stat -c '%#a' "$ROOT/chr-111-222" [[ "$output" == "0644" ]] sane_run stat -c '%F:%Hr:%Lr' "$ROOT/chr-111-222" [[ "$output" == "character special file:111:222" ]] } @test "root mknod [block device]" { requires root ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod --mode 0600 blk-123-456 b 123 456 [ "$status" -eq 0 ] [ -e "$ROOT/blk-123-456" ] [ -b "$ROOT/blk-123-456" ] sane_run stat -c '%#a' "$ROOT/blk-123-456" [[ "$output" == "0600" ]] sane_run stat -c '%F:%Hr:%Lr' "$ROOT/blk-123-456" [[ "$output" == "block special file:123:456" ]] } @test "root mknod [non-existent parent component]" { requires can-mkwhiteout ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" pathrs-cmd root --root "$ROOT" mknod --mode 0123 foo/nope/baz c 0 0 check-errno ENOENT ! [ -e "$ROOT/foo/nope" ] ! [ -e "$ROOT/foo/nope/baz" ] } @test "root mknod [bad parent component]" { requires can-mkwhiteout ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo" touch "$ROOT/foo/bar" pathrs-cmd root --root "$ROOT" mknod --mode 0123 foo/bar/baz c 0 0 check-errno ENOTDIR [ -f "$ROOT/foo/bar" ] ! [ -e "$ROOT/foo/bar/baz" ] } @test "root mknod [umask]" { ROOT="$(setup_tmpdir)" umask 022 pathrs-cmd root --root "$ROOT" mknod --mode 0777 with-umask-022 f [ "$status" -eq 0 ] [ -f "$ROOT/with-umask-022" ] umask 027 pathrs-cmd root --root "$ROOT" mknod --mode 0777 with-umask-027 f [ "$status" -eq 0 ] [ -f "$ROOT/with-umask-027" ] umask 117 pathrs-cmd root --root "$ROOT" mknod --mode 0777 with-umask-117 f [ "$status" -eq 0 ] [ -f "$ROOT/with-umask-117" ] sane_run stat -c '%#a' "$ROOT/with-umask-022" [[ "$output" == "0755" ]] sane_run stat -c '%#a' "$ROOT/with-umask-027" [[ "$output" == "0750" ]] sane_run stat -c '%#a' "$ROOT/with-umask-117" [[ "$output" == "0660" ]] } @test "root mknod [mode=7777]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" mknod --mode 07777 all-bits d [ "$status" -eq 0 ] [ -d "$ROOT/all-bits" ] sane_run stat -c '%#a' "$ROOT/all-bits" # On Linux, mknod(2) implicitly strips the setuid/setgid bits from mode. [[ "$output" == "01777" ]] } pathrs-0.2.1/e2e-tests/tests/root-link.bats000064400000000000000000000134371046102023000167030ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } @test "root symlink" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" echo "passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" symlink /etc/passwd passwd-link [ "$status" -eq 0 ] [ -f "$ROOT/etc/passwd" ] [ -L "$ROOT/passwd-link" ] sane_run readlink "$ROOT/passwd-link" [ "$status" -eq 0 ] [[ "$output" == "/etc/passwd" ]] pathrs-cmd root --root "$ROOT" readlink /passwd-link [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET /etc/passwd' <<<"$output" } @test "root symlink [no-clobber file]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" touch "$ROOT/link" ino="$(stat -c '%i' "$ROOT/link")" pathrs-cmd root --root "$ROOT" symlink ../target link check-errno EEXIST [ -f "$ROOT/link" ] sane_run stat -c '%i' "$ROOT/link" [[ "$output" == "$ino" ]] } @test "root symlink [no-clobber symlink]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" touch "$ROOT/new" ln -s /old/link "$ROOT/link" pathrs-cmd root --root "$ROOT" symlink /new link check-errno EEXIST [ -L "$ROOT/link" ] sane_run readlink "$ROOT/link" [ "$status" -eq 0 ] [[ "$output" == "/old/link" ]] pathrs-cmd root --root "$ROOT" readlink /link [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET /old/link' <<<"$output" } @test "root symlink [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" pathrs-cmd root --root "$ROOT" symlink ../foo/bar link [ "$status" -eq 0 ] [ -d "$ROOT/foo/bar" ] [ -L "$ROOT/link" ] sane_run readlink "$ROOT/link" [ "$status" -eq 0 ] [[ "$output" == "../foo/bar" ]] pathrs-cmd root --root "$ROOT" readlink link [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET ../foo/bar' <<<"$output" } @test "root symlink [symlink]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo" touch "$ROOT/foo/bar" ln -s /foo/bar "$ROOT/target-link" pathrs-cmd root --root "$ROOT" symlink target-link link [ "$status" -eq 0 ] [ -f "$ROOT/foo/bar" ] [ -L "$ROOT/target-link" ] [ -L "$ROOT/link" ] sane_run readlink "$ROOT/link" [ "$status" -eq 0 ] [[ "$output" == "target-link" ]] pathrs-cmd root --root "$ROOT" readlink link [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET target-link' <<<"$output" } @test "root symlink [non-existent target]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" symlink ../..//some/dummy/path link [ "$status" -eq 0 ] ! [ -e "$ROOT/some/dummy/path" ] [ -L "$ROOT/link" ] sane_run readlink "$ROOT/link" [ "$status" -eq 0 ] [[ "$output" == "../..//some/dummy/path" ]] pathrs-cmd root --root "$ROOT" readlink link [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET ../..//some/dummy/path' <<<"$output" } @test "root symlink [non-existent parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" mkdir -p "$ROOT/etc" echo "passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" symlink /etc/passwd foo/nope/baz check-errno ENOENT ! [ -e "$ROOT/foo/nope" ] ! [ -e "$ROOT/foo/nope/baz" ] } @test "root symlink [bad parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo" touch "$ROOT/foo/bar" mkdir -p "$ROOT/etc" echo "passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" symlink /etc/passwd foo/bar/baz check-errno ENOTDIR [ -f "$ROOT/foo/bar" ] ! [ -e "$ROOT/foo/bar/baz" ] } @test "root hardlink" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" echo "passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" hardlink /etc/passwd passwd-link [ "$status" -eq 0 ] [ -f "$ROOT/etc/passwd" ] [ -f "$ROOT/passwd-link" ] sane_run readlink "$ROOT/passwd-link" [ "$status" -ne 0 ] # not a symlink! # Hardlinks have the same inode. sane_run stat -c '%i' "$ROOT/etc/passwd" target_ino="$output" sane_run stat -c '%i' "$ROOT/passwd-link" link_ino="$output" [[ "$target_ino" == "$link_ino" ]] } @test "root hardlink [no-clobber file]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" touch "$ROOT/target" touch "$ROOT/link" ino="$(stat -c '%i' "$ROOT/link")" pathrs-cmd root --root "$ROOT" hardlink target link check-errno EEXIST [ -f "$ROOT/link" ] sane_run stat -c '%i' "$ROOT/link" [[ "$output" == "$ino" ]] } @test "root hardlink [no-clobber symlink]" { ROOT="$(setup_tmpdir)" touch "$ROOT/foobar" touch "$ROOT/target" ln -s /foobar "$ROOT/link" ino="$(stat -c '%i' "$ROOT/link")" pathrs-cmd root --root "$ROOT" hardlink /target link check-errno EEXIST [ -L "$ROOT/link" ] sane_run stat -c '%i' "$ROOT/link" [[ "$output" == "$ino" ]] } @test "root hardlink [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" pathrs-cmd root --root "$ROOT" hardlink /foo/bar link check-errno EPERM [ -d "$ROOT/foo/bar" ] ! [ -e "$ROOT/link" ] } @test "root hardlink [non-existent target]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" hardlink ../..//some/dummy/path link check-errno ENOENT } @test "root hardlink [non-existent parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo/bar/baz" mkdir -p "$ROOT/etc" echo "passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" hardlink /etc/passwd foo/nope/baz check-errno ENOENT ! [ -e "$ROOT/foo/nope" ] ! [ -e "$ROOT/foo/nope/baz" ] } @test "root hardlink [bad parent component]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/foo" touch "$ROOT/foo/bar" mkdir -p "$ROOT/etc" echo "passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" hardlink /etc/passwd foo/bar/baz check-errno ENOTDIR [ -f "$ROOT/foo/bar" ] ! [ -e "$ROOT/foo/bar/baz" ] } pathrs-0.2.1/e2e-tests/tests/root-readlink.bats000064400000000000000000000027341046102023000175350ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } @test "root readlink" { ROOT="$(setup_tmpdir)" ln -s /some/random/path "$ROOT/link" pathrs-cmd root --root "$ROOT" readlink link [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET /some/random/path' <<<"$output" } @test "root readlink [stacked trailing]" { ROOT="$(setup_tmpdir)" ln -s /some/random/path "$ROOT/link-a" ln -s /link-a "$ROOT/link-b" ln -s ../../link-b "$ROOT/link-c" pathrs-cmd root --root "$ROOT" readlink link-a [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET /some/random/path' <<<"$output" pathrs-cmd root --root "$ROOT" readlink link-b [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET /link-a' <<<"$output" pathrs-cmd root --root "$ROOT" readlink link-c [ "$status" -eq 0 ] grep -Fx 'LINK-TARGET ../../link-b' <<<"$output" } @test "root readlink [non-symlink]" { ROOT="$(setup_tmpdir)" echo "/some/random/path" >"$ROOT/file" pathrs-cmd root --root "$ROOT" readlink file check-errno ENOENT [[ "$output" == *"error:"*"readlinkat"* ]] # Make sure the error is from readlinkat(2). } pathrs-0.2.1/e2e-tests/tests/root-rename.bats000064400000000000000000000007531046102023000172120ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } pathrs-0.2.1/e2e-tests/tests/root-resolve.bats000064400000000000000000000234741046102023000174270ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } # TODO: All of these tests (especially for --reopen) are very limited because # we cannot verify anything useful about the opened files. Ideally we would # instead output something simple like the fdinfo, stat, and/or contents to # verify against. @test "root resolve [file]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" echo "dummy passwd" >"$ROOT/etc/passwd" ln -s /../../../../../../../../etc "$ROOT/bad-passwd" pathrs-cmd root --root "$ROOT" resolve /etc/passwd [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/etc/passwd" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve bad-passwd/passwd [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/etc/passwd" <<<"$output" ! grep '^FILE-PATH' <<<"$output" } @test "root resolve --reopen O_RDONLY [file]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" echo "dummy passwd" >"$ROOT/etc/passwd" ln -s /../../../../../../../../etc "$ROOT/bad-passwd" pathrs-cmd root --root "$ROOT" resolve --reopen O_RDONLY /etc/passwd [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/etc/passwd" <<<"$output" grep -Fx "FILE-PATH $ROOT/etc/passwd" <<<"$output" pathrs-cmd root --root "$ROOT" resolve --reopen O_RDONLY bad-passwd/passwd [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/etc/passwd" <<<"$output" grep -Fx "FILE-PATH $ROOT/etc/passwd" <<<"$output" } @test "root open --oflags O_RDONLY [file]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" echo "dummy passwd" >"$ROOT/etc/passwd" ln -s /../../../../../../../../etc "$ROOT/bad-passwd" pathrs-cmd root --root "$ROOT" open --oflags O_RDONLY /etc/passwd [ "$status" -eq 0 ] ! grep -Fx "HANDLE-PATH $ROOT/etc/passwd" <<<"$output" grep -Fx "FILE-PATH $ROOT/etc/passwd" <<<"$output" pathrs-cmd root --root "$ROOT" open --oflags O_RDONLY bad-passwd/passwd [ "$status" -eq 0 ] ! grep -Fx "HANDLE-PATH $ROOT/etc/passwd" <<<"$output" grep -Fx "FILE-PATH $ROOT/etc/passwd" <<<"$output" } @test "root resolve --reopen O_DIRECTORY [file]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" echo "dummy passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" resolve --reopen O_DIRECTORY /etc/passwd check-errno ENOTDIR } @test "root open --oflags O_DIRECTORY [file]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/etc" echo "dummy passwd" >"$ROOT/etc/passwd" pathrs-cmd root --root "$ROOT" open --oflags O_DIRECTORY /etc/passwd check-errno ENOTDIR } @test "root resolve --reopen O_RDWR|O_TRUNC [file]" { ROOT="$(setup_tmpdir)" echo "THIS SHOULD BE TRUNCATED" >"$ROOT/to-trunc" [ "$(stat -c '%s' "$ROOT/to-trunc")" -ne 0 ] pathrs-cmd root --root "$ROOT" resolve --reopen O_RDWR,O_TRUNC to-trunc [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/to-trunc" <<<"$output" grep -Fx "FILE-PATH $ROOT/to-trunc" <<<"$output" # The file should've been truncated by O_TRUNC. [ "$(stat -c '%s' "$ROOT/to-trunc")" -eq 0 ] } @test "root open --oflags O_RDWR|O_TRUNC [file]" { ROOT="$(setup_tmpdir)" echo "THIS SHOULD BE TRUNCATED" >"$ROOT/to-trunc" [ "$(stat -c '%s' "$ROOT/to-trunc")" -ne 0 ] pathrs-cmd root --root "$ROOT" open --oflags O_RDWR,O_TRUNC to-trunc [ "$status" -eq 0 ] ! grep -Fx "HANDLE-PATH $ROOT/to-trunc" <<<"$output" grep -Fx "FILE-PATH $ROOT/to-trunc" <<<"$output" # The file should've been truncated by O_TRUNC. [ "$(stat -c '%s' "$ROOT/to-trunc")" -eq 0 ] } @test "root resolve [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/random/dir" pathrs-cmd root --root "$ROOT" resolve some/../some/./random/dir/.. [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/some/random" <<<"$output" ! grep '^FILE-PATH' <<<"$output" } @test "root resolve --reopen [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/random/dir" pathrs-cmd root --root "$ROOT" resolve --reopen O_DIRECTORY some/random/dir [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/some/random/dir" <<<"$output" grep -Fx "FILE-PATH $ROOT/some/random/dir" <<<"$output" mkdir -p "$ROOT/some/random/dir" pathrs-cmd root --root "$ROOT" resolve --reopen O_WRONLY some check-errno EISDIR } @test "root open [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/random/dir" pathrs-cmd root --root "$ROOT" open --oflags O_DIRECTORY some/random/dir [ "$status" -eq 0 ] ! grep -Fx "HANDLE-PATH $ROOT/some/random/dir" <<<"$output" grep -Fx "FILE-PATH $ROOT/some/random/dir" <<<"$output" mkdir -p "$ROOT/some/random/dir" pathrs-cmd root --root "$ROOT" open --oflags O_WRONLY some check-errno EISDIR } @test "root resolve [device inode]" { requires can-mkwhiteout ROOT="$(setup_tmpdir)" mknod "$ROOT/char-0-0" c 0 0 pathrs-cmd root --root "$ROOT" resolve char-0-0 [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/char-0-0" <<<"$output" ! grep '^FILE-PATH' <<<"$output" } @test "root resolve [/dev/full]" { pathrs-cmd root --root /dev resolve full [ "$status" -eq 0 ] grep -Fx 'HANDLE-PATH /dev/full' <<<"$output" ! grep '^FILE-PATH' <<<"$output" } @test "root resolve [fifo]" { ROOT="$(setup_tmpdir)" mkfifo "$ROOT/fifo" pathrs-cmd root --root "$ROOT" resolve fifo [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/fifo" <<<"$output" ! grep '^FILE-PATH' <<<"$output" } @test "root resolve --follow [symlinks]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/target/dir" echo "TARGET" >"$ROOT/target/dir/file" [ "$(stat -c '%s' "$ROOT/target/dir/file")" -ne 0 ] ln -s /target/dir "$ROOT/a" ln -s /a/../dir "$ROOT/b" ln -s ../b/. "$ROOT/c" ln -s /../../../../c "$ROOT/d" ln -s d "$ROOT/e" ln -s e/file "$ROOT/file-link" pathrs-cmd root --root "$ROOT" resolve --follow "target/dir" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow "a" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow "b" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow "c" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow "d" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow "e" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow --reopen O_DIRECTORY "e" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" grep -Fx "FILE-PATH $ROOT/target/dir" <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow "file-link" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir/file" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --follow --reopen O_WRONLY,O_TRUNC "file-link" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir/file" <<<"$output" grep -Fx "FILE-PATH $ROOT/target/dir/file" <<<"$output" # The file should've been truncated by O_TRUNC. [ "$(stat -c '%s' "$ROOT/target/dir/file")" -eq 0 ] } @test "root resolve --no-follow [symlinks]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/target/dir" echo "TARGET" >"$ROOT/target/dir/file" [ "$(stat -c '%s' "$ROOT/target/dir/file")" -ne 0 ] ln -s /target/dir "$ROOT/a" ln -s /a/../dir "$ROOT/b" ln -s ../b/. "$ROOT/c" ln -s /../../../../c "$ROOT/d" ln -s d "$ROOT/e" ln -s e/file "$ROOT/file-link" pathrs-cmd root --root "$ROOT" resolve --follow "target/dir" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --no-follow "a" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/a" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --no-follow "b" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/b" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --no-follow "c" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/c" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --no-follow "d" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/d" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --no-follow "e" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/e" <<<"$output" ! grep '^FILE-PATH' <<<"$output" # You cannot reopen an O_PATH|O_NOFOLLOW handle to a symlink. pathrs-cmd root --root "$ROOT" resolve --no-follow --reopen O_RDONLY "e" check-errno ELOOP pathrs-cmd root --root "$ROOT" resolve --no-follow --reopen O_DIRECTORY "e" check-errno ELOOP pathrs-cmd root --root "$ROOT" resolve --no-follow "file-link" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/file-link" <<<"$output" ! grep '^FILE-PATH' <<<"$output" pathrs-cmd root --root "$ROOT" resolve --no-follow --reopen O_WRONLY,O_TRUNC "file-link" check-errno ELOOP # The file should NOT have been truncated by O_TRUNC. [ "$(stat -c '%s' "$ROOT/target/dir/file")" -ne 0 ] # --no-follow has no impact on non-final components. pathrs-cmd root --root "$ROOT" resolve --no-follow --reopen O_WRONLY,O_TRUNC "e/file" [ "$status" -eq 0 ] grep -Fx "HANDLE-PATH $ROOT/target/dir/file" <<<"$output" grep -Fx "FILE-PATH $ROOT/target/dir/file" <<<"$output" # The file should've been truncated by O_TRUNC. [ "$(stat -c '%s' "$ROOT/target/dir/file")" -eq 0 ] } pathrs-0.2.1/e2e-tests/tests/root-rm.bats000064400000000000000000000123411046102023000163550ustar 00000000000000#!/usr/bin/bats -t # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. load helpers function setup() { setup_tmpdirs } function teardown() { teardown_tmpdirs } @test "root unlink [non-existent]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" unlink non-exist check-errno ENOENT } @test "root rmdir [non-existent]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" rmdir non-exist check-errno ENOENT } @test "root rmdir-all [non-existent]" { ROOT="$(setup_tmpdir)" pathrs-cmd root --root "$ROOT" rmdir-all non-exist [ "$status" -eq 0 ] } @test "root unlink [file]" { ROOT="$(setup_tmpdir)" echo "file" >"$ROOT/file" [ -e "$ROOT/file" ] pathrs-cmd root --root "$ROOT" unlink /file [ "$status" -eq 0 ] ! [ -e "$ROOT/file" ] } @test "root rmdir [file]" { ROOT="$(setup_tmpdir)" echo "file" >"$ROOT/file" [ -e "$ROOT/file" ] pathrs-cmd root --root "$ROOT" rmdir /file check-errno ENOTDIR [ -e "$ROOT/file" ] } @test "root rmdir-all [file]" { ROOT="$(setup_tmpdir)" echo "file" >"$ROOT/file" [ -e "$ROOT/file" ] pathrs-cmd root --root "$ROOT" rmdir-all /file [ "$status" -eq 0 ] ! [ -e "$ROOT/file" ] } @test "root unlink [fifo]" { ROOT="$(setup_tmpdir)" mkfifo "$ROOT/fifo" [ -e "$ROOT/fifo" ] pathrs-cmd root --root "$ROOT" unlink /fifo [ "$status" -eq 0 ] ! [ -e "$ROOT/fifo" ] } @test "root rmdir [fifo]" { ROOT="$(setup_tmpdir)" mkfifo "$ROOT/fifo" [ -e "$ROOT/fifo" ] pathrs-cmd root --root "$ROOT" rmdir /fifo check-errno ENOTDIR [ -e "$ROOT/fifo" ] } @test "root rmdir-all [fifo]" { ROOT="$(setup_tmpdir)" mkfifo "$ROOT/fifo" [ -e "$ROOT/fifo" ] pathrs-cmd root --root "$ROOT" rmdir-all /fifo [ "$status" -eq 0 ] ! [ -e "$ROOT/fifo" ] } @test "root unlink [symlink]" { ROOT="$(setup_tmpdir)" touch "$ROOT/file" ln -s file "$ROOT/alt" pathrs-cmd root --root "$ROOT" unlink alt [ "$status" -eq 0 ] ! [ -e "$ROOT/alt" ] [ -e "$ROOT/file" ] } @test "root rmdir [symlink]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/directory" ln -s some "$ROOT/alt" pathrs-cmd root --root "$ROOT" rmdir-all alt [ "$status" -eq 0 ] ! [ -e "$ROOT/alt" ] [ -e "$ROOT/some" ] [ -e "$ROOT/some/directory" ] } @test "root rmdir-all [symlink]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/directory" ln -s some "$ROOT/alt" pathrs-cmd root --root "$ROOT" rmdir-all alt [ "$status" -eq 0 ] ! [ -e "$ROOT/alt" ] [ -e "$ROOT/some" ] [ -e "$ROOT/some/directory" ] } @test "root unlink [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/directory" pathrs-cmd root --root "$ROOT" unlink ../some/directory check-errno EISDIR [ -e "$ROOT/some/directory" ] pathrs-cmd root --root "$ROOT" unlink ../some check-errno EISDIR [ -e "$ROOT/some/directory" ] } @test "root rmdir [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/directory" pathrs-cmd root --root "$ROOT" rmdir ../some check-errno ENOTEMPTY [ -e "$ROOT/some" ] pathrs-cmd root --root "$ROOT" rmdir ../some/directory [ "$status" -eq 0 ] ! [ -e "$ROOT/some/directory" ] pathrs-cmd root --root "$ROOT" rmdir ../some [ "$status" -eq 0 ] ! [ -e "$ROOT/some" ] } @test "root rmdir-all [directory]" { ROOT="$(setup_tmpdir)" mkdir -p "$ROOT/some/directory" touch "$ROOT/some/file" pathrs-cmd root --root "$ROOT" rmdir-all ../some [ "$status" -eq 0 ] ! [ -e "$ROOT/some" ] ! [ -e "$ROOT/some/file" ] ! [ -e "$ROOT/some/directory" ] } @test "root rmdir-all [big tree]" { ROOT="$(setup_tmpdir)" # Make a fairly deep tree. for dir1 in $(seq 16); do for dir2 in $(seq 32); do subdir="tree/dir-$dir1/dirdir-$dir2" mkdir -p "$ROOT/$subdir" for file in $(seq 16); do echo "file $file in $subdir" >"$ROOT/$subdir/file-$file-$RANDOM" done done done pathrs-cmd root --root "$ROOT" rmdir-all tree [ "$status" -eq 0 ] ! [ -e "$ROOT/tree" ] } @test "root rmdir-all ." { ROOT="$(setup_tmpdir)" # Make a fairly deep tree. for dir1 in $(seq 16); do for dir2 in $(seq 16); do subdir="dir-$dir1/dirdir-$dir2" mkdir -p "$ROOT/$subdir" for file in $(seq 16); do echo "file $file in $subdir" >"$ROOT/$subdir/file-$file-$RANDOM" done done done pathrs-cmd root --root "$ROOT" rmdir-all . # TODO: Implement this. check-errno EINVAL #[ "$status" -eq 0 ] #! [ -e "$ROOT/tree" ] # The top-level root should not be removed. [ -d "$ROOT" ] } @test "root rmdir-all /" { ROOT="$(setup_tmpdir)" # Make a fairly deep tree. for dir1 in $(seq 16); do for dir2 in $(seq 16); do subdir="dir-$dir1/dirdir-$dir2" mkdir -p "$ROOT/$subdir" for file in $(seq 16); do echo "file $file in $subdir" >"$ROOT/$subdir/file-$file-$RANDOM" done done done pathrs-cmd root --root "$ROOT" rmdir-all / # TODO: Implement this. check-errno EINVAL #[ "$status" -eq 0 ] #! [ -e "$ROOT/tree" ] # The top-level root should not be removed. [ -d "$ROOT" ] } # TODO: Add a test for removing rootless-unfriendly chmod 000 directories. # See . pathrs-0.2.1/examples/Makefile000064400000000000000000000010461046102023000144040ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. .PHONY: smoke-test smoke-test: make -C c $@ make -C go $@ make -C python $@ make smoke-test-rust .PHONY: smoke-test-rust smoke-test-rust: make -C rust-cat smoke-test pathrs-0.2.1/examples/c/.gitignore000064400000000000000000000000261046102023000151530ustar 00000000000000/cat /cat_multithread pathrs-0.2.1/examples/c/Makefile000064400000000000000000000014221046102023000146240ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. CC ?= gcc CFLAGS := $(shell pkg-config --cflags pathrs) LDFLAGS := $(shell pkg-config --libs-only-L --libs-only-other pathrs) LDLIBS := $(shell pkg-config --libs-only-l pathrs) .PHONY: all all: $(patsubst %.c,%,$(wildcard *.c)) cat_multithread: LDLIBS += -lpthread .PHONY: smoke-test smoke-test: cat cat_multithread ./cat . ../../cat.c >/dev/null ./cat_multithread . ../../cat_multithread.c >/dev/null pathrs-0.2.1/examples/c/cat.c000064400000000000000000000047141046102023000141060ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ /* * File: examples/c/cat.c * * An example program which opens a file inside a root and outputs its contents * using libpathrs. */ #include #include #include #include #include #include "../../include/pathrs.h" #define bail(fmt, ...) \ do { fprintf(stderr, fmt "\n", #__VA_ARGS__); exit(1); } while (0) /* Helper to output a pathrs_error_t in a readable format. */ void print_error(pathrs_error_t *error) { int saved_errno = error->saved_errno; if (saved_errno) printf("ERROR[%s]: %s\n", strerror(saved_errno), error->description); else printf("ERROR: %s\n", error->description); errno = saved_errno; } int open_in_root(const char *root_path, const char *unsafe_path) { int liberr = 0; int rootfd = -EBADF, fd = -EBADF; rootfd = pathrs_open_root(root_path); if (IS_PATHRS_ERR(rootfd)) { liberr = rootfd; goto err; } fd = pathrs_inroot_open(rootfd, unsafe_path, O_RDONLY); if (IS_PATHRS_ERR(fd)) { liberr = fd; goto err; } err: close(rootfd); if (IS_PATHRS_ERR(liberr)) { pathrs_error_t *error = pathrs_errorinfo(liberr); print_error(error); pathrs_errorinfo_free(error); } return fd; } void usage(void) { printf("usage: cat \n"); exit(1); } int main(int argc, char **argv) { int fd; char *root, *path; if (argc != 3) usage(); root = argv[1]; path = argv[2]; /* * Safely open the file descriptor. Normally applications would create a * root handle and persist it for longer periods of time, but this is such * a trivial example it's not necessary. */ fd = open_in_root(root, path); if (fd < 0) bail("open_in_root failed: %m"); /* Pipe the contents to stdout. */ for (;;) { ssize_t copied, written; char buffer[1024]; copied = read(fd, buffer, sizeof(buffer)); if (copied < 0) bail("read failed: %m"); else if (copied == 0) break; written = write(STDOUT_FILENO, buffer, copied); if (written < 0) bail("write failed: %m"); if (written != copied) bail("write was short (read %dB, wrote %dB)", copied, written); } close(fd); return 0; } pathrs-0.2.1/examples/c/cat_multithread.c000064400000000000000000000063361046102023000165120ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ /* * File: examples/c/cat_multithread.c * * An example program which opens a file inside a root and outputs its contents * using libpathrs, but multithreaded to show that there are no obvious race * conditions when using pathrs. */ #include #include #include #include #include #include #include "../../include/pathrs.h" #define bail(fmt, ...) \ do { fprintf(stderr, fmt "\n", #__VA_ARGS__); exit(1); } while (0) /* Helper to output a pathrs_error_t in a readable format. */ void print_error(pathrs_error_t *error) { int saved_errno = error->saved_errno; if (saved_errno) printf("ERROR[%s]: %s\n", strerror(saved_errno), error->description); else printf("ERROR: %s\n", error->description); errno = saved_errno; } struct args { pthread_barrier_t *barrier; int rootfd; const char *path; }; void *worker(void *_arg) { struct args *arg = _arg; int liberr = 0; int handlefd = -EBADF, fd = -EBADF; pthread_barrier_wait(arg->barrier); handlefd = pathrs_inroot_resolve(arg->rootfd, arg->path); if (IS_PATHRS_ERR(handlefd)) { liberr = handlefd; goto err; } fd = pathrs_reopen(handlefd, O_RDONLY); if (IS_PATHRS_ERR(fd)) { liberr = fd; goto err; } /* Pipe the contents to stdout. */ for (;;) { ssize_t copied, written; char buffer[1024]; copied = read(fd, buffer, sizeof(buffer)); if (copied < 0) bail("read failed: %m"); else if (copied == 0) break; written = write(STDOUT_FILENO, buffer, copied); if (written < 0) bail("write failed: %m"); if (written != copied) bail("write was short (read %dB, wrote %dB)", copied, written); } err: if (IS_PATHRS_ERR(liberr)) { pathrs_error_t *error = pathrs_errorinfo(liberr); print_error(error); pathrs_errorinfo_free(error); } close(fd); close(handlefd); return NULL; } void usage(void) { printf("usage: cat \n"); exit(1); } #define NUM_THREADS 32 int main(int argc, char **argv) { char *path, *root_path; pthread_barrier_t barrier; pthread_t threads[NUM_THREADS] = {}; struct args thread_args[NUM_THREADS] = {}; int liberr = 0; int rootfd = -EBADF; if (argc != 3) usage(); root_path = argv[1]; path = argv[2]; rootfd = pathrs_open_root(root_path); if (IS_PATHRS_ERR(rootfd)) { liberr = rootfd; goto err; } pthread_barrier_init(&barrier, NULL, NUM_THREADS); for (size_t i = 0; i < NUM_THREADS; i++) { pthread_t *thread = &threads[i]; struct args *arg = &thread_args[i]; *arg = (struct args) { .path = path, .rootfd = rootfd, .barrier = &barrier, }; pthread_create(thread, NULL, worker, arg); } for (size_t i = 0; i < NUM_THREADS; i++) pthread_join(threads[i], NULL); err: if (IS_PATHRS_ERR(liberr)) { pathrs_error_t *error = pathrs_errorinfo(liberr); print_error(error); pathrs_errorinfo_free(error); } close(rootfd); return 0; } pathrs-0.2.1/examples/go/.gitignore000064400000000000000000000000151046102023000153340ustar 00000000000000/cat /sysctl pathrs-0.2.1/examples/go/Makefile000064400000000000000000000011331046102023000150060ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. GO ?= go GOMOD_FILES := go.mod go.sum %: %.go $(GOMOD_FILES) $(GO) build -o $@ $< .PHONY: smoke-test smoke-test: cat sysctl ./cat . ../../cat.go &>/dev/null ./sysctl kernel.hostname kernel.overflowuid kernel.overflowgid pathrs-0.2.1/examples/go/cat.go000064400000000000000000000030031046102023000144420ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Original author of this example code: // Maxim Zhiburt (2020) // File: examples/c/cat.c // // An example program which opens a file inside a root and outputs its contents // using libpathrs. package main import ( "errors" "fmt" "io" "os" "cyphar.com/go-pathrs" ) func Main(args ...string) error { if len(args) != 2 { fmt.Fprintln(os.Stderr, "usage: cat ") os.Exit(1) } rootPath, unsafePath := args[0], args[1] root, err := pathrs.OpenRoot(rootPath) if err != nil { return fmt.Errorf("open root %q: %w", rootPath, err) } defer root.Close() file, err := root.Open(unsafePath) if err != nil { return fmt.Errorf("open %q: %w", unsafePath, err) } defer file.Close() fmt.Fprintf(os.Stderr, "== file %q (from root %q) ==\n", file.Name(), root.IntoFile().Name()) if _, err := io.Copy(os.Stdout, file); err != nil { return fmt.Errorf("copy file contents to stdout: %w", err) } return nil } func main() { if err := Main(os.Args[1:]...); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Source: %v\n", errors.Unwrap(err)) os.Exit(1) } } pathrs-0.2.1/examples/go/go.mod000064400000000000000000000002271046102023000144570ustar 00000000000000module pathrs-example go 1.18 require cyphar.com/go-pathrs v0.0.0 require golang.org/x/sys v0.26.0 replace cyphar.com/go-pathrs => ../../go-pathrs pathrs-0.2.1/examples/go/go.sum000064400000000000000000000002311046102023000144770ustar 00000000000000golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= pathrs-0.2.1/examples/go/sysctl.go000064400000000000000000000024351046102023000152240ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package main import ( "errors" "fmt" "io" "os" "strings" "golang.org/x/sys/unix" "cyphar.com/go-pathrs/procfs" ) func Main(names ...string) error { proc, err := procfs.Open(procfs.UnmaskedProcRoot) if err != nil { return fmt.Errorf("open proc root: %w", err) } defer proc.Close() //nolint:errcheck // example code for _, name := range names { path := "sys/" + strings.ReplaceAll(name, ".", "/") file, err := proc.OpenRoot(path, unix.O_RDONLY) if err != nil { return fmt.Errorf("open sysctl %s: %w", name, err) } data, err := io.ReadAll(file) _ = file.Close() if err != nil { return fmt.Errorf("read sysctl %s: %w", name, err) } fmt.Printf("%s = %q\n", name, string(data)) } return nil } func main() { if err := Main(os.Args[1:]...); err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) fmt.Fprintf(os.Stderr, "Source: %v\n", errors.Unwrap(err)) os.Exit(1) } } pathrs-0.2.1/examples/python/.gitignore000064400000000000000000000000161046102023000162510ustar 00000000000000/__pycache__/ pathrs-0.2.1/examples/python/Makefile000064400000000000000000000007641046102023000157330ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. .PHONY: smoke-test smoke-test: ./cat.py . ../../cat.py ./sysctl.py kernel.hostname kernel.overflowuid kernel.overflowgid pathrs-0.2.1/examples/python/cat.py000075500000000000000000000021121046102023000154040ustar 00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. # File: examples/python/cat.py # # An example program which opens a file inside a root and outputs its contents # using libpathrs. import os import sys sys.path.append(os.path.dirname(__file__) + "/../contrib/bindings/python") import pathrs def chomp(s): for nl in ["\r\n", "\r", "\n"]: if s.endswith(nl): return s[: -len(nl)] return s def main(root_path, unsafe_path): # Test that context managers work properly with WrappedFd: with pathrs.Root(root_path) as root: with root.open(unsafe_path, "r") as f: for line in f: line = chomp(line) print(line) if __name__ == "__main__": main(*sys.argv[1:]) pathrs-0.2.1/examples/python/static_web.py000075500000000000000000000050671046102023000167750ustar 00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. # File: examples/python/static_web.py # # An example program which provides a static webserver which will serve files # from a directory, safely resolving paths with libpathrs. import os import sys import stat import errno import flask import flask.json sys.path.append(os.path.dirname(__file__) + "/../contrib/bindings/python") import pathrs from pathrs import PathrsError app = flask.Flask(__name__) def json_dentry(dentry): st = dentry.stat(follow_symlinks=False) return { "ino": st.st_ino, "mode": stat.S_IMODE(st.st_mode), "type": { stat.S_IFREG: "regular file", stat.S_IFDIR: "directory", stat.S_IFLNK: "symlink", stat.S_IFBLK: "block device", stat.S_IFCHR: "character device", stat.S_IFIFO: "named pipe", stat.S_IFSOCK: "socket", }.get(stat.S_IFMT(st.st_mode)), } @app.route("/") def get(path): try: handle = root.resolve(path) except PathrsError as e: e.pprint() status_code = { # No such file or directory => 404 Not Found. errno.ENOENT: 404, # Operation not permitted => 403 Forbidden. errno.EPERM: 403, # Permission denied => 403 Forbidden. errno.EACCES: 403, }.get(e.errno, 500) flask.abort(status_code, "Could not resolve path: %s." % (e,)) with handle: try: f = handle.reopen("rb") return flask.Response( f, mimetype="application/octet-stream", direct_passthrough=True ) except IsADirectoryError: with handle.reopen_raw(os.O_RDONLY) as dirf: with os.scandir(dirf.fileno()) as s: return flask.json.jsonify( {dentry.name: json_dentry(dentry) for dentry in s} ) def main(root_path=None): if root_path is None: root_path = os.getcwd() # Open a root handle. This is long-lived. global root root = pathrs.Root(root_path) # Now serve our dumb HTTP server. app.run(debug=True, port=8080) if __name__ == "__main__": main(*sys.argv[1:]) pathrs-0.2.1/examples/python/sysctl.py000075500000000000000000000051261046102023000161660ustar 00000000000000#!/usr/bin/env python3 # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. # File: examples/python/sysctl.py # # An example program which does sysctl operations using the libpathrs safe # procfs API. import os import sys sys.path.append(os.path.dirname(__file__) + "/../contrib/bindings/python") from pathrs import procfs from pathrs.procfs import ProcfsHandle def bail(*args): print("[!]", *args) os.exit(1) def chomp(s: str) -> str: for nl in ["\r\n", "\r", "\n"]: if s.endswith(nl): return s[: -len(nl)] return s def sysctl_subpath(name: str) -> str: # "kernel.foo.bar" -> /proc/sys/kernel/foo/bar return "sys/" + name.replace(".", "/") PROCFS = ProcfsHandle.new(unmasked=True) def sysctl_write(name: str, value: str) -> None: subpath = sysctl_subpath(name) with PROCFS.open(procfs.PROC_ROOT, subpath, "w") as f: f.write(value) def sysctl_read(name: str, *, value_only: bool = False) -> None: subpath = sysctl_subpath(name) with PROCFS.open(procfs.PROC_ROOT, subpath, "r") as f: value = chomp(f.read()) if value_only: print(f"{value}") else: print(f"{name} = {value}") def main(*args): import argparse parser = argparse.ArgumentParser( prog="sysctl.py", description="A minimal implementation of sysctl(8) but using the libpathrs procfs API.", ) parser.add_argument( "-n", "--values", dest="value_only", action="store_true", help="print only values of the given variable(s)", ) parser.add_argument( "-w", "--write", action="store_true", help="enable writing a value to a variable", ) parser.add_argument( "sysctls", nargs="*", metavar="variable[=value]", help="sysctl variable name (such as 'kernel.overflowuid')", ) args = parser.parse_args(args) for sysctl in args.sysctls: if "=" in sysctl: if not args.write: bail("you must pass -w to enable sysctl writing") name, value = sysctl.split("=", maxsplit=1) sysctl_write(name, value) else: sysctl_read(sysctl, value_only=args.value_only) if __name__ == "__main__": main(*sys.argv[1:]) pathrs-0.2.1/examples/rust-cat/Makefile000064400000000000000000000011541046102023000161460ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. CARGO ?= cargo EXAMPLE_NAME := $(basename $(shell basename $$PWD)) .PHONY: build build: $(CARGO) build --example $(EXAMPLE_NAME) .PHONY: smoke-test smoke-test: build ../../target/debug/examples/$(EXAMPLE_NAME) . ../../main.rs &>/dev/null pathrs-0.2.1/examples/rust-cat/main.rs000064400000000000000000000031131046102023000157750ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ /* * File: examples/rust-cat/main.rs * * An example program which opens a file inside a root and outputs its * contents using libpathrs. */ use pathrs::{flags::OpenFlags, Root}; use std::io::{prelude::*, BufReader}; use anyhow::{Context, Error}; use clap::{Arg, Command}; fn main() -> Result<(), Error> { let m = Command::new("cat") // MSRV(1.67): Use clap::crate_authors!. .author("Aleksa Sarai ") .version(clap::crate_version!()) .arg(Arg::new("root").value_name("ROOT")) .arg(Arg::new("unsafe-path").value_name("PATH")) .about("") .get_matches(); let root_path = m .get_one::("root") .context("required root argument not provided")?; let unsafe_path = m .get_one::("unsafe-path") .context("required unsafe-path argument not provided")?; let root = Root::open(root_path).context("open root failed")?; let file = root .open_subpath(unsafe_path, OpenFlags::O_RDONLY) .context("open unsafe path in root")?; let reader = BufReader::new(file); for line in reader.lines() { println!("{}", line.context("read lines")?); } Ok(()) } pathrs-0.2.1/go-pathrs/.golangci.yml000064400000000000000000000015441046102023000154210ustar 00000000000000# SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. version: "2" linters: enable: - bidichk - cyclop - errname - errorlint - exhaustive - goconst - godot - gomoddirectives - gosec - mirror - misspell - mnd - nilerr - nilnil - perfsprint - prealloc - reassign - revive - unconvert - unparam - usestdlibvars - wastedassign formatters: enable: - gofumpt - goimports settings: goimports: local-prefixes: - cyphar.com/go-pathrs pathrs-0.2.1/go-pathrs/COPYING000064400000000000000000000405261046102023000140730ustar 00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- 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/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. pathrs-0.2.1/go-pathrs/doc.go000064400000000000000000000007701046102023000141310ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package pathrs provides bindings for libpathrs, a library for safe path // resolution on Linux. package pathrs pathrs-0.2.1/go-pathrs/go.mod000064400000000000000000000001731046102023000141400ustar 00000000000000// Hosted at . module cyphar.com/go-pathrs go 1.18 require golang.org/x/sys v0.26.0 pathrs-0.2.1/go-pathrs/go.sum000064400000000000000000000002311046102023000141600ustar 00000000000000golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= pathrs-0.2.1/go-pathrs/handle_linux.go000064400000000000000000000076341046102023000160440ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package pathrs import ( "fmt" "os" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // Handle is a handle for a path within a given [Root]. This handle references // an already-resolved path which can be used for only one purpose -- to // "re-open" the handle and get an actual [os.File] which can be used for // ordinary operations. // // If you wish to open a file without having an intermediate [Handle] object, // you can try to use [Root.Open] or [Root.OpenFile]. // // It is critical that perform all relevant operations through this [Handle] // (rather than fetching the file descriptor yourself with [Handle.IntoRaw]), // because the security properties of libpathrs depend on users doing all // relevant filesystem operations through libpathrs. // // [os.File]: https://pkg.go.dev/os#File type Handle struct { inner *os.File } // HandleFromFile creates a new [Handle] from an existing file handle. The // handle will be copied by this method, so the original handle should still be // freed by the caller. // // This is effectively the inverse operation of [Handle.IntoRaw], and is used // for "deserialising" pathrs root handles. func HandleFromFile(file *os.File) (*Handle, error) { newFile, err := fdutils.DupFile(file) if err != nil { return nil, fmt.Errorf("duplicate handle fd: %w", err) } return &Handle{inner: newFile}, nil } // Open creates an "upgraded" file handle to the file referenced by the // [Handle]. Note that the original [Handle] is not consumed by this operation, // and can be opened multiple times. // // The handle returned is only usable for reading, and this is method is // shorthand for [Handle.OpenFile] with os.O_RDONLY. // // TODO: Rename these to "Reopen" or something. func (h *Handle) Open() (*os.File, error) { return h.OpenFile(os.O_RDONLY) } // OpenFile creates an "upgraded" file handle to the file referenced by the // [Handle]. Note that the original [Handle] is not consumed by this operation, // and can be opened multiple times. // // The provided flags indicate which open(2) flags are used to create the new // handle. // // TODO: Rename these to "Reopen" or something. func (h *Handle) OpenFile(flags int) (*os.File, error) { return fdutils.WithFileFd(h.inner, func(fd uintptr) (*os.File, error) { newFd, err := libpathrs.Reopen(fd, flags) if err != nil { return nil, err } return os.NewFile(newFd, h.inner.Name()), nil }) } // IntoFile unwraps the [Handle] into its underlying [os.File]. // // You almost certainly want to use [Handle.OpenFile] to get a non-O_PATH // version of this [Handle]. // // This operation returns the internal [os.File] of the [Handle] directly, so // calling [Handle.Close] will also close any copies of the returned [os.File]. // If you want to get an independent copy, use [Handle.Clone] followed by // [Handle.IntoFile] on the cloned [Handle]. // // [os.File]: https://pkg.go.dev/os#File func (h *Handle) IntoFile() *os.File { // TODO: Figure out if we really don't want to make a copy. // TODO: We almost certainly want to clear r.inner here, but we can't do // that easily atomically (we could use atomic.Value but that'll make // things quite a bit uglier). return h.inner } // Clone creates a copy of a [Handle], such that it has a separate lifetime to // the original (while referring to the same underlying file). func (h *Handle) Clone() (*Handle, error) { return HandleFromFile(h.inner) } // Close frees all of the resources used by the [Handle]. func (h *Handle) Close() error { return h.inner.Close() } pathrs-0.2.1/go-pathrs/internal/fdutils/fd_linux.go000064400000000000000000000042531046102023000204620ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package fdutils contains a few helper methods when dealing with *os.File and // file descriptors. package fdutils import ( "fmt" "os" "golang.org/x/sys/unix" "cyphar.com/go-pathrs/internal/libpathrs" ) // DupFd makes a duplicate of the given fd. func DupFd(fd uintptr, name string) (*os.File, error) { newFd, err := unix.FcntlInt(fd, unix.F_DUPFD_CLOEXEC, 0) if err != nil { return nil, fmt.Errorf("fcntl(F_DUPFD_CLOEXEC): %w", err) } return os.NewFile(uintptr(newFd), name), nil } // WithFileFd is a more ergonomic wrapper around file.SyscallConn().Control(). func WithFileFd[T any](file *os.File, fn func(fd uintptr) (T, error)) (T, error) { conn, err := file.SyscallConn() if err != nil { return *new(T), err } var ( ret T innerErr error ) if err := conn.Control(func(fd uintptr) { ret, innerErr = fn(fd) }); err != nil { return *new(T), err } return ret, innerErr } // DupFile makes a duplicate of the given file. func DupFile(file *os.File) (*os.File, error) { return WithFileFd(file, func(fd uintptr) (*os.File, error) { return DupFd(fd, file.Name()) }) } // MkFile creates a new *os.File from the provided file descriptor. However, // unlike os.NewFile, the file's Name is based on the real path (provided by // /proc/self/fd/$n). func MkFile(fd uintptr) (*os.File, error) { fdPath := fmt.Sprintf("fd/%d", fd) fdName, err := libpathrs.ProcReadlinkat(libpathrs.ProcDefaultRootFd, libpathrs.ProcThreadSelf, fdPath) if err != nil { _ = unix.Close(int(fd)) return nil, fmt.Errorf("failed to fetch real name of fd %d: %w", fd, err) } // TODO: Maybe we should prefix this name with something to indicate to // users that they must not use this path as a "safe" path. Something like // "//pathrs-handle:/foo/bar"? return os.NewFile(fd, fdName), nil } pathrs-0.2.1/go-pathrs/internal/libpathrs/error_unix.go000064400000000000000000000016551046102023000213670ustar 00000000000000//go:build linux // TODO: Use "go:build unix" once we bump the minimum Go version 1.19. // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package libpathrs import ( "syscall" ) // Error represents an underlying libpathrs error. type Error struct { description string errno syscall.Errno } // Error returns a textual description of the error. func (err *Error) Error() string { return err.description } // Unwrap returns the underlying error which was wrapped by this error (if // applicable). func (err *Error) Unwrap() error { if err.errno != 0 { return err.errno } return nil } pathrs-0.2.1/go-pathrs/internal/libpathrs/libpathrs_linux.go000064400000000000000000000252251046102023000224010ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package libpathrs is an internal thin wrapper around the libpathrs C API. package libpathrs import ( "fmt" "syscall" "unsafe" ) /* // TODO: Figure out if we need to add support for linking against libpathrs // statically even if in dynamically linked builds in order to make // packaging a bit easier (using "-Wl,-Bstatic -lpathrs -Wl,-Bdynamic" or // "-l:pathrs.a"). #cgo pkg-config: pathrs #include // This is a workaround for unsafe.Pointer() not working for non-void pointers. char *cast_ptr(void *ptr) { return ptr; } */ import "C" func fetchError(errID C.int) error { if errID >= C.__PATHRS_MAX_ERR_VALUE { return nil } cErr := C.pathrs_errorinfo(errID) defer C.pathrs_errorinfo_free(cErr) var err error if cErr != nil { err = &Error{ errno: syscall.Errno(cErr.saved_errno), description: C.GoString(cErr.description), } } return err } // OpenRoot wraps pathrs_open_root. func OpenRoot(path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_open_root(cPath) return uintptr(fd), fetchError(fd) } // Reopen wraps pathrs_reopen. func Reopen(fd uintptr, flags int) (uintptr, error) { newFd := C.pathrs_reopen(C.int(fd), C.int(flags)) return uintptr(newFd), fetchError(newFd) } // InRootResolve wraps pathrs_inroot_resolve. func InRootResolve(rootFd uintptr, path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_resolve(C.int(rootFd), cPath) return uintptr(fd), fetchError(fd) } // InRootResolveNoFollow wraps pathrs_inroot_resolve_nofollow. func InRootResolveNoFollow(rootFd uintptr, path string) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_resolve_nofollow(C.int(rootFd), cPath) return uintptr(fd), fetchError(fd) } // InRootOpen wraps pathrs_inroot_open. func InRootOpen(rootFd uintptr, path string, flags int) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_open(C.int(rootFd), cPath, C.int(flags)) return uintptr(fd), fetchError(fd) } // InRootReadlink wraps pathrs_inroot_readlink. func InRootReadlink(rootFd uintptr, path string) (string, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) size := 128 for { linkBuf := make([]byte, size) n := C.pathrs_inroot_readlink(C.int(rootFd), cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf))) switch { case int(n) < C.__PATHRS_MAX_ERR_VALUE: return "", fetchError(n) case int(n) <= len(linkBuf): return string(linkBuf[:int(n)]), nil default: // The contents were truncated. Unlike readlinkat, pathrs returns // the size of the link when it checked. So use the returned size // as a basis for the reallocated size (but in order to avoid a DoS // where a magic-link is growing by a single byte each iteration, // make sure we are a fair bit larger). size += int(n) } } } // InRootRmdir wraps pathrs_inroot_rmdir. func InRootRmdir(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_rmdir(C.int(rootFd), cPath) return fetchError(err) } // InRootUnlink wraps pathrs_inroot_unlink. func InRootUnlink(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_unlink(C.int(rootFd), cPath) return fetchError(err) } // InRootRemoveAll wraps pathrs_inroot_remove_all. func InRootRemoveAll(rootFd uintptr, path string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_remove_all(C.int(rootFd), cPath) return fetchError(err) } // InRootCreat wraps pathrs_inroot_creat. func InRootCreat(rootFd uintptr, path string, flags int, mode uint32) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_creat(C.int(rootFd), cPath, C.int(flags), C.uint(mode)) return uintptr(fd), fetchError(fd) } // InRootRename wraps pathrs_inroot_rename. func InRootRename(rootFd uintptr, src, dst string, flags uint) error { cSrc := C.CString(src) defer C.free(unsafe.Pointer(cSrc)) cDst := C.CString(dst) defer C.free(unsafe.Pointer(cDst)) err := C.pathrs_inroot_rename(C.int(rootFd), cSrc, cDst, C.uint(flags)) return fetchError(err) } // InRootMkdir wraps pathrs_inroot_mkdir. func InRootMkdir(rootFd uintptr, path string, mode uint32) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_mkdir(C.int(rootFd), cPath, C.uint(mode)) return fetchError(err) } // InRootMkdirAll wraps pathrs_inroot_mkdir_all. func InRootMkdirAll(rootFd uintptr, path string, mode uint32) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_inroot_mkdir_all(C.int(rootFd), cPath, C.uint(mode)) return uintptr(fd), fetchError(fd) } // InRootMknod wraps pathrs_inroot_mknod. func InRootMknod(rootFd uintptr, path string, mode uint32, dev uint64) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) err := C.pathrs_inroot_mknod(C.int(rootFd), cPath, C.uint(mode), C.dev_t(dev)) return fetchError(err) } // InRootSymlink wraps pathrs_inroot_symlink. func InRootSymlink(rootFd uintptr, path, target string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) cTarget := C.CString(target) defer C.free(unsafe.Pointer(cTarget)) err := C.pathrs_inroot_symlink(C.int(rootFd), cPath, cTarget) return fetchError(err) } // InRootHardlink wraps pathrs_inroot_hardlink. func InRootHardlink(rootFd uintptr, path, target string) error { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) cTarget := C.CString(target) defer C.free(unsafe.Pointer(cTarget)) err := C.pathrs_inroot_hardlink(C.int(rootFd), cPath, cTarget) return fetchError(err) } // ProcBase is pathrs_proc_base_t (uint64_t). type ProcBase C.pathrs_proc_base_t // FIXME: We need to open-code the constants because CGo unfortunately will // implicitly convert any non-literal constants (i.e. those resolved using gcc) // to signed integers. See for some // more information on the underlying issue (though. const ( // ProcRoot is PATHRS_PROC_ROOT. ProcRoot ProcBase = 0xFFFF_FFFE_7072_6F63 // C.PATHRS_PROC_ROOT // ProcSelf is PATHRS_PROC_SELF. ProcSelf ProcBase = 0xFFFF_FFFE_091D_5E1F // C.PATHRS_PROC_SELF // ProcThreadSelf is PATHRS_PROC_THREAD_SELF. ProcThreadSelf ProcBase = 0xFFFF_FFFE_3EAD_5E1F // C.PATHRS_PROC_THREAD_SELF // ProcBaseTypeMask is __PATHRS_PROC_TYPE_MASK. ProcBaseTypeMask ProcBase = 0xFFFF_FFFF_0000_0000 // C.__PATHRS_PROC_TYPE_MASK // ProcBaseTypePid is __PATHRS_PROC_TYPE_PID. ProcBaseTypePid ProcBase = 0x8000_0000_0000_0000 // C.__PATHRS_PROC_TYPE_PID // ProcDefaultRootFd is PATHRS_PROC_DEFAULT_ROOTFD. ProcDefaultRootFd = -int(syscall.EBADF) // C.PATHRS_PROC_DEFAULT_ROOTFD ) func assertEqual[T comparable](a, b T, msg string) { if a != b { panic(fmt.Sprintf("%s ((%T) %#v != (%T) %#v)", msg, a, a, b, b)) } } // Verify that the values above match the actual C values. Unfortunately, Go // only allows us to forcefully cast int64 to uint64 if you use a temporary // variable, which means we cannot do it in a const context and thus need to do // it at runtime (even though it is a check that fundamentally could be done at // compile-time)... func init() { var ( actualProcRoot int64 = C.PATHRS_PROC_ROOT actualProcSelf int64 = C.PATHRS_PROC_SELF actualProcThreadSelf int64 = C.PATHRS_PROC_THREAD_SELF ) assertEqual(ProcRoot, ProcBase(actualProcRoot), "PATHRS_PROC_ROOT") assertEqual(ProcSelf, ProcBase(actualProcSelf), "PATHRS_PROC_SELF") assertEqual(ProcThreadSelf, ProcBase(actualProcThreadSelf), "PATHRS_PROC_THREAD_SELF") var ( actualProcBaseTypeMask uint64 = C.__PATHRS_PROC_TYPE_MASK actualProcBaseTypePid uint64 = C.__PATHRS_PROC_TYPE_PID ) assertEqual(ProcBaseTypeMask, ProcBase(actualProcBaseTypeMask), "__PATHRS_PROC_TYPE_MASK") assertEqual(ProcBaseTypePid, ProcBase(actualProcBaseTypePid), "__PATHRS_PROC_TYPE_PID") assertEqual(ProcDefaultRootFd, int(C.PATHRS_PROC_DEFAULT_ROOTFD), "PATHRS_PROC_DEFAULT_ROOTFD") } // ProcPid reimplements the PROC_PID(x) conversion. func ProcPid(pid uint32) ProcBase { return ProcBaseTypePid | ProcBase(pid) } // ProcOpenat wraps pathrs_proc_openat. func ProcOpenat(procRootFd int, base ProcBase, path string, flags int) (uintptr, error) { cBase := C.pathrs_proc_base_t(base) cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) fd := C.pathrs_proc_openat(C.int(procRootFd), cBase, cPath, C.int(flags)) return uintptr(fd), fetchError(fd) } // ProcReadlinkat wraps pathrs_proc_readlinkat. func ProcReadlinkat(procRootFd int, base ProcBase, path string) (string, error) { // TODO: See if we can unify this code with InRootReadlink. cBase := C.pathrs_proc_base_t(base) cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) size := 128 for { linkBuf := make([]byte, size) n := C.pathrs_proc_readlinkat( C.int(procRootFd), cBase, cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf))) switch { case int(n) < C.__PATHRS_MAX_ERR_VALUE: return "", fetchError(n) case int(n) <= len(linkBuf): return string(linkBuf[:int(n)]), nil default: // The contents were truncated. Unlike readlinkat, pathrs returns // the size of the link when it checked. So use the returned size // as a basis for the reallocated size (but in order to avoid a DoS // where a magic-link is growing by a single byte each iteration, // make sure we are a fair bit larger). size += int(n) } } } // ProcfsOpenHow is pathrs_procfs_open_how (struct). type ProcfsOpenHow C.pathrs_procfs_open_how const ( // ProcfsNewUnmasked is PATHRS_PROCFS_NEW_UNMASKED. ProcfsNewUnmasked = C.PATHRS_PROCFS_NEW_UNMASKED ) // Flags returns a pointer to the internal flags field to allow other packages // to modify structure fields that are internal due to Go's visibility model. func (how *ProcfsOpenHow) Flags() *C.uint64_t { return &how.flags } // ProcfsOpen is pathrs_procfs_open (sizeof(*how) is passed automatically). func ProcfsOpen(how *ProcfsOpenHow) (uintptr, error) { fd := C.pathrs_procfs_open((*C.pathrs_procfs_open_how)(how), C.size_t(unsafe.Sizeof(*how))) return uintptr(fd), fetchError(fd) } pathrs-0.2.1/go-pathrs/pathrs.h000064400000000000000000000771631046102023000145210ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif #define __CBINDGEN_ALIGNED(n) __attribute__((aligned(n))) #ifndef LIBPATHRS_H #define LIBPATHRS_H /* * WARNING: This file was auto-generated by rust-cbindgen. Don't modify it. * Instead, re-generate it with: * % cbindgen -c cbindgen.toml -o include/pathrs.h */ #include #include #include #include #include #include /* * Returns whether the given numerical value is a libpathrs error (which can be * passed to pathrs_errorinfo()). Users are recommended to use this instead of * a bare `<0` comparison because some functions may return a negative number * even in a success condition. */ #define IS_PATHRS_ERR(ret) ((ret) < __PATHRS_MAX_ERR_VALUE) /* * Used to construct pathrs_proc_base_t values for a PID (or TID). Passing * PATHRS_PROC_PID(pid) to pathrs_proc_*() as pathrs_proc_base_t will cause * libpathrs to use /proc/$pid as the base of the operation. * * This is essentially functionally equivalent to prefixing "$pid/" to the * subpath argument and using PATHRS_PROC_ROOT. * * Note that this operation is inherently racy -- the process referenced by this * PID may have died and the PID recycled with a different process. In * principle, this means that it is only really safe to use this with: * * - PID 1 (the init process), as that PID cannot ever get recycled. * - Your current PID (though you should just use PATHRS_PROC_SELF). * - Your current TID (though you should just use PATHRS_PROC_THREAD_SELF), * or _possibly_ other TIDs in your thread-group if you are absolutely sure * they have not been reaped (typically with pthread_join(3), though there * are other ways). * - PIDs of child processes (as long as you are sure that no other part of * your program incorrectly catches or ignores SIGCHLD, and that you do it * *before* you call wait(2) or any equivalent method that could reap * zombies). * * Outside of those specific uses, users should probably avoid using this. */ #define PATHRS_PROC_PID(n) (__PATHRS_PROC_TYPE_PID | (n)) /* * A sentinel value to tell `pathrs_proc_*` methods to use the default procfs * root handle (which may be globally cached). */ #define PATHRS_PROC_DEFAULT_ROOTFD -9 /* (-EBADF) */ /** * Bits in `pathrs_proc_base_t` that indicate the type of the base value. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_PROC_TYPE_MASK 18446744069414584320ull /** * Bits in `pathrs_proc_base_t` that must be set for `/proc/$pid` values. Don't * use this directly, instead use `PATHRS_PROC_PID(n)` to convert a PID to an * appropriate `pathrs_proc_base_t` value. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_PROC_TYPE_PID 9223372036854775808ull /** * Construct a completely unmasked procfs handle. * * This is equivalent to [`ProcfsHandleBuilder::unmasked`], and is meant as * a flag argument to [`ProcfsOpenFlags`] (the `flags` field in `struct * pathrs_procfs_open_how`) for use with pathrs_procfs_open(). */ #define PATHRS_PROCFS_NEW_UNMASKED 1 /** * Indicate what base directory should be used when doing operations with * `pathrs_proc_*`. In addition to the values defined here, the following * macros can be used for other values: * * * `PATHRS_PROC_PID(pid)` refers to the `/proc/` directory for the * process with PID (or TID) `pid`. * * Note that this operation is inherently racy and should probably avoided * for most uses -- see the block comment above `PATHRS_PROC_PID(n)` for * more details. * * Unknown values will result in an error being returned. */ enum pathrs_proc_base_t { /** * Use /proc. Note that this mode may be more expensive because we have * to take steps to try to avoid leaking unmasked procfs handles, so you * should use PATHRS_PROC_SELF if you can. */ PATHRS_PROC_ROOT = 18446744067006164835ull, /** * Use /proc/self. For most programs, this is the standard choice. */ PATHRS_PROC_SELF = 18446744065272536607ull, /** * Use /proc/thread-self. In multi-threaded programs where one thread has a * different CLONE_FS, it is possible for /proc/self to point the wrong * thread and so /proc/thread-self may be necessary. * * NOTE: Using /proc/thread-self may require care if used from languages * where your code can change threads without warning and old threads can * be killed (such as Go -- where you want to use runtime.LockOSThread). */ PATHRS_PROC_THREAD_SELF = 18446744066171166239ull, }; typedef uint64_t pathrs_proc_base_t; typedef struct { uint64_t flags; } pathrs_procfs_open_how; /** * Attempts to represent a Rust Error type in C. This structure must be freed * using pathrs_errorinfo_free(). */ typedef struct __CBINDGEN_ALIGNED(8) { /** * Raw errno(3) value of the underlying error (or 0 if the source of the * error was not due to a syscall error). */ uint64_t saved_errno; /** * Textual description of the error. */ const char *description; } pathrs_error_t; /** * The smallest return value which cannot be a libpathrs error ID. * * While all libpathrs error IDs are negative numbers, some functions may * return a negative number in a success scenario. This macro defines the high * range end of the numbers that can be used as an error ID. Don't use this * value directly, instead use `IS_PATHRS_ERR(ret)` to check if a returned * value is an error or not. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_MAX_ERR_VALUE -4096 /** * Open a root handle. * * The provided path must be an existing directory. * * Note that root handles are not special -- this function is effectively * equivalent to * * ```c * fd = open(path, O_PATH|O_DIRECTORY); * ``` * * # Return Value * * On success, this function returns a file descriptor that can be used as a * root handle in subsequent pathrs_inroot_* operations. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_open_root(const char *path); /** * "Upgrade" an O_PATH file descriptor to a usable fd, suitable for reading and * writing. This does not consume the original file descriptor. (This can be * used with non-O_PATH file descriptors as well.) * * It should be noted that the use of O_CREAT *is not* supported (and will * result in an error). Handles only refer to *existing* files. Instead you * need to use pathrs_inroot_creat(). * * In addition, O_NOCTTY is automatically set when opening the path. If you * want to use the path as a controlling terminal, you will have to do * ioctl(fd, TIOCSCTTY, 0) yourself. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_reopen(int fd, int flags); /** * Resolve the given path within the rootfs referenced by root_fd. The path * *must already exist*, otherwise an error will occur. * * All symlinks (including trailing symlinks) are followed, but they are * resolved within the rootfs. If you wish to open a handle to the symlink * itself, use pathrs_inroot_resolve_nofollow(). * * # Return Value * * On success, this function returns an O_PATH file descriptor referencing the * resolved path. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_resolve(int root_fd, const char *path); /** * pathrs_inroot_resolve_nofollow() is effectively an O_NOFOLLOW version of * pathrs_inroot_resolve(). Their behaviour is identical, except that * *trailing* symlinks will not be followed. If the final component is a * trailing symlink, an O_PATH|O_NOFOLLOW handle to the symlink itself is * returned. * * # Return Value * * On success, this function returns an O_PATH file descriptor referencing the * resolved path. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_resolve_nofollow(int root_fd, const char *path); /** * pathrs_inroot_open() is effectively shorthand for pathrs_inroot_resolve() * followed by pathrs_reopen(). If you only need to open a path and don't care * about re-opening it later, this can be slightly more efficient than the * alternative for the openat2-based resolver as it doesn't require allocating * an extra file descriptor. For languages where C FFI is expensive (such as * Go), using this also saves a function call. * * If flags contains O_NOFOLLOW, the behaviour is like that of * pathrs_inroot_resolve_nofollow() followed by pathrs_reopen(). * * In addition, O_NOCTTY is automatically set when opening the path. If you * want to use the path as a controlling terminal, you will have to do * ioctl(fd, TIOCSCTTY, 0) yourself. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_open(int root_fd, const char *path, int flags); /** * Get the target of a symlink within the rootfs referenced by root_fd. * * NOTE: The returned path is not modified to be "safe" outside of the * root. You should not use this path for doing further path lookups -- use * pathrs_inroot_resolve() instead. * * This method is just shorthand for: * * ```c * int linkfd = pathrs_inroot_resolve_nofollow(rootfd, path); * if (IS_PATHRS_ERR(linkfd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * copied = readlinkat(linkfd, "", linkbuf, linkbuf_size); * close(linkfd); * ``` * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_inroot_readlink() will return *the * number of bytes it would have copied if the buffer was large enough*. This * matches the behaviour of pathrs_proc_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_readlink(int root_fd, const char *path, char *linkbuf, size_t linkbuf_size); /** * Rename a path within the rootfs referenced by root_fd. The flags argument is * identical to the renameat2(2) flags that are supported on the system. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_rename(int root_fd, const char *src, const char *dst, uint32_t flags); /** * Remove the empty directory at path within the rootfs referenced by root_fd. * * The semantics are effectively equivalent to unlinkat(..., AT_REMOVEDIR). * This function will return an error if the path doesn't exist, was not a * directory, or was a non-empty directory. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_rmdir(int root_fd, const char *path); /** * Remove the file (a non-directory inode) at path within the rootfs referenced * by root_fd. * * The semantics are effectively equivalent to unlinkat(..., 0). This function * will return an error if the path doesn't exist or was a directory. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_unlink(int root_fd, const char *path); /** * Recursively delete the path and any children it contains if it is a * directory. The semantics are equivalent to `rm -r`. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_remove_all(int root_fd, const char *path); /** * Create a new regular file within the rootfs referenced by root_fd. This is * effectively an O_CREAT operation, and so (unlike pathrs_inroot_resolve()), * this function can be used on non-existent paths. * * If you want to ensure the creation is a new file, use O_EXCL. * * If you want to create a file without opening a handle to it, you can do * pathrs_inroot_mknod(root_fd, path, S_IFREG|mode, 0) instead. * * As with pathrs_reopen(), O_NOCTTY is automatically set when opening the * path. If you want to use the path as a controlling terminal, you will have * to do ioctl(fd, TIOCSCTTY, 0) yourself. * * NOTE: Unlike O_CREAT, pathrs_inroot_creat() will return an error if the * final component is a dangling symlink. O_CREAT will create such files, and * while openat2 does support this it would be difficult to implement this in * the emulated resolver. * * # Return Value * * On success, this function returns a file descriptor to the requested file. * The open flags are based on the provided flags. The file descriptor will * have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_creat(int root_fd, const char *path, int flags, unsigned int mode); /** * Create a new directory within the rootfs referenced by root_fd. * * This is shorthand for pathrs_inroot_mknod(root_fd, path, S_IFDIR|mode, 0). * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mkdir(int root_fd, const char *path, unsigned int mode); /** * Create a new directory (and any of its path components if they don't exist) * within the rootfs referenced by root_fd. * * # Return Value * * On success, this function returns an O_DIRECTORY file descriptor to the * newly created directory. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mkdir_all(int root_fd, const char *path, unsigned int mode); /** * Create a inode within the rootfs referenced by root_fd. The type of inode to * be created is configured using the S_IFMT bits in mode (a-la mknod(2)). * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mknod(int root_fd, const char *path, unsigned int mode, dev_t dev); /** * Create a symlink within the rootfs referenced by root_fd. Note that the * symlink target string is not modified when creating the symlink. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_symlink(int root_fd, const char *path, const char *target); /** * Create a hardlink within the rootfs referenced by root_fd. Both the hardlink * path and target are resolved within the rootfs. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_hardlink(int root_fd, const char *path, const char *target); /** * Create a new (custom) procfs root handle. * * This is effectively a C wrapper around [`ProcfsHandleBuilder`], allowing you * to create a custom procfs root handle that can be used with other * `pathrs_proc_*at` methods. * * While most users should just use `PATHRS_PROC_DEFAULT_ROOTFD` (or the * non-`at` variants of `pathrs_proc_*`), creating an unmasked procfs root * handle (using `PATHRS_PROCFS_NEW_UNMASKED`) can be useful for programs that * need to operate on a lot of global procfs files. (Note that accessing global * procfs files does not *require* creating a custom procfs handle -- * `pathrs_proc_*` will automatically create a global-friendly handle * internally when necessary but will close it immediately after operating on * it.) * * # Extensible Structs * * The [`ProcfsOpenHow`] (`struct pathrs_procfs_open_how`) argument is * designed to be extensible, modelled after the extensible structs scheme used * by Linux (for syscalls such as [clone3(2)], [openat2(2)] and other such * syscalls). Normally one would use symbol versioning to achieve this, but * unfortunately Rust's symbol versioning support is incredibly primitive (one * might even say "non-existent") and so this system is more robust, even if * the calling convention is a little strange for userspace libraries. * * In addition to a pointer argument, the caller must also provide the size of * the structure it is passing. By providing this information, it is possible * for `pathrs_procfs_open()` to provide both forwards- and * backwards-compatibility, with size acting as an implicit version number. * (Because new extension fields will always be appended, the structure size * will always increase.) * * If we let `usize` be the structure specified by the caller, and `lsize` be * the size of the structure internal to libpathrs, then there are three cases * to consider: * * * If `usize == lsize`, then there is no version mismatch and the structure * provided by the caller can be used verbatim. * * If `usize < lsize`, then there are some extension fields which libpathrs * supports that the caller does not. Because a zero value in any added * extension field signifies a no-op, libpathrs treats all of the extension * fields not provided by the caller as having zero values. This provides * backwards-compatibility. * * If `usize > lsize`, then there are some extension fields which the caller * is aware of but this version of libpathrs does not support. Because any * extension field must have its zero values signify a no-op, libpathrs can * safely ignore the unsupported extension fields if they are all-zero. If * any unsupported extension fields are nonzero, then an `E2BIG` error is * returned. This provides forwards-compatibility. * * Because the definition of `struct pathrs_procfs_open_how` may open in the * future * * Because the definition of `struct pathrs_procfs_open_how` may change in the * future (with new fields being added when headers are updated), callers * should zero-fill the structure to ensure that recompiling the program with * new headers will not result in spurious errors at run time. The simplest * way is to use a designated initialiser: * * ```c * struct pathrs_procfs_open_how how = { * .flags = PATHRS_PROCFS_NEW_UNMASKED, * }; * ``` * * or explicitly using `memset(3)` or similar: * * ```c * struct pathrs_procfs_open_how how; * memset(&how, 0, sizeof(how)); * how.flags = PATHRS_PROCFS_NEW_UNMASKED; * ``` * * # Return Value * * On success, this function returns *either* a file descriptor *or* * `PATHRS_PROC_DEFAULT_ROOTFD` (this is a negative number, equal to `-EBADF`). * The file descriptor will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). * * [clone3(2)]: https://www.man7.org/linux/man-pages/man2/clone3.2.html * [openat2(2)]: https://www.man7.org/linux/man-pages/man2/openat2.2.html */ int pathrs_procfs_open(const pathrs_procfs_open_how *args, size_t size); /** * `pathrs_proc_open` but with a caller-provided file descriptor for `/proc`. * * Internally, `pathrs_proc_open` will attempt to use a cached copy of a very * restricted `/proc` handle (a detached mount object with `subset=pid` and * `hidepid=4`). If a user requests a global `/proc` file, a temporary handle * capable of accessing global files is created and destroyed after the * operation completes. * * For most users, this is more than sufficient. However, if a user needs to * operate on many global `/proc` files, the cost of creating handles can get * quite expensive. `pathrs_proc_openat` allows a user to manually manage the * global-friendly `/proc` handle. Note that passing a `subset=pid` file * descriptor to `pathrs_proc_openat` will *not* stop the automatic creation of * a global-friendly handle internally if necessary. * * In order to get the behaviour of `pathrs_proc_open`, you can pass the * special value `PATHRS_PROC_DEFAULT_ROOTFD` (`-EBADF`) as the `proc_rootfd` * argument. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_openat(int proc_rootfd, pathrs_proc_base_t base, const char *path, int flags); /** * Safely open a path inside a `/proc` handle. * * Any bind-mounts or other over-mounts will (depending on what kernel features * are available) be detected and an error will be returned. Non-trailing * symlinks are followed but care is taken to ensure the symlinks are * legitimate. * * Unless you intend to open a magic-link, `O_NOFOLLOW` should be set in flags. * Lookups with `O_NOFOLLOW` are guaranteed to never be tricked by bind-mounts * (on new enough Linux kernels). * * If you wish to resolve a magic-link, you need to unset `O_NOFOLLOW`. * Unfortunately (if libpathrs is using the regular host `/proc` mount), this * lookup mode cannot protect you against an attacker that can modify the mount * table during this operation. * * NOTE: Instead of using paths like `/proc/thread-self/fd`, `base` is used to * indicate what "base path" inside procfs is used. For example, to re-open a * file descriptor: * * ```c * fd = pathrs_proc_open(PATHRS_PROC_THREAD_SELF, "fd/101", O_RDWR); * if (IS_PATHRS_ERR(fd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * ``` * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_open(pathrs_proc_base_t base, const char *path, int flags); /** * `pathrs_proc_readlink` but with a caller-provided file descriptor for * `/proc`. * * See the documentation of pathrs_proc_openat() for when this API might be * useful. * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_proc_readlink() will return *the number * of bytes it would have copied if the buffer was large enough*. This matches * the behaviour of pathrs_inroot_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_readlinkat(int proc_rootfd, pathrs_proc_base_t base, const char *path, char *linkbuf, size_t linkbuf_size); /** * Safely read the contents of a symlink inside `/proc`. * * As with `pathrs_proc_open`, any bind-mounts or other over-mounts will * (depending on what kernel features are available) be detected and an error * will be returned. Non-trailing symlinks are followed but care is taken to * ensure the symlinks are legitimate. * * This function is effectively shorthand for * * ```c * fd = pathrs_proc_open(base, path, O_PATH|O_NOFOLLOW); * if (IS_PATHRS_ERR(fd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * copied = readlinkat(fd, "", linkbuf, linkbuf_size); * close(fd); * ``` * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_proc_readlink() will return *the number * of bytes it would have copied if the buffer was large enough*. This matches * the behaviour of pathrs_inroot_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_readlink(pathrs_proc_base_t base, const char *path, char *linkbuf, size_t linkbuf_size); /** * Retrieve error information about an error id returned by a pathrs operation. * * Whenever an error occurs with libpathrs, a negative number describing that * error (the error id) is returned. pathrs_errorinfo() is used to retrieve * that information: * * ```c * fd = pathrs_inroot_resolve(root, "/foo/bar"); * if (IS_PATHRS_ERR(fd)) { * // fd is an error id * pathrs_error_t *error = pathrs_errorinfo(fd); * // ... print the error information ... * pathrs_errorinfo_free(error); * } * ``` * * Once pathrs_errorinfo() is called for a particular error id, that error id * is no longer valid and should not be used for subsequent pathrs_errorinfo() * calls. * * Error ids are only unique from one another until pathrs_errorinfo() is * called, at which point the id can be reused for subsequent errors. The * precise format of error ids is completely opaque and they should never be * compared directly or used for anything other than with pathrs_errorinfo(). * * Error ids are not thread-specific and thus pathrs_errorinfo() can be called * on a different thread to the thread where the operation failed (this is of * particular note to green-thread language bindings like Go, where this is * important). * * # Return Value * * If there was a saved error with the provided id, a pathrs_error_t is * returned describing the error. Use pathrs_errorinfo_free() to free the * associated memory once you are done with the error. */ pathrs_error_t *pathrs_errorinfo(int err_id); /** * Free the pathrs_error_t object returned by pathrs_errorinfo(). */ void pathrs_errorinfo_free(pathrs_error_t *ptr); #endif /* LIBPATHRS_H */ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif pathrs-0.2.1/go-pathrs/procfs/procfs_linux.go000064400000000000000000000215241046102023000173730ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ // Package procfs provides a safe API for operating on /proc on Linux. package procfs import ( "os" "runtime" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // ProcBase is used with [ProcReadlink] and related functions to indicate what // /proc subpath path operations should be done relative to. type ProcBase struct { inner libpathrs.ProcBase } var ( // ProcRoot indicates to use /proc. Note that this mode may be more // expensive because we have to take steps to try to avoid leaking unmasked // procfs handles, so you should use [ProcBaseSelf] if you can. ProcRoot = ProcBase{inner: libpathrs.ProcRoot} // ProcSelf indicates to use /proc/self. For most programs, this is the // standard choice. ProcSelf = ProcBase{inner: libpathrs.ProcSelf} // ProcThreadSelf indicates to use /proc/thread-self. In multi-threaded // programs where one thread has a different CLONE_FS, it is possible for // /proc/self to point the wrong thread and so /proc/thread-self may be // necessary. ProcThreadSelf = ProcBase{inner: libpathrs.ProcThreadSelf} ) // ProcPid returns a ProcBase which indicates to use /proc/$pid for the given // PID (or TID). Be aware that due to PID recycling, using this is generally // not safe except in certain circumstances. Namely: // // - PID 1 (the init process), as that PID cannot ever get recycled. // - Your current PID (though you should just use [ProcBaseSelf]). // - Your current TID if you have used [runtime.LockOSThread] (though you // should just use [ProcBaseThreadSelf]). // - PIDs of child processes (as long as you are sure that no other part of // your program incorrectly catches or ignores SIGCHLD, and that you do it // *before* you call wait(2)or any equivalent method that could reap // zombies). func ProcPid(pid int) ProcBase { if pid < 0 || pid >= 1<<31 { panic("invalid ProcBasePid value") // TODO: should this be an error? } return ProcBase{inner: libpathrs.ProcPid(uint32(pid))} } // ThreadCloser is a callback that needs to be called when you are done // operating on an [os.File] fetched using [Handle.OpenThreadSelf]. // // [os.File]: https://pkg.go.dev/os#File type ThreadCloser func() // Handle is a wrapper around an *os.File handle to "/proc", which can be // used to do further procfs-related operations in a safe way. type Handle struct { inner *os.File } // Close releases all internal resources for this [Handle]. // // Note that if the handle is actually the global cached handle, this operation // is a no-op. func (proc *Handle) Close() error { var err error if proc.inner != nil { err = proc.inner.Close() } return err } // OpenOption is a configuration function passed as an argument to [Open]. type OpenOption func(*libpathrs.ProcfsOpenHow) error // UnmaskedProcRoot can be passed to [Open] to request an unmasked procfs // handle be created. // // procfs, err := procfs.OpenRoot(procfs.UnmaskedProcRoot) func UnmaskedProcRoot(how *libpathrs.ProcfsOpenHow) error { *how.Flags() |= libpathrs.ProcfsNewUnmasked return nil } // Open creates a new [Handle] to a safe "/proc", based on the passed // configuration options (in the form of a series of [OpenOption]s). func Open(opts ...OpenOption) (*Handle, error) { var how libpathrs.ProcfsOpenHow for _, opt := range opts { if err := opt(&how); err != nil { return nil, err } } fd, err := libpathrs.ProcfsOpen(&how) if err != nil { return nil, err } var procFile *os.File if int(fd) >= 0 { procFile = os.NewFile(fd, "/proc") } // TODO: Check that fd == PATHRS_PROC_DEFAULT_ROOTFD in the <0 case? return &Handle{inner: procFile}, nil } // TODO: Switch to something fdutils.WithFileFd-like. func (proc *Handle) fd() int { if proc.inner != nil { return int(proc.inner.Fd()) } return libpathrs.ProcDefaultRootFd } // TODO: Should we expose open? func (proc *Handle) open(base ProcBase, path string, flags int) (_ *os.File, Closer ThreadCloser, Err error) { var closer ThreadCloser if base == ProcThreadSelf { runtime.LockOSThread() closer = runtime.UnlockOSThread } defer func() { if closer != nil && Err != nil { closer() Closer = nil } }() fd, err := libpathrs.ProcOpenat(proc.fd(), base.inner, path, flags) if err != nil { return nil, nil, err } file, err := fdutils.MkFile(fd) return file, closer, err } // OpenRoot safely opens a given path from inside /proc/. // // This function must only be used for accessing global information from procfs // (such as /proc/cpuinfo) or information about other processes (such as // /proc/1). Accessing your own process information should be done using // [Handle.OpenSelf] or [Handle.OpenThreadSelf]. func (proc *Handle) OpenRoot(path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcRoot, path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcRoot)") } return file, err } // OpenSelf safely opens a given path from inside /proc/self/. // // This method is recommend for getting process information about the current // process for almost all Go processes *except* for cases where there are // [runtime.LockOSThread] threads that have changed some aspect of their state // (such as through unshare(CLONE_FS) or changing namespaces). // // For such non-heterogeneous processes, /proc/self may reference to a task // that has different state from the current goroutine and so it may be // preferable to use [Handle.OpenThreadSelf]. The same is true if a user // really wants to inspect the current OS thread's information (such as // /proc/thread-self/stack or /proc/thread-self/status which is always uniquely // per-thread). // // Unlike [Handle.OpenThreadSelf], this method does not involve locking // the goroutine to the current OS thread and so is simpler to use and // theoretically has slightly less overhead. // // [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread func (proc *Handle) OpenSelf(path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcSelf, path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcSelf)") } return file, err } // OpenPid safely opens a given path from inside /proc/$pid/, where pid can be // either a PID or TID. // // This is effectively equivalent to calling [Handle.OpenRoot] with the // pid prefixed to the subpath. // // Be aware that due to PID recycling, using this is generally not safe except // in certain circumstances. See the documentation of [ProcPid] for more // details. func (proc *Handle) OpenPid(pid int, path string, flags int) (*os.File, error) { file, closer, err := proc.open(ProcPid(pid), path, flags) if closer != nil { // should not happen panic("non-zero closer returned from procOpen(ProcPidOpen)") } return file, err } // OpenThreadSelf safely opens a given path from inside /proc/thread-self/. // // Most Go processes have heterogeneous threads (all threads have most of the // same kernel state such as CLONE_FS) and so [Handle.OpenSelf] is // preferable for most users. // // For non-heterogeneous threads, or users that actually want thread-specific // information (such as /proc/thread-self/stack or /proc/thread-self/status), // this method is necessary. // // Because Go can change the running OS thread of your goroutine without notice // (and then subsequently kill the old thread), this method will lock the // current goroutine to the OS thread (with [runtime.LockOSThread]) and the // caller is responsible for unlocking the the OS thread with the // [ThreadCloser] callback once they are done using the returned file. This // callback MUST be called AFTER you have finished using the returned // [os.File]. This callback is completely separate to [os.File.Close], so it // must be called regardless of how you close the handle. // // [runtime.LockOSThread]: https://pkg.go.dev/runtime#LockOSThread // [os.File]: https://pkg.go.dev/os#File // [os.File.Close]: https://pkg.go.dev/os#File.Close func (proc *Handle) OpenThreadSelf(path string, flags int) (*os.File, ThreadCloser, error) { return proc.open(ProcThreadSelf, path, flags) } // Readlink safely reads the contents of a symlink from the given procfs base. // // This is effectively equivalent to doing an Open*(O_PATH|O_NOFOLLOW) of the // path and then doing unix.Readlinkat(fd, ""), but with the benefit that // thread locking is not necessary for [ProcThreadSelf]. func (proc *Handle) Readlink(base ProcBase, path string) (string, error) { return libpathrs.ProcReadlinkat(proc.fd(), base.inner, path) } pathrs-0.2.1/go-pathrs/root_linux.go000064400000000000000000000276731046102023000156010ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package pathrs import ( "errors" "fmt" "os" "syscall" "cyphar.com/go-pathrs/internal/fdutils" "cyphar.com/go-pathrs/internal/libpathrs" ) // Root is a handle to the root of a directory tree to resolve within. The only // purpose of this "root handle" is to perform operations within the directory // tree, or to get a [Handle] to inodes within the directory tree. // // At time of writing, it is considered a *VERY BAD IDEA* to open a [Root] // inside a possibly-attacker-controlled directory tree. While we do have // protections that should defend against it, it's far more dangerous than just // opening a directory tree which is not inside a potentially-untrusted // directory. type Root struct { inner *os.File } // OpenRoot creates a new [Root] handle to the directory at the given path. func OpenRoot(path string) (*Root, error) { fd, err := libpathrs.OpenRoot(path) if err != nil { return nil, err } file, err := fdutils.MkFile(fd) if err != nil { return nil, err } return &Root{inner: file}, nil } // RootFromFile creates a new [Root] handle from an [os.File] referencing a // directory. The provided file will be duplicated, so the original file should // still be closed by the caller. // // This is effectively the inverse operation of [Root.IntoFile]. // // [os.File]: https://pkg.go.dev/os#File func RootFromFile(file *os.File) (*Root, error) { newFile, err := fdutils.DupFile(file) if err != nil { return nil, fmt.Errorf("duplicate root fd: %w", err) } return &Root{inner: newFile}, nil } // Resolve resolves the given path within the [Root]'s directory tree, and // returns a [Handle] to the resolved path. The path must already exist, // otherwise an error will occur. // // All symlinks (including trailing symlinks) are followed, but they are // resolved within the rootfs. If you wish to open a handle to the symlink // itself, use [ResolveNoFollow]. func (r *Root) Resolve(path string) (*Handle, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootResolve(rootFd, path) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, nil }) } // ResolveNoFollow is effectively an O_NOFOLLOW version of [Resolve]. Their // behaviour is identical, except that *trailing* symlinks will not be // followed. If the final component is a trailing symlink, an O_PATH|O_NOFOLLOW // handle to the symlink itself is returned. func (r *Root) ResolveNoFollow(path string) (*Handle, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootResolveNoFollow(rootFd, path) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, nil }) } // Open is effectively shorthand for [Resolve] followed by [Handle.Open], but // can be slightly more efficient (it reduces CGo overhead and the number of // syscalls used when using the openat2-based resolver) and is arguably more // ergonomic to use. // // This is effectively equivalent to [os.Open]. // // [os.Open]: https://pkg.go.dev/os#Open func (r *Root) Open(path string) (*os.File, error) { return r.OpenFile(path, os.O_RDONLY) } // OpenFile is effectively shorthand for [Resolve] followed by // [Handle.OpenFile], but can be slightly more efficient (it reduces CGo // overhead and the number of syscalls used when using the openat2-based // resolver) and is arguably more ergonomic to use. // // However, if flags contains os.O_NOFOLLOW and the path is a symlink, then // OpenFile's behaviour will match that of openat2. In most cases an error will // be returned, but if os.O_PATH is provided along with os.O_NOFOLLOW then a // file equivalent to [ResolveNoFollow] will be returned instead. // // This is effectively equivalent to [os.OpenFile], except that os.O_CREAT is // not supported. // // [os.OpenFile]: https://pkg.go.dev/os#OpenFile func (r *Root) OpenFile(path string, flags int) (*os.File, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) { fd, err := libpathrs.InRootOpen(rootFd, path, flags) if err != nil { return nil, err } return fdutils.MkFile(fd) }) } // Create creates a file within the [Root]'s directory tree at the given path, // and returns a handle to the file. The provided mode is used for the new file // (the process's umask applies). // // Unlike [os.Create], if the file already exists an error is created rather // than the file being opened and truncated. // // [os.Create]: https://pkg.go.dev/os#Create func (r *Root) Create(path string, flags int, mode os.FileMode) (*os.File, error) { unixMode, err := toUnixMode(mode, false) if err != nil { return nil, err } return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*os.File, error) { handleFd, err := libpathrs.InRootCreat(rootFd, path, flags, unixMode) if err != nil { return nil, err } return fdutils.MkFile(handleFd) }) } // Rename two paths within a [Root]'s directory tree. The flags argument is // identical to the RENAME_* flags to the renameat2(2) system call. func (r *Root) Rename(src, dst string, flags uint) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRename(rootFd, src, dst, flags) return struct{}{}, err }) return err } // RemoveDir removes the named empty directory within a [Root]'s directory // tree. func (r *Root) RemoveDir(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRmdir(rootFd, path) return struct{}{}, err }) return err } // RemoveFile removes the named file within a [Root]'s directory tree. func (r *Root) RemoveFile(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootUnlink(rootFd, path) return struct{}{}, err }) return err } // Remove removes the named file or (empty) directory within a [Root]'s // directory tree. // // This is effectively equivalent to [os.Remove]. // // [os.Remove]: https://pkg.go.dev/os#Remove func (r *Root) Remove(path string) error { // In order to match os.Remove's implementation we need to also do both // syscalls unconditionally and adjust the error based on whether // pathrs_inroot_rmdir() returned ENOTDIR. unlinkErr := r.RemoveFile(path) if unlinkErr == nil { return nil } rmdirErr := r.RemoveDir(path) if rmdirErr == nil { return nil } // Both failed, adjust the error in the same way that os.Remove does. err := rmdirErr if errors.Is(err, syscall.ENOTDIR) { err = unlinkErr } return err } // RemoveAll recursively deletes a path and all of its children. // // This is effectively equivalent to [os.RemoveAll]. // // [os.RemoveAll]: https://pkg.go.dev/os#RemoveAll func (r *Root) RemoveAll(path string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootRemoveAll(rootFd, path) return struct{}{}, err }) return err } // Mkdir creates a directory within a [Root]'s directory tree. The provided // mode is used for the new directory (the process's umask applies). // // This is effectively equivalent to [os.Mkdir]. // // [os.Mkdir]: https://pkg.go.dev/os#Mkdir func (r *Root) Mkdir(path string, mode os.FileMode) error { unixMode, err := toUnixMode(mode, false) if err != nil { return err } _, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootMkdir(rootFd, path, unixMode) return struct{}{}, err }) return err } // MkdirAll creates a directory (and any parent path components if they don't // exist) within a [Root]'s directory tree. The provided mode is used for any // directories created by this function (the process's umask applies). // // This is effectively equivalent to [os.MkdirAll]. // // [os.MkdirAll]: https://pkg.go.dev/os#MkdirAll func (r *Root) MkdirAll(path string, mode os.FileMode) (*Handle, error) { unixMode, err := toUnixMode(mode, false) if err != nil { return nil, err } return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { handleFd, err := libpathrs.InRootMkdirAll(rootFd, path, unixMode) if err != nil { return nil, err } handleFile, err := fdutils.MkFile(handleFd) if err != nil { return nil, err } return &Handle{inner: handleFile}, err }) } // Mknod creates a new device inode of the given type within a [Root]'s // directory tree. The provided mode is used for the new directory (the // process's umask applies). // // This is effectively equivalent to [unix.Mknod]. // // [unix.Mknod]: https://pkg.go.dev/golang.org/x/sys/unix#Mknod func (r *Root) Mknod(path string, mode os.FileMode, dev uint64) error { unixMode, err := toUnixMode(mode, true) if err != nil { return err } _, err = fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootMknod(rootFd, path, unixMode, dev) return struct{}{}, err }) return err } // Symlink creates a symlink within a [Root]'s directory tree. The symlink is // created at path and is a link to target. // // This is effectively equivalent to [os.Symlink]. // // [os.Symlink]: https://pkg.go.dev/os#Symlink func (r *Root) Symlink(path, target string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootSymlink(rootFd, path, target) return struct{}{}, err }) return err } // Hardlink creates a hardlink within a [Root]'s directory tree. The hardlink // is created at path and is a link to target. Both paths are within the // [Root]'s directory tree (you cannot hardlink to a different [Root] or the // host). // // This is effectively equivalent to [os.Link]. // // [os.Link]: https://pkg.go.dev/os#Link func (r *Root) Hardlink(path, target string) error { _, err := fdutils.WithFileFd(r.inner, func(rootFd uintptr) (struct{}, error) { err := libpathrs.InRootHardlink(rootFd, path, target) return struct{}{}, err }) return err } // Readlink returns the target of a symlink with a [Root]'s directory tree. // // This is effectively equivalent to [os.Readlink]. // // [os.Readlink]: https://pkg.go.dev/os#Readlink func (r *Root) Readlink(path string) (string, error) { return fdutils.WithFileFd(r.inner, func(rootFd uintptr) (string, error) { return libpathrs.InRootReadlink(rootFd, path) }) } // IntoFile unwraps the [Root] into its underlying [os.File]. // // It is critical that you do not operate on this file descriptor yourself, // because the security properties of libpathrs depend on users doing all // relevant filesystem operations through libpathrs. // // This operation returns the internal [os.File] of the [Root] directly, so // calling [Root.Close] will also close any copies of the returned [os.File]. // If you want to get an independent copy, use [Root.Clone] followed by // [Root.IntoFile] on the cloned [Root]. // // [os.File]: https://pkg.go.dev/os#File func (r *Root) IntoFile() *os.File { // TODO: Figure out if we really don't want to make a copy. // TODO: We almost certainly want to clear r.inner here, but we can't do // that easily atomically (we could use atomic.Value but that'll make // things quite a bit uglier). return r.inner } // Clone creates a copy of a [Root] handle, such that it has a separate // lifetime to the original (while referring to the same underlying directory). func (r *Root) Clone() (*Root, error) { return RootFromFile(r.inner) } // Close frees all of the resources used by the [Root] handle. func (r *Root) Close() error { return r.inner.Close() } pathrs-0.2.1/go-pathrs/utils_linux.go000064400000000000000000000025141046102023000157410ustar 00000000000000//go:build linux // SPDX-License-Identifier: MPL-2.0 /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * 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/. */ package pathrs import ( "fmt" "os" "golang.org/x/sys/unix" ) //nolint:cyclop // this function needs to handle a lot of cases func toUnixMode(mode os.FileMode, needsType bool) (uint32, error) { sysMode := uint32(mode.Perm()) switch mode & os.ModeType { //nolint:exhaustive // we only care about ModeType bits case 0: if needsType { sysMode |= unix.S_IFREG } case os.ModeDir: sysMode |= unix.S_IFDIR case os.ModeSymlink: sysMode |= unix.S_IFLNK case os.ModeCharDevice | os.ModeDevice: sysMode |= unix.S_IFCHR case os.ModeDevice: sysMode |= unix.S_IFBLK case os.ModeNamedPipe: sysMode |= unix.S_IFIFO case os.ModeSocket: sysMode |= unix.S_IFSOCK default: return 0, fmt.Errorf("invalid mode filetype %+o", mode) } if mode&os.ModeSetuid != 0 { sysMode |= unix.S_ISUID } if mode&os.ModeSetgid != 0 { sysMode |= unix.S_ISGID } if mode&os.ModeSticky != 0 { sysMode |= unix.S_ISVTX } return sysMode, nil } pathrs-0.2.1/hack/ci-compute-test-partition.jq000075500000000000000000000023561046102023000174310ustar 00000000000000#!/usr/bin/jq -f # SPDX-License-Identifier: MPL-2.0 # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # 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/. # Compute the inverse set of the filters provided to this script. This script # is used by .github/workflows/rust.yml to partition tests into logical blocks # without accidentally missing any tests. def compute_misc($arr): $arr + [ { "name": "misc", "pattern": "not (\($arr | map("(" + .pattern + ")") | join(" or ")))" } ] ; # Use a sane default if nothing is specified. compute_misc(. // [ { "name": "Root", "pattern": "test(#tests::test_root_ops::root_*) or test(#root::*)" }, { "name": "RootRef", "pattern": "test(#tests::test_root_ops::rootref_*) or test(#tests::test_root_ops::capi_*)" }, { "name": "procfs", "pattern": "test(#tests::test_procfs*) or test(*proc*)" }, { "name": "resolver", "pattern": "test(#tests::test_resolve*) or test(#resolvers::*)" }, { "name": "race", "pattern": "test(#tests::test_race*)" } ]) pathrs-0.2.1/hack/rust-tests.sh000075500000000000000000000200501046102023000145240ustar 00000000000000#!/bin/bash # SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # == 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 . set -Eeuo pipefail SRC_ROOT="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")/..")" function bail() { echo "rust tests: $*" >&2 exit 1 } function contains() { local elem needle="$1" shift for elem in "$@"; do [[ "$elem" == "$needle" ]] && return 0 done return 1 } function strjoin() { local sep="$1" shift local str= until [[ "$#" == 0 ]]; do str+="${1:-}" shift [[ "$#" == 0 ]] && break str+="$sep" done echo "$str" } TEMP="$(getopt -o sc:p:S: --long sudo,cargo:,partition:,enosys:,archive-file: -- "$@")" eval set -- "$TEMP" sudo= partition= enosys_syscalls=() nextest_archive= CARGO="${CARGO_NIGHTLY:-cargo +nightly}" while [ "$#" -gt 0 ]; do case "$1" in --archive-file) nextest_archive="$2" shift 2 ;; -s|--sudo) sudo=1 shift ;; -c|--cargo) CARGO="$2" shift 2 ;; -p|--partition) partition="$2" shift 2 ;; -S|--enosys) [ -n "$2" ] && enosys_syscalls+=("$2") shift 2 ;; --) shift break ;; *) bail "unknown option $1" esac done tests_to_run=("$@") [ -n "$partition" ] || { partition=1 # When running in GHA, if we run the *entire* test suite without splitting # it to compact the profraw coverage data, we run out of disk space, so by # default we should at least partition the run into 2. If the caller # specified an explicit test set then instead we should just stick to one. if [ "${#tests_to_run[@]}" -eq 0 ] && [[ "${GITHUB_ACTIONS:-}" == "true" ]]; then partition=2 fi } # If --partition contains a slash then it indicates that we are running as # a _specific_ partition. If it is just numeric then it is indicating the # _number_ of partitions. partitions= if [[ "$partition" == *"/"* ]]; then partitions=1 partition="hash:$partition" else partitions="$partition" partition= fi [ "${#enosys_syscalls[@]}" -gt 0 ] && { cargo build --release --manifest-path "$SRC_ROOT/contrib/fake-enosys/Cargo.toml" FAKE_ENOSYS="$SRC_ROOT/target/release/fake-enosys" # Make sure the syscalls are valid. "$FAKE_ENOSYS" -s "$(strjoin , "${enosys_syscalls[@]}")" true || \ bail "--enosys=$(strjoin , "${enosys_syscalls[@]}") contains invalid syscalls" } function llvm-profdata() { local profdata { command llvm-profdata --help &>/dev/null && profdata=llvm-profdata ; } || { command rust-profdata --help &>/dev/null && profdata=rust-profdata ; } || { command cargo-profdata --help &>/dev/null && profdata=cargo-profdata ; } || bail "cannot find llvm-profdata!" command "$profdata" "$@" } function merge_llvmcov_profdata() { local llvmcov_targetdir=target/llvm-cov-target # Get a list of *.profraw files for merging. local profraw_list profraw_list="$(mktemp --tmpdir libpathrs-profraw.XXXXXXXX)" find "$llvmcov_targetdir" -name '*.profraw' -type f >"$profraw_list" #shellcheck disable=SC2064 trap "rm -f '$profraw_list'; trap - RETURN" RETURN # Merge profiling data. This is what cargo-llvm-cov does internally, and # they also make use of --sparse to remove useless entries. local combined_profraw combined_profraw="$(mktemp libpathrs-combined-profraw.XXXXXXXX)" llvm-profdata merge --sparse -f "$profraw_list" -o "$combined_profraw" # 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" } function nextest_run() { local features=("capi") # Add any extra features passed in the environment. local extra extra_features IFS=, read -ra extra_features <<<"${EXTRA_FEATURES:-}" for extra in "${extra_features[@]}"; do [ -n "$extra" ] && features+=("$extra") done if [ -v CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER ]; then unset CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER fi if [ -n "$sudo" ]; then features+=("_test_as_root") # 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 " fi build_args=() if [ "${#features[@]}" -gt 0 ]; then build_args=("--workspace" "--features" "$(strjoin , "${features[@]}")") fi if [ "${#enosys_syscalls[@]}" -gt 0 ]; then export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER+="$FAKE_ENOSYS -s $(strjoin , "${enosys_syscalls[@]}") -- " fi archive_args=() if [ -n "$nextest_archive" ]; then archive_args+=(--archive-file "$nextest_archive" --workspace-remap "$SRC_ROOT") # When using --archive-file we cannot set --features. echo "features '${features[*]}' are ignored with --archive-file" >&2 build_args=() fi for partnum in $(seq "$partitions"); do part="${partition:-hash:$partnum/$partitions}" if [ -n "$nextest_archive" ]; then # When using --archive-file, using the same target dir for multiple # "cargo llvm-cov nextest run" instances causes errors: # error: error extracting archive `./pathrs.tar.zst` # destination `/home/cyphar/src/libpathrs/target/llvm-cov-target/target` already exists rm -rf "$SRC_ROOT/target/llvm-cov-target/target" fi $CARGO \ llvm-cov --no-report --branch "${build_args[@]}" \ nextest --partition="$part" "${archive_args[@]}" "$@" # It turns out that a very large amount of diskspace gets used up by # the thousands of tiny .profraw files generated during each # integration test run (up to ~22GB in our GHA CI). # # This can cause disk exhaustion (and thus CI failures), so we need to # proactively merge the profiling data to reduce its size (in addition # to running the tests in partitions to avoid accumulating all 22GBs of # profiling data before merging). # # cargo-llvm-cov will happily accept these merged files, and this kind # of merging is what it does internally anyway. merge_llvmcov_profdata done if [ -v CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER ]; then unset CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER fi } set -x # Increase the maximum file descriptor limit from the default 1024 to whatever # the hard limit is (which should be large enough) so that our racing # remove_all tests won't fail with EMFILE. Ideally this workaround wouldn't be # necessary, see . ulimit -n "$(ulimit -Hn)" if [ "${#tests_to_run[@]}" -gt 0 ]; then for test_spec in "${tests_to_run[@]}"; do nextest_run --no-fail-fast -E "$test_spec" done else # We need to run race and non-race tests separately because the racing # tests can cause the non-race tests to error out spuriously. Hopefully in # the future will # be resolved and nextest will make it easier to do this. nextest_run --no-fail-fast -E "not test(#tests::test_race_*)" nextest_run --no-fail-fast -E "test(#tests::test_race_*)" fi pathrs-0.2.1/include/pathrs.h000064400000000000000000000771631046102023000142400ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif #define __CBINDGEN_ALIGNED(n) __attribute__((aligned(n))) #ifndef LIBPATHRS_H #define LIBPATHRS_H /* * WARNING: This file was auto-generated by rust-cbindgen. Don't modify it. * Instead, re-generate it with: * % cbindgen -c cbindgen.toml -o include/pathrs.h */ #include #include #include #include #include #include /* * Returns whether the given numerical value is a libpathrs error (which can be * passed to pathrs_errorinfo()). Users are recommended to use this instead of * a bare `<0` comparison because some functions may return a negative number * even in a success condition. */ #define IS_PATHRS_ERR(ret) ((ret) < __PATHRS_MAX_ERR_VALUE) /* * Used to construct pathrs_proc_base_t values for a PID (or TID). Passing * PATHRS_PROC_PID(pid) to pathrs_proc_*() as pathrs_proc_base_t will cause * libpathrs to use /proc/$pid as the base of the operation. * * This is essentially functionally equivalent to prefixing "$pid/" to the * subpath argument and using PATHRS_PROC_ROOT. * * Note that this operation is inherently racy -- the process referenced by this * PID may have died and the PID recycled with a different process. In * principle, this means that it is only really safe to use this with: * * - PID 1 (the init process), as that PID cannot ever get recycled. * - Your current PID (though you should just use PATHRS_PROC_SELF). * - Your current TID (though you should just use PATHRS_PROC_THREAD_SELF), * or _possibly_ other TIDs in your thread-group if you are absolutely sure * they have not been reaped (typically with pthread_join(3), though there * are other ways). * - PIDs of child processes (as long as you are sure that no other part of * your program incorrectly catches or ignores SIGCHLD, and that you do it * *before* you call wait(2) or any equivalent method that could reap * zombies). * * Outside of those specific uses, users should probably avoid using this. */ #define PATHRS_PROC_PID(n) (__PATHRS_PROC_TYPE_PID | (n)) /* * A sentinel value to tell `pathrs_proc_*` methods to use the default procfs * root handle (which may be globally cached). */ #define PATHRS_PROC_DEFAULT_ROOTFD -9 /* (-EBADF) */ /** * Bits in `pathrs_proc_base_t` that indicate the type of the base value. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_PROC_TYPE_MASK 18446744069414584320ull /** * Bits in `pathrs_proc_base_t` that must be set for `/proc/$pid` values. Don't * use this directly, instead use `PATHRS_PROC_PID(n)` to convert a PID to an * appropriate `pathrs_proc_base_t` value. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_PROC_TYPE_PID 9223372036854775808ull /** * Construct a completely unmasked procfs handle. * * This is equivalent to [`ProcfsHandleBuilder::unmasked`], and is meant as * a flag argument to [`ProcfsOpenFlags`] (the `flags` field in `struct * pathrs_procfs_open_how`) for use with pathrs_procfs_open(). */ #define PATHRS_PROCFS_NEW_UNMASKED 1 /** * Indicate what base directory should be used when doing operations with * `pathrs_proc_*`. In addition to the values defined here, the following * macros can be used for other values: * * * `PATHRS_PROC_PID(pid)` refers to the `/proc/` directory for the * process with PID (or TID) `pid`. * * Note that this operation is inherently racy and should probably avoided * for most uses -- see the block comment above `PATHRS_PROC_PID(n)` for * more details. * * Unknown values will result in an error being returned. */ enum pathrs_proc_base_t { /** * Use /proc. Note that this mode may be more expensive because we have * to take steps to try to avoid leaking unmasked procfs handles, so you * should use PATHRS_PROC_SELF if you can. */ PATHRS_PROC_ROOT = 18446744067006164835ull, /** * Use /proc/self. For most programs, this is the standard choice. */ PATHRS_PROC_SELF = 18446744065272536607ull, /** * Use /proc/thread-self. In multi-threaded programs where one thread has a * different CLONE_FS, it is possible for /proc/self to point the wrong * thread and so /proc/thread-self may be necessary. * * NOTE: Using /proc/thread-self may require care if used from languages * where your code can change threads without warning and old threads can * be killed (such as Go -- where you want to use runtime.LockOSThread). */ PATHRS_PROC_THREAD_SELF = 18446744066171166239ull, }; typedef uint64_t pathrs_proc_base_t; typedef struct { uint64_t flags; } pathrs_procfs_open_how; /** * Attempts to represent a Rust Error type in C. This structure must be freed * using pathrs_errorinfo_free(). */ typedef struct __CBINDGEN_ALIGNED(8) { /** * Raw errno(3) value of the underlying error (or 0 if the source of the * error was not due to a syscall error). */ uint64_t saved_errno; /** * Textual description of the error. */ const char *description; } pathrs_error_t; /** * The smallest return value which cannot be a libpathrs error ID. * * While all libpathrs error IDs are negative numbers, some functions may * return a negative number in a success scenario. This macro defines the high * range end of the numbers that can be used as an error ID. Don't use this * value directly, instead use `IS_PATHRS_ERR(ret)` to check if a returned * value is an error or not. * * NOTE: This is used internally by libpathrs. You should avoid using this * macro if possible. */ #define __PATHRS_MAX_ERR_VALUE -4096 /** * Open a root handle. * * The provided path must be an existing directory. * * Note that root handles are not special -- this function is effectively * equivalent to * * ```c * fd = open(path, O_PATH|O_DIRECTORY); * ``` * * # Return Value * * On success, this function returns a file descriptor that can be used as a * root handle in subsequent pathrs_inroot_* operations. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_open_root(const char *path); /** * "Upgrade" an O_PATH file descriptor to a usable fd, suitable for reading and * writing. This does not consume the original file descriptor. (This can be * used with non-O_PATH file descriptors as well.) * * It should be noted that the use of O_CREAT *is not* supported (and will * result in an error). Handles only refer to *existing* files. Instead you * need to use pathrs_inroot_creat(). * * In addition, O_NOCTTY is automatically set when opening the path. If you * want to use the path as a controlling terminal, you will have to do * ioctl(fd, TIOCSCTTY, 0) yourself. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_reopen(int fd, int flags); /** * Resolve the given path within the rootfs referenced by root_fd. The path * *must already exist*, otherwise an error will occur. * * All symlinks (including trailing symlinks) are followed, but they are * resolved within the rootfs. If you wish to open a handle to the symlink * itself, use pathrs_inroot_resolve_nofollow(). * * # Return Value * * On success, this function returns an O_PATH file descriptor referencing the * resolved path. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_resolve(int root_fd, const char *path); /** * pathrs_inroot_resolve_nofollow() is effectively an O_NOFOLLOW version of * pathrs_inroot_resolve(). Their behaviour is identical, except that * *trailing* symlinks will not be followed. If the final component is a * trailing symlink, an O_PATH|O_NOFOLLOW handle to the symlink itself is * returned. * * # Return Value * * On success, this function returns an O_PATH file descriptor referencing the * resolved path. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_resolve_nofollow(int root_fd, const char *path); /** * pathrs_inroot_open() is effectively shorthand for pathrs_inroot_resolve() * followed by pathrs_reopen(). If you only need to open a path and don't care * about re-opening it later, this can be slightly more efficient than the * alternative for the openat2-based resolver as it doesn't require allocating * an extra file descriptor. For languages where C FFI is expensive (such as * Go), using this also saves a function call. * * If flags contains O_NOFOLLOW, the behaviour is like that of * pathrs_inroot_resolve_nofollow() followed by pathrs_reopen(). * * In addition, O_NOCTTY is automatically set when opening the path. If you * want to use the path as a controlling terminal, you will have to do * ioctl(fd, TIOCSCTTY, 0) yourself. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_open(int root_fd, const char *path, int flags); /** * Get the target of a symlink within the rootfs referenced by root_fd. * * NOTE: The returned path is not modified to be "safe" outside of the * root. You should not use this path for doing further path lookups -- use * pathrs_inroot_resolve() instead. * * This method is just shorthand for: * * ```c * int linkfd = pathrs_inroot_resolve_nofollow(rootfd, path); * if (IS_PATHRS_ERR(linkfd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * copied = readlinkat(linkfd, "", linkbuf, linkbuf_size); * close(linkfd); * ``` * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_inroot_readlink() will return *the * number of bytes it would have copied if the buffer was large enough*. This * matches the behaviour of pathrs_proc_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_readlink(int root_fd, const char *path, char *linkbuf, size_t linkbuf_size); /** * Rename a path within the rootfs referenced by root_fd. The flags argument is * identical to the renameat2(2) flags that are supported on the system. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_rename(int root_fd, const char *src, const char *dst, uint32_t flags); /** * Remove the empty directory at path within the rootfs referenced by root_fd. * * The semantics are effectively equivalent to unlinkat(..., AT_REMOVEDIR). * This function will return an error if the path doesn't exist, was not a * directory, or was a non-empty directory. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_rmdir(int root_fd, const char *path); /** * Remove the file (a non-directory inode) at path within the rootfs referenced * by root_fd. * * The semantics are effectively equivalent to unlinkat(..., 0). This function * will return an error if the path doesn't exist or was a directory. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_unlink(int root_fd, const char *path); /** * Recursively delete the path and any children it contains if it is a * directory. The semantics are equivalent to `rm -r`. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_remove_all(int root_fd, const char *path); /** * Create a new regular file within the rootfs referenced by root_fd. This is * effectively an O_CREAT operation, and so (unlike pathrs_inroot_resolve()), * this function can be used on non-existent paths. * * If you want to ensure the creation is a new file, use O_EXCL. * * If you want to create a file without opening a handle to it, you can do * pathrs_inroot_mknod(root_fd, path, S_IFREG|mode, 0) instead. * * As with pathrs_reopen(), O_NOCTTY is automatically set when opening the * path. If you want to use the path as a controlling terminal, you will have * to do ioctl(fd, TIOCSCTTY, 0) yourself. * * NOTE: Unlike O_CREAT, pathrs_inroot_creat() will return an error if the * final component is a dangling symlink. O_CREAT will create such files, and * while openat2 does support this it would be difficult to implement this in * the emulated resolver. * * # Return Value * * On success, this function returns a file descriptor to the requested file. * The open flags are based on the provided flags. The file descriptor will * have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_creat(int root_fd, const char *path, int flags, unsigned int mode); /** * Create a new directory within the rootfs referenced by root_fd. * * This is shorthand for pathrs_inroot_mknod(root_fd, path, S_IFDIR|mode, 0). * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mkdir(int root_fd, const char *path, unsigned int mode); /** * Create a new directory (and any of its path components if they don't exist) * within the rootfs referenced by root_fd. * * # Return Value * * On success, this function returns an O_DIRECTORY file descriptor to the * newly created directory. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mkdir_all(int root_fd, const char *path, unsigned int mode); /** * Create a inode within the rootfs referenced by root_fd. The type of inode to * be created is configured using the S_IFMT bits in mode (a-la mknod(2)). * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_mknod(int root_fd, const char *path, unsigned int mode, dev_t dev); /** * Create a symlink within the rootfs referenced by root_fd. Note that the * symlink target string is not modified when creating the symlink. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_symlink(int root_fd, const char *path, const char *target); /** * Create a hardlink within the rootfs referenced by root_fd. Both the hardlink * path and target are resolved within the rootfs. * * # Return Value * * On success, this function returns 0. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_inroot_hardlink(int root_fd, const char *path, const char *target); /** * Create a new (custom) procfs root handle. * * This is effectively a C wrapper around [`ProcfsHandleBuilder`], allowing you * to create a custom procfs root handle that can be used with other * `pathrs_proc_*at` methods. * * While most users should just use `PATHRS_PROC_DEFAULT_ROOTFD` (or the * non-`at` variants of `pathrs_proc_*`), creating an unmasked procfs root * handle (using `PATHRS_PROCFS_NEW_UNMASKED`) can be useful for programs that * need to operate on a lot of global procfs files. (Note that accessing global * procfs files does not *require* creating a custom procfs handle -- * `pathrs_proc_*` will automatically create a global-friendly handle * internally when necessary but will close it immediately after operating on * it.) * * # Extensible Structs * * The [`ProcfsOpenHow`] (`struct pathrs_procfs_open_how`) argument is * designed to be extensible, modelled after the extensible structs scheme used * by Linux (for syscalls such as [clone3(2)], [openat2(2)] and other such * syscalls). Normally one would use symbol versioning to achieve this, but * unfortunately Rust's symbol versioning support is incredibly primitive (one * might even say "non-existent") and so this system is more robust, even if * the calling convention is a little strange for userspace libraries. * * In addition to a pointer argument, the caller must also provide the size of * the structure it is passing. By providing this information, it is possible * for `pathrs_procfs_open()` to provide both forwards- and * backwards-compatibility, with size acting as an implicit version number. * (Because new extension fields will always be appended, the structure size * will always increase.) * * If we let `usize` be the structure specified by the caller, and `lsize` be * the size of the structure internal to libpathrs, then there are three cases * to consider: * * * If `usize == lsize`, then there is no version mismatch and the structure * provided by the caller can be used verbatim. * * If `usize < lsize`, then there are some extension fields which libpathrs * supports that the caller does not. Because a zero value in any added * extension field signifies a no-op, libpathrs treats all of the extension * fields not provided by the caller as having zero values. This provides * backwards-compatibility. * * If `usize > lsize`, then there are some extension fields which the caller * is aware of but this version of libpathrs does not support. Because any * extension field must have its zero values signify a no-op, libpathrs can * safely ignore the unsupported extension fields if they are all-zero. If * any unsupported extension fields are nonzero, then an `E2BIG` error is * returned. This provides forwards-compatibility. * * Because the definition of `struct pathrs_procfs_open_how` may open in the * future * * Because the definition of `struct pathrs_procfs_open_how` may change in the * future (with new fields being added when headers are updated), callers * should zero-fill the structure to ensure that recompiling the program with * new headers will not result in spurious errors at run time. The simplest * way is to use a designated initialiser: * * ```c * struct pathrs_procfs_open_how how = { * .flags = PATHRS_PROCFS_NEW_UNMASKED, * }; * ``` * * or explicitly using `memset(3)` or similar: * * ```c * struct pathrs_procfs_open_how how; * memset(&how, 0, sizeof(how)); * how.flags = PATHRS_PROCFS_NEW_UNMASKED; * ``` * * # Return Value * * On success, this function returns *either* a file descriptor *or* * `PATHRS_PROC_DEFAULT_ROOTFD` (this is a negative number, equal to `-EBADF`). * The file descriptor will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). * * [clone3(2)]: https://www.man7.org/linux/man-pages/man2/clone3.2.html * [openat2(2)]: https://www.man7.org/linux/man-pages/man2/openat2.2.html */ int pathrs_procfs_open(const pathrs_procfs_open_how *args, size_t size); /** * `pathrs_proc_open` but with a caller-provided file descriptor for `/proc`. * * Internally, `pathrs_proc_open` will attempt to use a cached copy of a very * restricted `/proc` handle (a detached mount object with `subset=pid` and * `hidepid=4`). If a user requests a global `/proc` file, a temporary handle * capable of accessing global files is created and destroyed after the * operation completes. * * For most users, this is more than sufficient. However, if a user needs to * operate on many global `/proc` files, the cost of creating handles can get * quite expensive. `pathrs_proc_openat` allows a user to manually manage the * global-friendly `/proc` handle. Note that passing a `subset=pid` file * descriptor to `pathrs_proc_openat` will *not* stop the automatic creation of * a global-friendly handle internally if necessary. * * In order to get the behaviour of `pathrs_proc_open`, you can pass the * special value `PATHRS_PROC_DEFAULT_ROOTFD` (`-EBADF`) as the `proc_rootfd` * argument. * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_openat(int proc_rootfd, pathrs_proc_base_t base, const char *path, int flags); /** * Safely open a path inside a `/proc` handle. * * Any bind-mounts or other over-mounts will (depending on what kernel features * are available) be detected and an error will be returned. Non-trailing * symlinks are followed but care is taken to ensure the symlinks are * legitimate. * * Unless you intend to open a magic-link, `O_NOFOLLOW` should be set in flags. * Lookups with `O_NOFOLLOW` are guaranteed to never be tricked by bind-mounts * (on new enough Linux kernels). * * If you wish to resolve a magic-link, you need to unset `O_NOFOLLOW`. * Unfortunately (if libpathrs is using the regular host `/proc` mount), this * lookup mode cannot protect you against an attacker that can modify the mount * table during this operation. * * NOTE: Instead of using paths like `/proc/thread-self/fd`, `base` is used to * indicate what "base path" inside procfs is used. For example, to re-open a * file descriptor: * * ```c * fd = pathrs_proc_open(PATHRS_PROC_THREAD_SELF, "fd/101", O_RDWR); * if (IS_PATHRS_ERR(fd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * ``` * * # Return Value * * On success, this function returns a file descriptor. The file descriptor * will have the `O_CLOEXEC` flag automatically applied. * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_open(pathrs_proc_base_t base, const char *path, int flags); /** * `pathrs_proc_readlink` but with a caller-provided file descriptor for * `/proc`. * * See the documentation of pathrs_proc_openat() for when this API might be * useful. * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_proc_readlink() will return *the number * of bytes it would have copied if the buffer was large enough*. This matches * the behaviour of pathrs_inroot_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_readlinkat(int proc_rootfd, pathrs_proc_base_t base, const char *path, char *linkbuf, size_t linkbuf_size); /** * Safely read the contents of a symlink inside `/proc`. * * As with `pathrs_proc_open`, any bind-mounts or other over-mounts will * (depending on what kernel features are available) be detected and an error * will be returned. Non-trailing symlinks are followed but care is taken to * ensure the symlinks are legitimate. * * This function is effectively shorthand for * * ```c * fd = pathrs_proc_open(base, path, O_PATH|O_NOFOLLOW); * if (IS_PATHRS_ERR(fd)) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; * } * copied = readlinkat(fd, "", linkbuf, linkbuf_size); * close(fd); * ``` * * # Return Value * * On success, this function copies the symlink contents to `linkbuf` (up to * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. * This function will not copy the trailing NUL byte, and the return size does * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to * contain the symlink contents, pathrs_proc_readlink() will return *the number * of bytes it would have copied if the buffer was large enough*. This matches * the behaviour of pathrs_inroot_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, * the system errno(7) value associated with the error, etc), use * pathrs_errorinfo(). */ int pathrs_proc_readlink(pathrs_proc_base_t base, const char *path, char *linkbuf, size_t linkbuf_size); /** * Retrieve error information about an error id returned by a pathrs operation. * * Whenever an error occurs with libpathrs, a negative number describing that * error (the error id) is returned. pathrs_errorinfo() is used to retrieve * that information: * * ```c * fd = pathrs_inroot_resolve(root, "/foo/bar"); * if (IS_PATHRS_ERR(fd)) { * // fd is an error id * pathrs_error_t *error = pathrs_errorinfo(fd); * // ... print the error information ... * pathrs_errorinfo_free(error); * } * ``` * * Once pathrs_errorinfo() is called for a particular error id, that error id * is no longer valid and should not be used for subsequent pathrs_errorinfo() * calls. * * Error ids are only unique from one another until pathrs_errorinfo() is * called, at which point the id can be reused for subsequent errors. The * precise format of error ids is completely opaque and they should never be * compared directly or used for anything other than with pathrs_errorinfo(). * * Error ids are not thread-specific and thus pathrs_errorinfo() can be called * on a different thread to the thread where the operation failed (this is of * particular note to green-thread language bindings like Go, where this is * important). * * # Return Value * * If there was a saved error with the provided id, a pathrs_error_t is * returned describing the error. Use pathrs_errorinfo_free() to free the * associated memory once you are done with the error. */ pathrs_error_t *pathrs_errorinfo(int err_id); /** * Free the pathrs_error_t object returned by pathrs_errorinfo(). */ void pathrs_errorinfo_free(pathrs_error_t *ptr); #endif /* LIBPATHRS_H */ #ifdef __CBINDGEN_ALIGNED #undef __CBINDGEN_ALIGNED #endif pathrs-0.2.1/install.sh000075500000000000000000000162631046102023000131420ustar 00000000000000#!/bin/bash # SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later # # libpathrs: safe path resolution on Linux # Copyright (C) 2019-2025 Aleksa Sarai # Copyright (C) 2019-2025 SUSE LLC # # == 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 . set -Eeuo pipefail src_dir="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")" pushd "$src_dir" |: get_crate_info() { # TODO: Should we use toml-cli if it's available? field="$1" sed -En '/^'"$field"'/ s/^.*=\s+"(.*)"/\1/ p' "$src_dir/Cargo.toml" } FULLVERSION="$(get_crate_info version)" get_so_version() { # TODO: The soversion should probably be separated from the Crate version # -- we only need to bump the soversion if we have to introduce a # completely incompatible change to the C API (that can't be kept using # symbol versioning and aliases). It seems very unlikely we will ever need # to bump this. echo "${FULLVERSION%%.*}" } SOVERSION="$(get_so_version)" SONAME="lib$(get_crate_info name).so.$FULLVERSION" # Try to emulate autoconf's basic flags. DEFAULT_PREFIX=/usr/local usage() { [ "$#" -eq 0 ] || echo "ERROR:" "$@" >&2 cat >&2 <<-EOF usage: ${BASH_SOURCE[0]} [a subset of autoconf args] Install libpathrs in a way that should make it easier to package. This script takes a small subset of autoconf arguments (such as --prefix) and uses them to tailor the installation destinations and generated pkg-config manifest so that distributions should be able to just use this script. Arguments: The following autoconf arguments are accepted by this script. The value in brackets is the default value used if the flags are not specified. --prefix=[$DEFAULT_PREFIX] --exec-prefix=[PREFIX] --includedir=[EPREFIX/include] --libdir=[EPREFIX/lib(64)] (lib64 is used if available) --pkgconfigdir=[LIBDIR/pkgconfig] As with automake, if the DESTDIR= environment variable is set, this script will install the files into DESTDIR as though it were the root of the filesystem. This is usually used for distribution packaging. You can also pass environment variables as command-line arguments. Example: In an openSUSE rpm spec, this script could be used like this: %install ./install.sh \\ DESTDIR=%{buildroot} \\ --prefix=%{_prefix} \\ --exec-prefix=%{_exec_prefix} \\ --includedir=%{_includedir} \\ --libdir=%{_libdir} This script is part of the libpathrs project. If you find a bug, please report it to . EOF exit_code=0 [ "$#" -gt 0 ] && exit_code=1 exit "$exit_code" } GETOPT="$(getopt -o h --long help,prefix:,exec-prefix:,includedir:,libdir:,pkgconfigdir: -- "$@")" eval set -- "$GETOPT" DESTDIR="${DESTDIR:-}" prefix="$DEFAULT_PREFIX" exec_prefix= includedir= libdir= pkgconfigdir= while true; do case "$1" in --prefix) prefix="$2"; shift 2 ;; --exec-prefix) exec_prefix="$2"; shift 2 ;; --includedir) includedir="$2"; shift 2 ;; --libdir) libdir="$2"; shift 2 ;; --pkgconfigdir) pkgconfigdir="$2"; shift 2 ;; --) shift; break ;; -h | --help) usage ;; *) usage "unknown argument $1" ;; esac done for extra_arg in "$@"; do if [[ "$extra_arg" = *=* ]]; then echo "[options] using $extra_arg from command-line" eval "$extra_arg" else usage "unknown trailing argument $extra_arg" fi done find_libdir() { exec_prefix="$1" if [ -d "$exec_prefix/lib64" ]; then echo "$exec_prefix/lib64" else echo "$exec_prefix/lib" fi } # Apply default values using $prefix. Do this after parsing the other values so # that if a user just changes --prefix things still work. exec_prefix="${exec_prefix:-$prefix}" includedir="${includedir:-$prefix/include}" libdir="${libdir:-$(find_libdir "$exec_prefix")}" pkgconfigdir="${pkgconfigdir:-$libdir/pkgconfig}" # TODO: These flags come from RUSTFLAGS="--print=native-static-libs". # Unfortunately, getting this information from cargo is incredibly unergonomic # and will hopefully be fixed at some point. # native_static_libs="-lgcc_s -lutil -lrt -lpthread -lm -ldl -lc" echo "[pkg-config] generating pathrs pkg-config" cat >"pathrs.pc" < # Copyright (C) 2019-2025 SUSE LLC # # == 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 . prefix=$prefix exec_prefix=$exec_prefix includedir=$includedir libdir=$libdir Name: libpathrs Version: $FULLVERSION Description: Safe path resolution library for Linux URL: https://github.com/cyphar/libpathrs Cflags: -I\${includedir} Libs: -L\${libdir} -lpathrs Libs.private: $native_static_libs EOF echo "[install] installing libpathrs into DESTDIR=${DESTDIR:-/}" set -x # pkg-config. install -Dt "$DESTDIR/$pkgconfigdir/" -m 0644 pathrs.pc install -Dt "$DESTDIR/$includedir/" -m 0644 include/pathrs.h # Static library. install -Dt "$DESTDIR/$libdir" -m 0644 target/release/libpathrs.a # Shared library. install -DT -m 0755 target/release/libpathrs.so "$DESTDIR/$libdir/$SONAME" ln -sf "$SONAME" "$DESTDIR/$libdir/libpathrs.so.$SOVERSION" ln -sf "$SONAME" "$DESTDIR/$libdir/libpathrs.so" pathrs-0.2.1/src/capi/core.rs000064400000000000000000000665771046102023000141530ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ capi::{ ret::IntoCReturn, utils::{self, CBorrowedFd}, }, error::{Error, ErrorImpl}, flags::{OpenFlags, RenameFlags}, procfs::ProcfsHandle, utils::FdExt, InodeType, Root, RootRef, }; use std::{ fs::Permissions, os::unix::{fs::PermissionsExt, io::RawFd}, }; use libc::{c_char, c_int, c_uint, dev_t, size_t}; /// Open a root handle. /// /// The provided path must be an existing directory. /// /// Note that root handles are not special -- this function is effectively /// equivalent to /// /// ```c /// fd = open(path, O_PATH|O_DIRECTORY); /// ``` /// /// # Return Value /// /// On success, this function returns a file descriptor that can be used as a /// root handle in subsequent pathrs_inroot_* operations. The file descriptor /// will have the `O_CLOEXEC` flag automatically applied. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_open_root(path: *const c_char) -> RawFd { unsafe { utils::parse_path(path) } // SAFETY: C caller says path is safe. .and_then(Root::open) .into_c_return() } utils::symver! { fn pathrs_open_root <- (pathrs_open_root, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_open_root <- (pathrs_root_open, version = "LIBPATHRS_0.1", default); } /// "Upgrade" an O_PATH file descriptor to a usable fd, suitable for reading and /// writing. This does not consume the original file descriptor. (This can be /// used with non-O_PATH file descriptors as well.) /// /// It should be noted that the use of O_CREAT *is not* supported (and will /// result in an error). Handles only refer to *existing* files. Instead you /// need to use pathrs_inroot_creat(). /// /// In addition, O_NOCTTY is automatically set when opening the path. If you /// want to use the path as a controlling terminal, you will have to do /// ioctl(fd, TIOCSCTTY, 0) yourself. /// /// # Return Value /// /// On success, this function returns a file descriptor. The file descriptor /// will have the `O_CLOEXEC` flag automatically applied. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub extern "C" fn pathrs_reopen(fd: CBorrowedFd<'_>, flags: c_int) -> RawFd { let flags = OpenFlags::from_bits_retain(flags); || -> Result<_, Error> { fd.try_as_borrowed_fd()? .reopen(&ProcfsHandle::new()?, flags) }() .into_c_return() } utils::symver! { fn pathrs_reopen <- (pathrs_reopen, version = "LIBPATHRS_0.1", default); } /// Resolve the given path within the rootfs referenced by root_fd. The path /// *must already exist*, otherwise an error will occur. /// /// All symlinks (including trailing symlinks) are followed, but they are /// resolved within the rootfs. If you wish to open a handle to the symlink /// itself, use pathrs_inroot_resolve_nofollow(). /// /// # Return Value /// /// On success, this function returns an O_PATH file descriptor referencing the /// resolved path. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_resolve( root_fd: CBorrowedFd<'_>, path: *const c_char, ) -> RawFd { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. root.resolve(path) }() .into_c_return() } utils::symver! { fn pathrs_inroot_resolve <- (pathrs_inroot_resolve, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so that // loaders will pick it when searching for the unversioned name. fn pathrs_inroot_resolve <- (pathrs_resolve, version = "LIBPATHRS_0.1", default); } /// pathrs_inroot_resolve_nofollow() is effectively an O_NOFOLLOW version of /// pathrs_inroot_resolve(). Their behaviour is identical, except that /// *trailing* symlinks will not be followed. If the final component is a /// trailing symlink, an O_PATH|O_NOFOLLOW handle to the symlink itself is /// returned. /// /// # Return Value /// /// On success, this function returns an O_PATH file descriptor referencing the /// resolved path. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_resolve_nofollow( root_fd: CBorrowedFd<'_>, path: *const c_char, ) -> RawFd { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. root.resolve_nofollow(path) }() .into_c_return() } utils::symver! { fn pathrs_inroot_resolve_nofollow <- (pathrs_inroot_resolve_nofollow, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so that // loaders will pick it when searching for the unversioned name. fn pathrs_inroot_resolve_nofollow <- (pathrs_resolve_nofollow, version = "LIBPATHRS_0.1", default); } /// pathrs_inroot_open() is effectively shorthand for pathrs_inroot_resolve() /// followed by pathrs_reopen(). If you only need to open a path and don't care /// about re-opening it later, this can be slightly more efficient than the /// alternative for the openat2-based resolver as it doesn't require allocating /// an extra file descriptor. For languages where C FFI is expensive (such as /// Go), using this also saves a function call. /// /// If flags contains O_NOFOLLOW, the behaviour is like that of /// pathrs_inroot_resolve_nofollow() followed by pathrs_reopen(). /// /// In addition, O_NOCTTY is automatically set when opening the path. If you /// want to use the path as a controlling terminal, you will have to do /// ioctl(fd, TIOCSCTTY, 0) yourself. /// /// # Return Value /// /// On success, this function returns a file descriptor. The file descriptor /// will have the `O_CLOEXEC` flag automatically applied. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_open( root_fd: CBorrowedFd<'_>, path: *const c_char, flags: c_int, ) -> RawFd { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. let flags = OpenFlags::from_bits_retain(flags); root.open_subpath(path, flags) }() .into_c_return() } utils::symver! { fn pathrs_inroot_open <- (pathrs_inroot_open, version = "LIBPATHRS_0.2", default); } /// Get the target of a symlink within the rootfs referenced by root_fd. /// /// NOTE: The returned path is not modified to be "safe" outside of the /// root. You should not use this path for doing further path lookups -- use /// pathrs_inroot_resolve() instead. /// /// This method is just shorthand for: /// /// ```c /// int linkfd = pathrs_inroot_resolve_nofollow(rootfd, path); /// if (IS_PATHRS_ERR(linkfd)) { /// liberr = fd; // for use with pathrs_errorinfo() /// goto err; /// } /// copied = readlinkat(linkfd, "", linkbuf, linkbuf_size); /// close(linkfd); /// ``` /// /// # Return Value /// /// On success, this function copies the symlink contents to `linkbuf` (up to /// `linkbuf_size` bytes) and returns the full size of the symlink path buffer. /// This function will not copy the trailing NUL byte, and the return size does /// not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are /// treated as zero-size buffers. /// /// NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to /// contain the symlink contents, pathrs_inroot_readlink() will return *the /// number of bytes it would have copied if the buffer was large enough*. This /// matches the behaviour of pathrs_proc_readlink(). /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_readlink( root_fd: CBorrowedFd<'_>, path: *const c_char, linkbuf: *mut c_char, linkbuf_size: size_t, ) -> RawFd { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. let link_target = root.readlink(path)?; // SAFETY: C caller guarantees buffer is at least linkbuf_size and can // be written to. unsafe { utils::copy_path_into_buffer(link_target, linkbuf, linkbuf_size) } }() .into_c_return() } utils::symver! { fn pathrs_inroot_readlink <- (pathrs_inroot_readlink, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so that // loaders will pick it when searching for the unversioned name. fn pathrs_inroot_readlink <- (pathrs_readlink, version = "LIBPATHRS_0.1", default); } /// Rename a path within the rootfs referenced by root_fd. The flags argument is /// identical to the renameat2(2) flags that are supported on the system. /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_rename( root_fd: CBorrowedFd<'_>, src: *const c_char, dst: *const c_char, flags: u32, ) -> c_int { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let src = unsafe { utils::parse_path(src) }?; // SAFETY: C caller guarantees path is safe. let dst = unsafe { utils::parse_path(dst) }?; // SAFETY: C caller guarantees path is safe. let rflags = RenameFlags::from_bits_retain(flags); root.rename(src, dst, rflags) }() .into_c_return() } utils::symver! { fn pathrs_inroot_rename <- (pathrs_inroot_rename, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_rename <- (pathrs_rename, version = "LIBPATHRS_0.1", default); } /// Remove the empty directory at path within the rootfs referenced by root_fd. /// /// The semantics are effectively equivalent to unlinkat(..., AT_REMOVEDIR). /// This function will return an error if the path doesn't exist, was not a /// directory, or was a non-empty directory. /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_rmdir( root_fd: CBorrowedFd<'_>, path: *const c_char, ) -> c_int { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. root.remove_dir(path) }() .into_c_return() } utils::symver! { fn pathrs_inroot_rmdir <- (pathrs_inroot_rmdir, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_rmdir <- (pathrs_rmdir, version = "LIBPATHRS_0.1", default); } /// Remove the file (a non-directory inode) at path within the rootfs referenced /// by root_fd. /// /// The semantics are effectively equivalent to unlinkat(..., 0). This function /// will return an error if the path doesn't exist or was a directory. /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_unlink( root_fd: CBorrowedFd<'_>, path: *const c_char, ) -> c_int { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. root.remove_file(path) }() .into_c_return() } utils::symver! { fn pathrs_inroot_unlink <- (pathrs_inroot_unlink, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_unlink <- (pathrs_unlink, version = "LIBPATHRS_0.1", default); } /// Recursively delete the path and any children it contains if it is a /// directory. The semantics are equivalent to `rm -r`. /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_remove_all( root_fd: CBorrowedFd<'_>, path: *const c_char, ) -> c_int { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. root.remove_all(path) }() .into_c_return() } utils::symver! { fn pathrs_inroot_remove_all <- (pathrs_inroot_remove_all, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_remove_all <- (pathrs_remove_all, version = "LIBPATHRS_0.1", default); } // Within the root, create an inode at the path with the given mode. If the // path already exists, an error is returned (effectively acting as though // O_EXCL is always set). Each pathrs_inroot_* corresponds to the matching // syscall. // TODO: Replace all these wrappers with macros. It's quite repetitive. /// Create a new regular file within the rootfs referenced by root_fd. This is /// effectively an O_CREAT operation, and so (unlike pathrs_inroot_resolve()), /// this function can be used on non-existent paths. /// /// If you want to ensure the creation is a new file, use O_EXCL. /// /// If you want to create a file without opening a handle to it, you can do /// pathrs_inroot_mknod(root_fd, path, S_IFREG|mode, 0) instead. /// /// As with pathrs_reopen(), O_NOCTTY is automatically set when opening the /// path. If you want to use the path as a controlling terminal, you will have /// to do ioctl(fd, TIOCSCTTY, 0) yourself. /// /// NOTE: Unlike O_CREAT, pathrs_inroot_creat() will return an error if the /// final component is a dangling symlink. O_CREAT will create such files, and /// while openat2 does support this it would be difficult to implement this in /// the emulated resolver. /// /// # Return Value /// /// On success, this function returns a file descriptor to the requested file. /// The open flags are based on the provided flags. The file descriptor will /// have the `O_CLOEXEC` flag automatically applied. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_creat( root_fd: CBorrowedFd<'_>, path: *const c_char, flags: c_int, mode: c_uint, ) -> RawFd { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. let mode = mode & !libc::S_IFMT; let perm = Permissions::from_mode(mode); root.create_file(path, OpenFlags::from_bits_retain(flags), &perm) }() .into_c_return() } utils::symver! { fn pathrs_inroot_creat <- (pathrs_inroot_creat, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_creat <- (pathrs_creat, version = "LIBPATHRS_0.1", default); } /// Create a new directory within the rootfs referenced by root_fd. /// /// This is shorthand for pathrs_inroot_mknod(root_fd, path, S_IFDIR|mode, 0). /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_mkdir( root_fd: CBorrowedFd<'_>, path: *const c_char, mode: c_uint, ) -> c_int { let mode = mode & !libc::S_IFMT; pathrs_inroot_mknod(root_fd, path, libc::S_IFDIR | mode, 0) } utils::symver! { fn pathrs_inroot_mkdir <- (pathrs_inroot_mkdir, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_mkdir <- (pathrs_mkdir, version = "LIBPATHRS_0.1", default); } /// Create a new directory (and any of its path components if they don't exist) /// within the rootfs referenced by root_fd. /// /// # Return Value /// /// On success, this function returns an O_DIRECTORY file descriptor to the /// newly created directory. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_mkdir_all( root_fd: CBorrowedFd<'_>, path: *const c_char, mode: c_uint, ) -> RawFd { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. let perm = Permissions::from_mode(mode); root.mkdir_all(path, &perm) }() .into_c_return() } utils::symver! { fn pathrs_inroot_mkdir_all <- (pathrs_inroot_mkdir_all, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_mkdir_all <- (pathrs_mkdir_all, version = "LIBPATHRS_0.1", default); } /// Create a inode within the rootfs referenced by root_fd. The type of inode to /// be created is configured using the S_IFMT bits in mode (a-la mknod(2)). /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_mknod( root_fd: CBorrowedFd<'_>, path: *const c_char, mode: c_uint, dev: dev_t, ) -> c_int { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path)? }; // SAFETY: C caller guarantees path is safe. let fmt = mode & libc::S_IFMT; let perms = Permissions::from_mode(mode ^ fmt); let inode_type = match fmt { libc::S_IFREG => InodeType::File(perms), libc::S_IFDIR => InodeType::Directory(perms), libc::S_IFBLK => InodeType::BlockDevice(perms, dev), libc::S_IFCHR => InodeType::CharacterDevice(perms, dev), libc::S_IFIFO => InodeType::Fifo(perms), libc::S_IFSOCK => Err(ErrorImpl::NotImplemented { feature: "mknod(S_IFSOCK)".into(), })?, _ => Err(ErrorImpl::InvalidArgument { name: "mode".into(), description: "invalid S_IFMT mask".into(), })?, }; root.create(path, &inode_type) }() .into_c_return() } utils::symver! { fn pathrs_inroot_mknod <- (pathrs_inroot_mknod, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_mknod <- (pathrs_mknod, version = "LIBPATHRS_0.1", default); } /// Create a symlink within the rootfs referenced by root_fd. Note that the /// symlink target string is not modified when creating the symlink. /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_symlink( root_fd: CBorrowedFd<'_>, path: *const c_char, target: *const c_char, ) -> c_int { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path)? }; // SAFETY: C caller guarantees path is safe. let target = unsafe { utils::parse_path(target)? }; // SAFETY: C caller guarantees path is safe. root.create(path, &InodeType::Symlink(target.into())) }() .into_c_return() } utils::symver! { fn pathrs_inroot_symlink <- (pathrs_inroot_symlink, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_symlink <- (pathrs_symlink, version = "LIBPATHRS_0.1", default); } /// Create a hardlink within the rootfs referenced by root_fd. Both the hardlink /// path and target are resolved within the rootfs. /// /// # Return Value /// /// On success, this function returns 0. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_inroot_hardlink( root_fd: CBorrowedFd<'_>, path: *const c_char, target: *const c_char, ) -> c_int { || -> Result<_, Error> { let root_fd = root_fd.try_as_borrowed_fd()?; let root = RootRef::from_fd(root_fd); let path = unsafe { utils::parse_path(path)? }; // SAFETY: C caller guarantees path is safe. let target = unsafe { utils::parse_path(target)? }; // SAFETY: C caller guarantees path is safe. root.create(path, &InodeType::Hardlink(target.into())) }() .into_c_return() } utils::symver! { fn pathrs_inroot_hardlink <- (pathrs_inroot_hardlink, version = "LIBPATHRS_0.2", default); // This symbol was renamed in libpathrs 0.2. For backward compatibility with // pre-symbol-versioned builds of libpathrs, it needs to be a default so // that loaders will pick it when searching for the unversioned name. fn pathrs_inroot_hardlink <- (pathrs_hardlink, version = "LIBPATHRS_0.1", default); } pathrs-0.2.1/src/capi/error.rs000064400000000000000000000255351046102023000143410ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ capi::{ret::CReturn, utils, utils::Leakable}, error::Error, }; use std::{ collections::{hash_map::Entry as HashMapEntry, HashMap}, error::Error as StdError, ffi::CString, ptr, sync::Mutex, }; use libc::{c_char, c_int}; use once_cell::sync::Lazy; use rand::{self, Rng}; /// The smallest return value which cannot be a libpathrs error ID. /// /// While all libpathrs error IDs are negative numbers, some functions may /// return a negative number in a success scenario. This macro defines the high /// range end of the numbers that can be used as an error ID. Don't use this /// value directly, instead use `IS_PATHRS_ERR(ret)` to check if a returned /// value is an error or not. /// /// NOTE: This is used internally by libpathrs. You should avoid using this /// macro if possible. // We avoid using anything in -4096..0 to avoid users interpreting the return // value as an -errno (at the moment, the largest errno is ~150 but the kernel // currently reserves 4096 values as possible ERR_PTR values). pub const __PATHRS_MAX_ERR_VALUE: CReturn = -4096; // TODO: Switch this to using a slab or similar structure, possibly using a less // heavy-weight lock? // MSRV(1.80): Use LazyLock. static ERROR_MAP: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); pub(crate) fn store_error(err: Error) -> CReturn { let mut err_map = ERROR_MAP.lock().unwrap(); // Try to find a negative error value we can use. let mut g = rand::rng(); loop { let idx = g.random_range(CReturn::MIN..__PATHRS_MAX_ERR_VALUE); match err_map.entry(idx) { HashMapEntry::Occupied(_) => continue, HashMapEntry::Vacant(slot) => { slot.insert(err); break idx; } } } } /// Attempts to represent a Rust Error type in C. This structure must be freed /// using pathrs_errorinfo_free(). // NOTE: This API is exposed to library users in a read-only manner with memory // management done by libpathrs -- so you may only ever append to it. #[repr(align(8), C)] pub struct CError { // TODO: Put a version or size here so that C users can tell what fields are // valid if we add fields in the future. /// Raw errno(3) value of the underlying error (or 0 if the source of the /// error was not due to a syscall error). // We can't call this field "errno" because glibc defines errno(3) as a // macro, causing all sorts of problems if you have a struct with an "errno" // field. Best to avoid those headaches. pub saved_errno: u64, /// Textual description of the error. pub description: *const c_char, } impl Leakable for CError {} impl From<&Error> for CError { /// Construct a new CError struct based on the given error. The description /// is pretty-printed in a C-like manner (causes are appended to one another /// with separating colons). In addition, if the root-cause of the error is /// an IOError then errno is populated with that value. fn from(err: &Error) -> Self { // TODO: Switch to Error::chain() once it's stabilised. // let desc = { let mut desc = err.to_string(); let mut err: &dyn StdError = err; while let Some(next) = err.source() { desc.push_str(": "); desc.push_str(&next.to_string()); err = next; } // Create a C-compatible string for CError.description. CString::new(desc).expect("CString::new(description) failed in CError generation") }; // Map the error to a C errno if possible. // TODO: We might want to use ESERVERFAULT (An untranslatable error // occurred) for untranslatable errors? let saved_errno = err.kind().errno().unwrap_or(0).unsigned_abs(); CError { saved_errno: saved_errno.into(), description: desc.into_raw(), } } } impl Drop for CError { fn drop(&mut self) { if !self.description.is_null() { let description = self.description as *mut c_char; // Clear the pointer to avoid double-frees. self.description = ptr::null_mut(); // SAFETY: CString::from_raw is safe because the C caller guarantees // that the pointer we get is the same one we gave them. let _ = unsafe { CString::from_raw(description) }; // drop the CString } } } /// Retrieve error information about an error id returned by a pathrs operation. /// /// Whenever an error occurs with libpathrs, a negative number describing that /// error (the error id) is returned. pathrs_errorinfo() is used to retrieve /// that information: /// /// ```c /// fd = pathrs_inroot_resolve(root, "/foo/bar"); /// if (IS_PATHRS_ERR(fd)) { /// // fd is an error id /// pathrs_error_t *error = pathrs_errorinfo(fd); /// // ... print the error information ... /// pathrs_errorinfo_free(error); /// } /// ``` /// /// Once pathrs_errorinfo() is called for a particular error id, that error id /// is no longer valid and should not be used for subsequent pathrs_errorinfo() /// calls. /// /// Error ids are only unique from one another until pathrs_errorinfo() is /// called, at which point the id can be reused for subsequent errors. The /// precise format of error ids is completely opaque and they should never be /// compared directly or used for anything other than with pathrs_errorinfo(). /// /// Error ids are not thread-specific and thus pathrs_errorinfo() can be called /// on a different thread to the thread where the operation failed (this is of /// particular note to green-thread language bindings like Go, where this is /// important). /// /// # Return Value /// /// If there was a saved error with the provided id, a pathrs_error_t is /// returned describing the error. Use pathrs_errorinfo_free() to free the /// associated memory once you are done with the error. #[no_mangle] pub unsafe extern "C" fn pathrs_errorinfo(err_id: c_int) -> Option<&'static mut CError> { let mut err_map = ERROR_MAP.lock().unwrap(); err_map .remove(&err_id) .as_ref() .map(CError::from) .map(Leakable::leak) } utils::symver! { fn pathrs_errorinfo <- (pathrs_errorinfo, version = "LIBPATHRS_0.1", default); } /// Free the pathrs_error_t object returned by pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_errorinfo_free(ptr: *mut CError) { if ptr.is_null() { return; } // SAFETY: The C caller guarantees that the pointer is of the correct type // and that this isn't a double-free. unsafe { (*ptr).free() } } utils::symver! { fn pathrs_errorinfo_free <- (pathrs_errorinfo_free, version = "LIBPATHRS_0.1", default); } #[cfg(test)] mod tests { use super::*; use crate::error::{Error, ErrorImpl}; use std::io::Error as IOError; use pretty_assertions::assert_eq; #[test] fn cerror_ioerror_errno() { let err = Error::from(ErrorImpl::OsError { operation: "fake operation".into(), source: IOError::from_raw_os_error(libc::ENOANO), }); assert_eq!( err.kind().errno(), Some(libc::ENOANO), "basic kind().errno() should return the right error" ); let cerr = CError::from(&err); assert_eq!( cerr.saved_errno, libc::ENOANO as u64, "cerror should contain errno for OsError" ); } #[test] fn cerror_einval_errno() { let err = Error::from(ErrorImpl::InvalidArgument { name: "fake argument".into(), description: "fake description".into(), }); assert_eq!( err.kind().errno(), Some(libc::EINVAL), "InvalidArgument kind().errno() should return the right error" ); let cerr = CError::from(&err); assert_eq!( cerr.saved_errno, libc::EINVAL as u64, "cerror should contain EINVAL errno for InvalidArgument" ); } #[test] fn cerror_enosys_errno() { let err = Error::from(ErrorImpl::NotImplemented { feature: "fake feature".into(), }); assert_eq!( err.kind().errno(), Some(libc::ENOSYS), "NotImplemented kind().errno() should return the right error" ); let cerr = CError::from(&err); assert_eq!( cerr.saved_errno, libc::ENOSYS as u64, "cerror should contain ENOSYS errno for NotImplemented" ); } #[test] fn cerror_exdev_errno() { let err = Error::from(ErrorImpl::SafetyViolation { description: "fake safety violation".into(), }); assert_eq!( err.kind().errno(), Some(libc::EXDEV), "SafetyViolation kind().errno() should return the right error" ); let cerr = CError::from(&err); assert_eq!( cerr.saved_errno, libc::EXDEV as u64, "cerror should contain EXDEV errno for SafetyViolation" ); } #[test] fn cerror_no_errno() { let parse_err = "a123".parse::().unwrap_err(); let err = Error::from(parse_err); assert_eq!( err.kind().errno(), None, "ParseIntError kind().errno() should return no errno" ); let cerr = CError::from(&err); assert_eq!( cerr.saved_errno, 0, "cerror should contain zero errno for ParseIntError" ); } } pathrs-0.2.1/src/capi/procfs.rs000064400000000000000000001131141046102023000144730ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ capi::{ ret::IntoCReturn, utils::{self, CBorrowedFd}, }, error::{Error, ErrorExt, ErrorImpl}, flags::OpenFlags, procfs::{ProcfsBase, ProcfsHandle, ProcfsHandleBuilder, ProcfsHandleRef}, }; use std::os::unix::io::{AsRawFd, IntoRawFd, OwnedFd, RawFd}; use bitflags::bitflags; use bytemuck::{Pod, Zeroable}; use libc::{c_char, c_int, size_t}; use open_enum::open_enum; /// Bits in `pathrs_proc_base_t` that indicate the type of the base value. /// /// NOTE: This is used internally by libpathrs. You should avoid using this /// macro if possible. pub const __PATHRS_PROC_TYPE_MASK: u64 = 0xFFFF_FFFF_0000_0000; /// Bits in `pathrs_proc_base_t` that must be set for "special" `PATHRS_PROC_*` /// values. const __PATHRS_PROC_TYPE_SPECIAL: u64 = 0xFFFF_FFFE_0000_0000; // Make sure that __PATHRS_PROC_TYPE_SPECIAL only uses bits in the mask. static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_SPECIAL, __PATHRS_PROC_TYPE_SPECIAL & __PATHRS_PROC_TYPE_MASK, ); /// Bits in `pathrs_proc_base_t` that must be set for `/proc/$pid` values. Don't /// use this directly, instead use `PATHRS_PROC_PID(n)` to convert a PID to an /// appropriate `pathrs_proc_base_t` value. /// /// NOTE: This is used internally by libpathrs. You should avoid using this /// macro if possible. // For future-proofing the top 32 bits are blocked off by the mask, but in case // we ever need to expand the size of pid_t (incredibly unlikely) we only use // the top-most bit. pub const __PATHRS_PROC_TYPE_PID: u64 = 0x8000_0000_0000_0000; // Make sure that __PATHRS_PROC_TYPE_PID only uses bits in the mask. static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_PID, __PATHRS_PROC_TYPE_PID & __PATHRS_PROC_TYPE_MASK, ); /// Indicate what base directory should be used when doing operations with /// `pathrs_proc_*`. In addition to the values defined here, the following /// macros can be used for other values: /// /// * `PATHRS_PROC_PID(pid)` refers to the `/proc/` directory for the /// process with PID (or TID) `pid`. /// /// Note that this operation is inherently racy and should probably avoided /// for most uses -- see the block comment above `PATHRS_PROC_PID(n)` for /// more details. /// /// Unknown values will result in an error being returned. // NOTE: We need to open-code the values in the definition because cbindgen // cannot yet evaluate constexprs (see <>) and both Go's CGo and Python's cffi // struggle to deal with non-constant values (actually CGo struggles even with // unsigned literals -- see ). #[open_enum] #[repr(u64)] #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[allow(non_camel_case_types, dead_code)] #[allow(clippy::unusual_byte_groupings)] // FIXME: workaround for pub enum CProcfsBase { /// Use /proc. Note that this mode may be more expensive because we have /// to take steps to try to avoid leaking unmasked procfs handles, so you /// should use PATHRS_PROC_SELF if you can. PATHRS_PROC_ROOT = 0xFFFF_FFFE_7072_6F63u64, // "proc" /// Use /proc/self. For most programs, this is the standard choice. PATHRS_PROC_SELF = 0xFFFF_FFFE_091D_5E1Fu64, // pid-self /// Use /proc/thread-self. In multi-threaded programs where one thread has a /// different CLONE_FS, it is possible for /proc/self to point the wrong /// thread and so /proc/thread-self may be necessary. /// /// NOTE: Using /proc/thread-self may require care if used from languages /// where your code can change threads without warning and old threads can /// be killed (such as Go -- where you want to use runtime.LockOSThread). PATHRS_PROC_THREAD_SELF = 0xFFFF_FFFE_3EAD_5E1Fu64, // thread-self } // Make sure the defined special values have the right type flag and the right // values. The value checks are critical because we must not change these -- // changing them will break API compatibility silently. static_assertions::const_assert_eq!(0xFFFFFFFE70726F63, CProcfsBase::PATHRS_PROC_ROOT.0); static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_SPECIAL | 0x7072_6F63, CProcfsBase::PATHRS_PROC_ROOT.0, ); static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_SPECIAL, CProcfsBase::PATHRS_PROC_ROOT.0 & __PATHRS_PROC_TYPE_MASK, ); static_assertions::const_assert_eq!(0xFFFFFFFE091D5E1F, CProcfsBase::PATHRS_PROC_SELF.0); static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_SPECIAL | 0x091D_5E1F, CProcfsBase::PATHRS_PROC_SELF.0, ); static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_SPECIAL, CProcfsBase::PATHRS_PROC_SELF.0 & __PATHRS_PROC_TYPE_MASK, ); static_assertions::const_assert_eq!(0xFFFFFFFE3EAD5E1F, CProcfsBase::PATHRS_PROC_THREAD_SELF.0); static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_SPECIAL | 0x3EAD_5E1F, CProcfsBase::PATHRS_PROC_THREAD_SELF.0, ); static_assertions::const_assert_eq!( __PATHRS_PROC_TYPE_SPECIAL, CProcfsBase::PATHRS_PROC_THREAD_SELF.0 & __PATHRS_PROC_TYPE_MASK, ); impl TryFrom for ProcfsBase { type Error = Error; fn try_from(c_base: CProcfsBase) -> Result { // Cannot be used inline in a pattern. const U32_MAX: u64 = u32::MAX as _; const U32_MAX_PLUS_ONE: u64 = U32_MAX + 1; match c_base { CProcfsBase::PATHRS_PROC_ROOT => Ok(ProcfsBase::ProcRoot), CProcfsBase::PATHRS_PROC_SELF => Ok(ProcfsBase::ProcSelf), CProcfsBase::PATHRS_PROC_THREAD_SELF => Ok(ProcfsBase::ProcThreadSelf), CProcfsBase(arg) => match ( arg & __PATHRS_PROC_TYPE_MASK, arg & !__PATHRS_PROC_TYPE_MASK, ) { // Make sure that we never run into a situation where the // argument value doesn't fit in a u32. If we ever need to // support this, we will need additional code changes. (_, value @ U32_MAX_PLUS_ONE..) => { // This should really never actually happen, so ensure we // have a compile-time check to avoid it. static_assertions::const_assert_eq!(!__PATHRS_PROC_TYPE_MASK, u32::MAX as _); // And mark this branch as unreachable. unreachable!("the value portion of CProcfsBase({arg:#x}) cannot be larger than u32 (but {value:#x} is)"); } // PATHRS_PROC_PID(pid) (__PATHRS_PROC_TYPE_PID, pid @ 1..=U32_MAX) => { // Just make sure... static_assertions::const_assert_eq!(U32_MAX, u32::MAX as u64); // We can be sure it's okay to cast to u32, as we've checked // statically and at runtime that the value is within the // correct range to not truncate bits. Ok(ProcfsBase::ProcPid(pid as u32)) } // Error fallbacks for invalid subvalues or types. (base_type, value) => Err(ErrorImpl::InvalidArgument { name: "procfs base".into(), description: match base_type { __PATHRS_PROC_TYPE_SPECIAL => format!("{arg:#X} is an invalid special procfs base (unknown sub-value {value:#X})"), __PATHRS_PROC_TYPE_PID => format!("pid {value} is an invalid value for PATHRS_PROC_PID"), _ => format!("{arg:#X} has an unknown procfs base type {base_type:#X}"), }.into(), }.into()), } .wrap("the procfs base must be one of the PATHRS_PROC_* values or PATHRS_PROC_PID(n)") } } } #[cfg(test)] impl From for CProcfsBase { fn from(base: ProcfsBase) -> Self { match base { ProcfsBase::ProcPid(pid) => { // TODO: See if we can add some kind of static assertion that // the type of the pid is not larger than the reserved // block in __PATHRS_PROC_TYPE_MASK. Unfortunately Rust // doesn't have a way of doing typeof(pid)... Maybe // pattern_types would let us do this with something like // ProcfsBase::ProcPid::0::MAX? // // static_assertions::const_assert_eq!( // type_of::MAX & _PATHRS_PROC_TYPE_MASK, // 0, // ); // static_assertions::const_assert_eq!(type_of::MAX, u32::MAX); // We know this to be true from the check in the above TryFrom // impl for ProcfsBase, but add an assertion here since we // cannot actually verify this statically at the moment. #[allow(clippy::absurd_extreme_comparisons)] { assert!(pid <= u32::MAX, "pid in CProcfsBase must fit inside a u32"); } assert_eq!( pid as u64 & __PATHRS_PROC_TYPE_MASK, 0, "invalid pid found when converting to CProcfsBase -- pid {pid} includes type bits ({__PATHRS_PROC_TYPE_MASK:#X})" ); CProcfsBase(__PATHRS_PROC_TYPE_PID | pid as u64) } ProcfsBase::ProcRoot => CProcfsBase::PATHRS_PROC_ROOT, ProcfsBase::ProcSelf => CProcfsBase::PATHRS_PROC_SELF, ProcfsBase::ProcThreadSelf => CProcfsBase::PATHRS_PROC_THREAD_SELF, } } } /// A sentinel value to tell `pathrs_proc_*` methods to use the default procfs /// root handle (which may be globally cached). pub const PATHRS_PROC_DEFAULT_ROOTFD: CBorrowedFd<'static> = // SAFETY: The lifetime of fake fd values are 'static. unsafe { CBorrowedFd::from_raw_fd(-libc::EBADF) }; fn parse_proc_rootfd<'fd>(fd: CBorrowedFd<'fd>) -> Result, Error> { match fd { PATHRS_PROC_DEFAULT_ROOTFD => ProcfsHandle::new(), _ => ProcfsHandleRef::try_from_borrowed_fd(fd.try_as_borrowed_fd()?), } } // This is needed because the macro expansion cbindgen produces for // bitflags! is not really usable for regular structs. /// Construct a completely unmasked procfs handle. /// /// This is equivalent to [`ProcfsHandleBuilder::unmasked`], and is meant as /// a flag argument to [`ProcfsOpenFlags`] (the `flags` field in `struct /// pathrs_procfs_open_how`) for use with pathrs_procfs_open(). pub const PATHRS_PROCFS_NEW_UNMASKED: u64 = 0x0000_0000_0000_0001; bitflags! { #[repr(C)] #[derive(Default, Debug, Clone, Copy, Pod, Zeroable)] pub struct ProcfsOpenFlags: u64 { const PATHRS_PROCFS_NEW_UNMASKED = PATHRS_PROCFS_NEW_UNMASKED; // NOTE: Make sure to add a `pub const` for any new flags to make // sure they show up when cbindgen generates our header. } } impl ProcfsOpenFlags { const fn contains_unknown_bits(&self) -> bool { Self::from_bits(self.bits()).is_none() } } static_assertions::const_assert_eq!( ProcfsOpenFlags::PATHRS_PROCFS_NEW_UNMASKED.contains_unknown_bits(), false, ); static_assertions::const_assert_eq!( ProcfsOpenFlags::from_bits_retain(0x1000_0000).contains_unknown_bits(), true, ); static_assertions::const_assert_eq!( ProcfsOpenFlags::from_bits_retain(0xF000_0001).contains_unknown_bits(), true, ); #[repr(C)] #[derive(Default, Debug, Clone, Copy, Pod, Zeroable)] pub struct ProcfsOpenHow { pub flags: ProcfsOpenFlags, } impl ProcfsOpenHow { fn into_builder(self) -> Result { let mut builder = ProcfsHandleBuilder::new(); if self.flags.contains_unknown_bits() { return Err(ErrorImpl::InvalidArgument { name: "flags".into(), description: format!( "contains unknown flag bits {:#x}", self.flags.difference(ProcfsOpenFlags::all()) ) .into(), })?; } if self .flags .contains(ProcfsOpenFlags::PATHRS_PROCFS_NEW_UNMASKED) { builder.set_unmasked(); } Ok(builder) } } /// Create a new (custom) procfs root handle. /// /// This is effectively a C wrapper around [`ProcfsHandleBuilder`], allowing you /// to create a custom procfs root handle that can be used with other /// `pathrs_proc_*at` methods. /// /// While most users should just use `PATHRS_PROC_DEFAULT_ROOTFD` (or the /// non-`at` variants of `pathrs_proc_*`), creating an unmasked procfs root /// handle (using `PATHRS_PROCFS_NEW_UNMASKED`) can be useful for programs that /// need to operate on a lot of global procfs files. (Note that accessing global /// procfs files does not *require* creating a custom procfs handle -- /// `pathrs_proc_*` will automatically create a global-friendly handle /// internally when necessary but will close it immediately after operating on /// it.) /// /// # Extensible Structs /// /// The [`ProcfsOpenHow`] (`struct pathrs_procfs_open_how`) argument is /// designed to be extensible, modelled after the extensible structs scheme used /// by Linux (for syscalls such as [clone3(2)], [openat2(2)] and other such /// syscalls). Normally one would use symbol versioning to achieve this, but /// unfortunately Rust's symbol versioning support is incredibly primitive (one /// might even say "non-existent") and so this system is more robust, even if /// the calling convention is a little strange for userspace libraries. /// /// In addition to a pointer argument, the caller must also provide the size of /// the structure it is passing. By providing this information, it is possible /// for `pathrs_procfs_open()` to provide both forwards- and /// backwards-compatibility, with size acting as an implicit version number. /// (Because new extension fields will always be appended, the structure size /// will always increase.) /// /// If we let `usize` be the structure specified by the caller, and `lsize` be /// the size of the structure internal to libpathrs, then there are three cases /// to consider: /// /// * If `usize == lsize`, then there is no version mismatch and the structure /// provided by the caller can be used verbatim. /// * If `usize < lsize`, then there are some extension fields which libpathrs /// supports that the caller does not. Because a zero value in any added /// extension field signifies a no-op, libpathrs treats all of the extension /// fields not provided by the caller as having zero values. This provides /// backwards-compatibility. /// * If `usize > lsize`, then there are some extension fields which the caller /// is aware of but this version of libpathrs does not support. Because any /// extension field must have its zero values signify a no-op, libpathrs can /// safely ignore the unsupported extension fields if they are all-zero. If /// any unsupported extension fields are nonzero, then an `E2BIG` error is /// returned. This provides forwards-compatibility. /// /// Because the definition of `struct pathrs_procfs_open_how` may open in the /// future /// /// Because the definition of `struct pathrs_procfs_open_how` may change in the /// future (with new fields being added when headers are updated), callers /// should zero-fill the structure to ensure that recompiling the program with /// new headers will not result in spurious errors at run time. The simplest /// way is to use a designated initialiser: /// /// ```c /// struct pathrs_procfs_open_how how = { /// .flags = PATHRS_PROCFS_NEW_UNMASKED, /// }; /// ``` /// /// or explicitly using `memset(3)` or similar: /// /// ```c /// struct pathrs_procfs_open_how how; /// memset(&how, 0, sizeof(how)); /// how.flags = PATHRS_PROCFS_NEW_UNMASKED; /// ``` /// /// # Return Value /// /// On success, this function returns *either* a file descriptor *or* /// `PATHRS_PROC_DEFAULT_ROOTFD` (this is a negative number, equal to `-EBADF`). /// The file descriptor will have the `O_CLOEXEC` flag automatically applied. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). /// /// [clone3(2)]: https://www.man7.org/linux/man-pages/man2/clone3.2.html /// [openat2(2)]: https://www.man7.org/linux/man-pages/man2/openat2.2.html #[no_mangle] pub unsafe extern "C" fn pathrs_procfs_open(args: *const ProcfsOpenHow, size: usize) -> RawFd { || -> Result<_, Error> { unsafe { utils::copy_from_extensible_struct(args, size) }? .into_builder()? .build() .map(ProcfsHandle::into_owned_fd) }() .map(|fd| match fd { Some(fd) => fd.into_raw_fd(), None => PATHRS_PROC_DEFAULT_ROOTFD.as_raw_fd(), }) .into_c_return() } utils::symver! { fn pathrs_procfs_open <- (pathrs_procfs_open, version = "LIBPATHRS_0.2", default); } /// `pathrs_proc_open` but with a caller-provided file descriptor for `/proc`. /// /// Internally, `pathrs_proc_open` will attempt to use a cached copy of a very /// restricted `/proc` handle (a detached mount object with `subset=pid` and /// `hidepid=4`). If a user requests a global `/proc` file, a temporary handle /// capable of accessing global files is created and destroyed after the /// operation completes. /// /// For most users, this is more than sufficient. However, if a user needs to /// operate on many global `/proc` files, the cost of creating handles can get /// quite expensive. `pathrs_proc_openat` allows a user to manually manage the /// global-friendly `/proc` handle. Note that passing a `subset=pid` file /// descriptor to `pathrs_proc_openat` will *not* stop the automatic creation of /// a global-friendly handle internally if necessary. /// /// In order to get the behaviour of `pathrs_proc_open`, you can pass the /// special value `PATHRS_PROC_DEFAULT_ROOTFD` (`-EBADF`) as the `proc_rootfd` /// argument. /// /// # Return Value /// /// On success, this function returns a file descriptor. The file descriptor /// will have the `O_CLOEXEC` flag automatically applied. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_proc_openat( proc_rootfd: CBorrowedFd<'_>, base: CProcfsBase, path: *const c_char, flags: c_int, ) -> RawFd { || -> Result<_, Error> { let base = base.try_into()?; let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. let oflags = OpenFlags::from_bits_retain(flags); let procfs = parse_proc_rootfd(proc_rootfd)?; if oflags.contains(OpenFlags::O_NOFOLLOW) { procfs.open(base, path, oflags) } else { procfs.open_follow(base, path, oflags) } }() .map(OwnedFd::from) .into_c_return() } utils::symver! { fn pathrs_proc_openat <- (pathrs_proc_openat, version = "LIBPATHRS_0.2", default); } /// Safely open a path inside a `/proc` handle. /// /// Any bind-mounts or other over-mounts will (depending on what kernel features /// are available) be detected and an error will be returned. Non-trailing /// symlinks are followed but care is taken to ensure the symlinks are /// legitimate. /// /// Unless you intend to open a magic-link, `O_NOFOLLOW` should be set in flags. /// Lookups with `O_NOFOLLOW` are guaranteed to never be tricked by bind-mounts /// (on new enough Linux kernels). /// /// If you wish to resolve a magic-link, you need to unset `O_NOFOLLOW`. /// Unfortunately (if libpathrs is using the regular host `/proc` mount), this /// lookup mode cannot protect you against an attacker that can modify the mount /// table during this operation. /// /// NOTE: Instead of using paths like `/proc/thread-self/fd`, `base` is used to /// indicate what "base path" inside procfs is used. For example, to re-open a /// file descriptor: /// /// ```c /// fd = pathrs_proc_open(PATHRS_PROC_THREAD_SELF, "fd/101", O_RDWR); /// if (IS_PATHRS_ERR(fd)) { /// liberr = fd; // for use with pathrs_errorinfo() /// goto err; /// } /// ``` /// /// # Return Value /// /// On success, this function returns a file descriptor. The file descriptor /// will have the `O_CLOEXEC` flag automatically applied. /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_proc_open( base: CProcfsBase, path: *const c_char, flags: c_int, ) -> RawFd { pathrs_proc_openat(PATHRS_PROC_DEFAULT_ROOTFD, base, path, flags) } utils::symver! { fn pathrs_proc_open <- (pathrs_proc_open, version = "LIBPATHRS_0.1", default); } /// `pathrs_proc_readlink` but with a caller-provided file descriptor for /// `/proc`. /// /// See the documentation of pathrs_proc_openat() for when this API might be /// useful. /// /// # Return Value /// /// On success, this function copies the symlink contents to `linkbuf` (up to /// `linkbuf_size` bytes) and returns the full size of the symlink path buffer. /// This function will not copy the trailing NUL byte, and the return size does /// not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are /// treated as zero-size buffers. /// /// NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to /// contain the symlink contents, pathrs_proc_readlink() will return *the number /// of bytes it would have copied if the buffer was large enough*. This matches /// the behaviour of pathrs_inroot_readlink(). /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_proc_readlinkat( proc_rootfd: CBorrowedFd<'_>, base: CProcfsBase, path: *const c_char, linkbuf: *mut c_char, linkbuf_size: size_t, ) -> c_int { || -> Result<_, Error> { let base = base.try_into()?; let path = unsafe { utils::parse_path(path) }?; // SAFETY: C caller guarantees path is safe. let procfs = parse_proc_rootfd(proc_rootfd)?; let link_target = procfs.readlink(base, path)?; // SAFETY: C caller guarantees buffer is at least linkbuf_size and can // be written to. unsafe { utils::copy_path_into_buffer(link_target, linkbuf, linkbuf_size) } }() .into_c_return() } utils::symver! { fn pathrs_proc_readlinkat <- (pathrs_proc_readlinkat, version = "LIBPATHRS_0.2", default); } /// Safely read the contents of a symlink inside `/proc`. /// /// As with `pathrs_proc_open`, any bind-mounts or other over-mounts will /// (depending on what kernel features are available) be detected and an error /// will be returned. Non-trailing symlinks are followed but care is taken to /// ensure the symlinks are legitimate. /// /// This function is effectively shorthand for /// /// ```c /// fd = pathrs_proc_open(base, path, O_PATH|O_NOFOLLOW); /// if (IS_PATHRS_ERR(fd)) { /// liberr = fd; // for use with pathrs_errorinfo() /// goto err; /// } /// copied = readlinkat(fd, "", linkbuf, linkbuf_size); /// close(fd); /// ``` /// /// # Return Value /// /// On success, this function copies the symlink contents to `linkbuf` (up to /// `linkbuf_size` bytes) and returns the full size of the symlink path buffer. /// This function will not copy the trailing NUL byte, and the return size does /// not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are /// treated as zero-size buffers. /// /// NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to /// contain the symlink contents, pathrs_proc_readlink() will return *the number /// of bytes it would have copied if the buffer was large enough*. This matches /// the behaviour of pathrs_inroot_readlink(). /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, /// the system errno(7) value associated with the error, etc), use /// pathrs_errorinfo(). #[no_mangle] pub unsafe extern "C" fn pathrs_proc_readlink( base: CProcfsBase, path: *const c_char, linkbuf: *mut c_char, linkbuf_size: size_t, ) -> c_int { pathrs_proc_readlinkat( PATHRS_PROC_DEFAULT_ROOTFD, base, path, linkbuf, linkbuf_size, ) } utils::symver! { fn pathrs_proc_readlink <- (pathrs_proc_readlink, version = "LIBPATHRS_0.1", default); } #[cfg(test)] mod tests { use super::*; use crate::{ capi::{error as capi_error, utils::Leakable}, error::ErrorKind, procfs::ProcfsBase, }; use std::{ mem, os::unix::io::{FromRawFd, OwnedFd}, }; use pretty_assertions::assert_eq; #[test] fn procfsbase_try_from_crepr_procroot() { assert_eq!( ProcfsBase::try_from(CProcfsBase::PATHRS_PROC_ROOT).map_err(|e| e.kind()), Ok(ProcfsBase::ProcRoot), "PATHRS_PROC_ROOT.try_into()" ); } #[test] fn procfsbase_try_from_crepr_procself() { assert_eq!( ProcfsBase::try_from(CProcfsBase::PATHRS_PROC_SELF).map_err(|e| e.kind()), Ok(ProcfsBase::ProcSelf), "PATHRS_PROC_SELF.try_into()" ); } #[test] fn procfsbase_try_from_crepr_procthreadself() { assert_eq!( ProcfsBase::try_from(CProcfsBase::PATHRS_PROC_THREAD_SELF).map_err(|e| e.kind()), Ok(ProcfsBase::ProcThreadSelf), "PATHRS_PROC_THREAD_SELF.try_into()" ); } #[test] fn procfsbase_try_from_crepr_procpid() { assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_PID | 1)).map_err(|e| e.kind()), Ok(ProcfsBase::ProcPid(1)), "PATHRS_PROC_PID(12345).try_into()" ); assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_PID | 12345)).map_err(|e| e.kind()), Ok(ProcfsBase::ProcPid(12345)), "PATHRS_PROC_PID(12345).try_into()" ); assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_PID | u32::MAX as u64)) .map_err(|e| e.kind()), Ok(ProcfsBase::ProcPid(u32::MAX)), "PATHRS_PROC_PID(u32::MAX).try_into()" ); } #[test] fn procfsbase_try_from_crepr_procspecial_invalid() { // Invalid __PATHRS_PROC_TYPE_SPECIAL values. assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_SPECIAL)).map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "__PATHRS_PROC_TYPE_SPECIAL.try_into() -- invalid type" ); assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_SPECIAL | 0xDEADBEEF)) .map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "(__PATHRS_PROC_TYPE_SPECIAL | 0xDEADBEEF).try_into() -- invalid type" ); } #[test] fn procfsbase_try_from_crepr_procpid_invalid() { // 0 is an invalid pid. assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_PID)).map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "PATHRS_PROC_PID(0).try_into() -- invalid pid" ); // u32::MAX + 1 is an invalid value for multiple reasons. assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_PID | (u32::MAX as u64 + 1))) .map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "PATHRS_PROC_PID(u32::MAX + 1).try_into() -- invalid pid" ); } #[test] fn procfsbase_try_from_crepr_proctype_invalid() { // Invalid __PATHRS_PROC_TYPE_MASK values. assert_eq!( ProcfsBase::try_from(CProcfsBase(0xDEAD_BEEF_0000_0001)).map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "0xDEAD_BEEF_0000_0001.try_into() -- invalid type" ); assert_eq!( ProcfsBase::try_from(CProcfsBase(0xDEAD_BEEF_3EAD_5E1F)).map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "0xDEAD_BEEF_3EAD_5E1F.try_into() -- invalid type" ); assert_eq!( ProcfsBase::try_from(CProcfsBase(__PATHRS_PROC_TYPE_MASK)).map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "__PATHRS_PROC_TYPE_MASK.try_into() -- invalid type" ); } #[test] fn procfsbase_try_from_crepr_invalid() { // Plain values are invalid. assert_eq!( ProcfsBase::try_from(CProcfsBase(0)).map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "(0).try_into() -- invalid value" ); assert_eq!( ProcfsBase::try_from(CProcfsBase(0xDEADBEEF)).map_err(|e| e.kind()), Err(ErrorKind::InvalidArgument), "(0xDEADBEEF).try_into() -- invalid value" ); } #[test] fn procfsbase_into_crepr_procroot() { assert_eq!( CProcfsBase::from(ProcfsBase::ProcRoot), CProcfsBase::PATHRS_PROC_ROOT, "ProcRoot.into() == PATHRS_PROC_ROOT" ); } #[test] fn procfsbase_into_crepr_procself() { assert_eq!( CProcfsBase::from(ProcfsBase::ProcSelf), CProcfsBase::PATHRS_PROC_SELF, "ProcSelf.into() == PATHRS_PROC_SELF" ); } #[test] fn procfsbase_into_crepr_procthreadself() { assert_eq!( CProcfsBase::from(ProcfsBase::ProcThreadSelf), CProcfsBase::PATHRS_PROC_THREAD_SELF, "ProcThreadSelf.into() == PATHRS_PROC_THREAD_SELF" ); } #[test] fn procfsbase_into_crepr_procpid() { assert_eq!( CProcfsBase::from(ProcfsBase::ProcPid(1)), CProcfsBase(__PATHRS_PROC_TYPE_PID | 1), "ProcPid(1).into() == 1" ); assert_eq!( CProcfsBase::from(ProcfsBase::ProcPid(1122334455)), CProcfsBase(__PATHRS_PROC_TYPE_PID | 1122334455), "ProcPid(1122334455).into() == 1122334455" ); } fn check_round_trip(rust: ProcfsBase, c: CProcfsBase) { let c_to_rust: ProcfsBase = c.try_into().expect("should be valid value"); assert_eq!( rust, c_to_rust, "c-to-rust ProcfsBase conversion ({c:?}.try_into())" ); let rust_to_c: CProcfsBase = rust.into(); assert_eq!( c, rust_to_c, "rust-to-c ProcfsBase conversion ({rust:?}.into())" ); let c_to_rust_to_c: CProcfsBase = c_to_rust.into(); assert_eq!( c, c_to_rust_to_c, "rust-to-c-to-rust ProcfsBase conversion ({c_to_rust:?}.into())" ); let rust_to_c_to_rust: ProcfsBase = rust_to_c .try_into() .expect("must be valid value when round-tripping"); assert_eq!( rust, rust_to_c_to_rust, "rust-to-c-to-rust ProcfsBase conversion ({rust_to_c:?}.try_into())" ); } #[test] fn procfsbase_round_trip_procroot() { check_round_trip(ProcfsBase::ProcRoot, CProcfsBase::PATHRS_PROC_ROOT); } #[test] fn procfsbase_round_trip_procself() { check_round_trip(ProcfsBase::ProcSelf, CProcfsBase::PATHRS_PROC_SELF); } #[test] fn procfsbase_round_trip_procthreadself() { check_round_trip( ProcfsBase::ProcThreadSelf, CProcfsBase::PATHRS_PROC_THREAD_SELF, ); } #[test] fn procfsbase_round_trip_procpid() { check_round_trip( ProcfsBase::ProcPid(1), CProcfsBase(__PATHRS_PROC_TYPE_PID | 1), ); check_round_trip( ProcfsBase::ProcPid(12345), CProcfsBase(__PATHRS_PROC_TYPE_PID | 12345), ); check_round_trip( ProcfsBase::ProcPid(1122334455), CProcfsBase(__PATHRS_PROC_TYPE_PID | 1122334455), ); check_round_trip( ProcfsBase::ProcPid(u32::MAX), CProcfsBase(__PATHRS_PROC_TYPE_PID | u32::MAX as u64), ); } #[test] fn pathrs_procfs_open_cached() { let procfs_is_cached = ProcfsHandle::new() .expect("ProcfsHandle::new should not fail") .into_owned_fd() .is_none(); let how = ProcfsOpenHow::default(); let fd = unsafe { pathrs_procfs_open(&how as *const _, mem::size_of::()) }; let procfs = if procfs_is_cached { assert_eq!( fd, PATHRS_PROC_DEFAULT_ROOTFD.as_raw_fd(), "if ProcfsHandle::new() is cached then pathrs_procfs_open() should return PATHRS_PROC_DEFAULT_ROOTFD", ); ProcfsHandle::new() } else { ProcfsHandle::try_from_fd(unsafe { OwnedFd::from_raw_fd(fd) }) }.expect("pathrs_procfs_open should return a valid procfs fd"); let _ = procfs .open(ProcfsBase::ProcSelf, ".", OpenFlags::O_PATH) .expect("open(.) should always succeed"); } #[test] fn pathrs_procfs_open_unmasked() { let how = ProcfsOpenHow { flags: ProcfsOpenFlags::PATHRS_PROCFS_NEW_UNMASKED, }; let fd = unsafe { pathrs_procfs_open(&how as *const _, mem::size_of::()) }; assert!(fd >= 0, "fd value {fd:#x} should be >= 0"); let procfs = ProcfsHandle::try_from_fd(unsafe { OwnedFd::from_raw_fd(fd) }) .expect("pathrs_procfs_open should return a valid procfs fd"); let _ = procfs .open(ProcfsBase::ProcSelf, ".", OpenFlags::O_PATH) .expect("open(.) should always succeed"); } #[test] fn pathrs_procfs_open_bad_flag() { let how_bad_flags = ProcfsOpenHow { flags: ProcfsOpenFlags::from_bits_retain(0xF000), }; let ret = unsafe { pathrs_procfs_open(&how_bad_flags as *const _, mem::size_of::()) }; assert!( ret < capi_error::__PATHRS_MAX_ERR_VALUE, "ret value {ret:#x} should be error value" ); { let err = unsafe { capi_error::pathrs_errorinfo(ret) .expect("error must be retrievable") .unleak() }; // err.kind() == ErrorKind::InvalidArgument assert_eq!( err.saved_errno, libc::EINVAL as _, "invalid flag should return EINVAL" ); } } #[test] fn pathrs_procfs_open_bad_struct() { #[repr(C)] #[derive(Default, Debug, Clone, Copy, Pod, Zeroable)] struct ProcfsOpenHowV2 { inner: ProcfsOpenHow, extra: u64, } let how_ok_struct = ProcfsOpenHowV2 { inner: ProcfsOpenHow { flags: ProcfsOpenFlags::PATHRS_PROCFS_NEW_UNMASKED, }, extra: 0, }; let fd = unsafe { pathrs_procfs_open( &how_ok_struct as *const _ as *const _, mem::size_of::(), ) }; assert!(fd >= 0, "fd value {fd:#x} should be >= 0"); { // Close the file. let _ = unsafe { OwnedFd::from_raw_fd(fd) }; } let how_bad_struct = ProcfsOpenHowV2 { inner: ProcfsOpenHow { flags: ProcfsOpenFlags::PATHRS_PROCFS_NEW_UNMASKED, }, extra: 0xFF, }; let ret = unsafe { pathrs_procfs_open( &how_bad_struct as *const _ as *const _, mem::size_of::(), ) }; assert!( ret < capi_error::__PATHRS_MAX_ERR_VALUE, "ret value {ret:#x} should be error value" ); { let err = unsafe { capi_error::pathrs_errorinfo(ret) .expect("error must be retrievable") .unleak() }; // err.kind() == ErrorKind::UnsupportedStructureData assert_eq!( err.saved_errno, libc::E2BIG as _, "structure with extra trailing bytes should return E2BIG" ); } } } pathrs-0.2.1/src/capi/ret.rs000064400000000000000000000054361046102023000140000ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{capi::error as capi_error, error::Error, Handle, Root}; use std::{ fs::File, os::unix::io::{IntoRawFd, OwnedFd}, }; use libc::c_int; pub(super) type CReturn = c_int; pub(super) trait IntoCReturn { fn into_c_return(self) -> CReturn; } // TODO: Is it possible for us to return an actual OwnedFd through FFI when we // need to? Unfortunately, CReturn may end up returning -1 which is an // invalid OwnedFd/BorrowedFd value... // impl IntoCReturn for () { fn into_c_return(self) -> CReturn { 0 } } impl IntoCReturn for CReturn { fn into_c_return(self) -> CReturn { self } } impl IntoCReturn for OwnedFd { fn into_c_return(self) -> CReturn { self.into_raw_fd() } } impl IntoCReturn for Root { fn into_c_return(self) -> CReturn { OwnedFd::from(self).into_c_return() } } impl IntoCReturn for Handle { fn into_c_return(self) -> CReturn { OwnedFd::from(self).into_c_return() } } impl IntoCReturn for File { fn into_c_return(self) -> CReturn { OwnedFd::from(self).into_c_return() } } impl IntoCReturn for Result where V: IntoCReturn, { fn into_c_return(self) -> CReturn { // self.map_or_else(store_error, IntoCReturn::into_c_return) match self { Ok(ok) => ok.into_c_return(), Err(err) => capi_error::store_error(err), } } } pathrs-0.2.1/src/capi/utils.rs000064400000000000000000000376221046102023000143500ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::error::{Error, ErrorImpl}; use std::{ any, cmp, ffi::{CStr, CString, OsStr}, marker::PhantomData, mem, os::unix::{ ffi::OsStrExt, io::{AsRawFd, BorrowedFd, RawFd}, }, path::Path, ptr, slice, }; use bytemuck::Pod; use libc::{c_char, c_int, size_t}; /// Generate `.symver` entries for a given function. /// /// On platforms without stabilised /// /// ```ignore /// # use pathrs::capi::utils::symver; /// // Create a symbol version with the same name. /// #[no_mangle] /// fn foo_bar(a: u64) -> u64 { a + 32 } /// symver!{ /// fn foo_bar <- (foo_bar, version = "LIBFOO_2.0", default); /// } /// // Create a compatibility symbol with a different name. In this case, you /// // should actually name the implementation function something like __foo_v1 /// // because the symbol will still be public (still unclear why...). /// fn __foo_bar_v1(a: u64) -> u64 { a + 16 } /// symver!{ /// #[cfg(feature = "v1_compat")] // meta attributes work /// fn __foo_bar_v1 <- (foo_bar, version = "LIBFOO_1.0"); /// } /// ``` macro_rules! symver { () => {}; ($(#[$meta:meta])* fn $implsym:ident <- ($symname:ident, version = $version:literal); $($tail:tt)*) => { // Some architectures still have unstable ASM, which stops us from // injecting the ".symver" section. You can see the list in // LoweringContext::lower_inline_asm (compiler/rustc_asm_lowering). #[cfg(any( target_arch = "arm", target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64", target_arch = "riscv32", target_arch = "riscv64", //target_arch = "loongarch32", // MSRV(1.91?) // TODO: Once stabilised, add these arches: //target_arch = "powerpc", //target_arch = "powerpc64", //target_arch = "sparc64", ))] #[::rustversion::attr(since(1.72), cfg(target_arch = "loongarch64"))] #[::rustversion::attr(since(1.84), cfg(target_arch = "arm64ec"))] #[::rustversion::attr(since(1.84), cfg(target_arch = "s390x"))] // .symver $implsym, $symname@$version $(#[$meta])* ::std::arch::global_asm! {concat!( ".symver ", stringify!($implsym), ", ", stringify!($symname), "@", $version, )} $crate::capi::utils::symver! { $($tail)* } }; ($(#[$meta:meta])* fn $implsym:ident <- ($symname:ident, version = $version:literal, default); $($tail:tt)*) => { // Some architectures still have unstable ASM, which stops us from // injecting the ".symver" section. You can see the list in // LoweringContext::lower_inline_asm (compiler/rustc_asm_lowering). #[cfg(any( target_arch = "arm", target_arch = "aarch64", target_arch = "x86", target_arch = "x86_64", target_arch = "riscv32", target_arch = "riscv64", // TODO: Once stabilised, add these arches: //target_arch = "loongarch32", // MSRV(1.91?) //target_arch = "powerpc", //target_arch = "powerpc64", //target_arch = "sparc64", ))] #[::rustversion::attr(since(1.72), cfg(target_arch = "loongarch64"))] #[::rustversion::attr(since(1.84), cfg(target_arch = "arm64ec"))] #[::rustversion::attr(since(1.84), cfg(target_arch = "s390x"))] // .symver $implsym, $symname@@$version $(#[$meta])* ::std::arch::global_asm! {concat!( ".symver ", stringify!($implsym), ", ", stringify!($symname), "@@", $version, )} $crate::capi::utils::symver! { $($tail)* } }; } pub(crate) use symver; /// Equivalent to [`BorrowedFd`], except that there are no restrictions on what /// value the inner [`RawFd`] can take. This is necessary because C callers /// could reasonably pass `-1` as a file descriptor value and we need to verify /// that the value is valid to avoid UB. /// /// This type is FFI-safe and is intended for use in `extern "C" fn` signatures. /// While [`BorrowedFd`] (and `Option`) are technically FFI-safe, /// apparently using them in `extern "C" fn` signatures directly is not /// recommended for the above reason. #[derive(PartialEq, Eq, Debug, Copy, Clone)] #[repr(transparent)] pub struct CBorrowedFd<'fd> { inner: RawFd, _phantom: PhantomData>, } impl<'fd> CBorrowedFd<'fd> { /// Construct a [`CBorrowedFd`] in a `const`-friendly context. /// /// # Safety /// The caller guarantees that the file descriptor will live long enough for /// the returned lifetime `'fd`. pub(crate) const unsafe fn from_raw_fd(fd: RawFd) -> Self { Self { inner: fd, _phantom: PhantomData, } } /// Take a [`CBorrowedFd`] from C FFI and convert it to a proper /// [`BorrowedFd`] after making sure that it has a valid value (ie. is not /// negative). pub(crate) fn try_as_borrowed_fd(&self) -> Result, Error> { // TODO: We might want to support AT_FDCWD in the future. The // openat2 resolver handles it correctly, but the O_PATH // resolver and try_clone() probably need some work. // MSRV(1.66): Use match ..0? if self.inner.is_negative() { Err(ErrorImpl::InvalidArgument { // TODO: Should this error be EBADF? name: "fd".into(), description: "passed file descriptors must not be negative".into(), } .into()) } else { // SAFETY: The C caller guarantees that the file descriptor is valid for // the lifetime of CBorrowedFd (which is the same lifetime as // BorrowedFd). We verify that the file descriptor is not // negative, so it is definitely valid. Ok(unsafe { BorrowedFd::borrow_raw(self.inner) }) } } } impl<'fd> AsRawFd for CBorrowedFd<'fd> { fn as_raw_fd(&self) -> RawFd { self.inner } } impl<'fd> From> for CBorrowedFd<'fd> { fn from(fd: BorrowedFd<'_>) -> CBorrowedFd<'_> { CBorrowedFd { inner: fd.as_raw_fd(), _phantom: PhantomData, } } } // TODO: An AsFd impl would be even nicer but I suspect the lifetimes can't be // expressed. pub(crate) unsafe fn parse_path<'a>(path: *const c_char) -> Result<&'a Path, Error> { if path.is_null() { Err(ErrorImpl::InvalidArgument { name: "path".into(), description: "cannot be NULL".into(), })? } // SAFETY: C caller guarantees that the path is a valid C-style string. let bytes = unsafe { CStr::from_ptr(path) }.to_bytes(); Ok(OsStr::from_bytes(bytes).as_ref()) } pub(crate) unsafe fn copy_path_into_buffer( path: impl AsRef, buf: *mut c_char, bufsize: size_t, ) -> Result { let path = CString::new(path.as_ref().as_os_str().as_bytes()) .expect("link from readlink should not contain any nulls"); // MSRV(1.79): Switch to .count_bytes(). let path_len = path.to_bytes().len(); // If the linkbuf is null, we just return the number of bytes we // would've written. if !buf.is_null() && bufsize > 0 { // SAFETY: The C caller guarantees that buf is safe to write to // up to bufsize bytes. unsafe { let to_copy = cmp::min(path_len, bufsize); ptr::copy_nonoverlapping(path.as_ptr(), buf, to_copy); } } Ok(path_len as c_int) } pub(crate) unsafe fn copy_from_extensible_struct( ptr: *const T, size: usize, ) -> Result { // SAFETY: The C caller guarantees that ptr is from a single allocation, is // aligned for a u8 slice (generally true for all arrays), and is at least // size bytes in length. let raw_data = unsafe { slice::from_raw_parts(ptr as *const u8, size) }; let struct_size = mem::size_of::(); // We may need to make a copy if the structure is smaller than sizeof(T) to // zero-pad it, and the Vec needs to live for the rest of this function. #[allow(unused_assignments)] // only needed for storage let mut struct_data_buf: Option> = None; // MSRV(1.80): Use slice::split_at_checked()? let (struct_data, trailing) = if raw_data.len() >= struct_size { raw_data.split_at(struct_size) } else { let mut buf = vec![0u8; struct_size]; buf[0..raw_data.len()].copy_from_slice(raw_data); struct_data_buf = Some(buf); ( &struct_data_buf .as_ref() .expect("Option just assigned with Some must contain Some")[..], &[][..], ) }; debug_assert!( struct_data.len() == struct_size, "copy_from_extensible_struct should compute the struct size correctly" ); // TODO: Can we get an optimised memchr_inv implementation? Unfortunately, // see -- memchr doesn't // have this and is unlikely to have it in the future. if trailing.iter().any(|&ch| ch != 0) { return Err(ErrorImpl::UnsupportedStructureData { name: format!("c struct {}", any::type_name::()).into(), } .into()); } // NOTE: Even though we have a slice, we can only be sure it's aligned to u8 // (i.e., any alignment). It's better to just return a copy than error out // in the non-aligned case... bytemuck::try_pod_read_unaligned(struct_data).map_err(|err| { ErrorImpl::BytemuckPodCastError { description: format!("cannot cast passed buffer into {}", any::type_name::()).into(), source: err, } .into() }) } pub(crate) trait Leakable: Sized { /// Leak a structure such that it can be passed through C-FFI. fn leak(self) -> &'static mut Self { Box::leak(Box::new(self)) } /// Given a structure leaked through Leakable::leak, un-leak it. /// /// SAFETY: Callers must be sure to only ever call this once on a given /// pointer (otherwise memory corruption will occur). unsafe fn unleak(&'static mut self) -> Self { // SAFETY: Box::from_raw is safe because the caller guarantees that // the pointer we get is the same one we gave them, and it will only // ever be called once with the same pointer. *unsafe { Box::from_raw(self as *mut Self) } } /// Shorthand for `std::mem::drop(self.unleak())`. /// /// SAFETY: Same unsafety issue as `self.unleak()`. unsafe fn free(&'static mut self) { // SAFETY: Caller guarantees this is safe to do. let _ = unsafe { self.unleak() }; // drop Self } } #[cfg(test)] mod tests { use super::*; use crate::error::ErrorKind; use bytemuck::{Pod, Zeroable}; use pretty_assertions::assert_eq; #[repr(C)] #[derive(PartialEq, Eq, Default, Debug, Clone, Copy, Pod, Zeroable)] struct Struct { foo: u64, bar: u32, baz: u32, } #[test] fn extensible_struct() { let example = Struct { foo: 0xdeadbeeff00dcafe, bar: 0x01234567, baz: 0x89abcdef, }; assert_eq!( unsafe { copy_from_extensible_struct(&example as *const Struct, mem::size_of::()) } .expect("copy_from_extensible_struct with size=sizeof(struct)"), example, "copy_from_extensible_struct(struct, sizeof(struct))", ); } #[test] fn extensible_struct_short() { let example = Struct { foo: 0xdeadbeeff00dcafe, bar: 0x01234567, baz: 0x89abcdef, }; assert_eq!( unsafe { copy_from_extensible_struct(&example as *const Struct, 0) } .expect("copy_from_extensible_struct with size=0"), Struct::default(), "copy_from_extensible_struct(struct, 0)", ); assert_eq!( unsafe { copy_from_extensible_struct( &example as *const Struct, bytemuck::offset_of!(Struct, bar), ) } .expect("copy_from_extensible_struct with size=offsetof(struct.bar)"), Struct { foo: example.foo, ..Default::default() }, "copy_from_extensible_struct(struct, offsetof(struct, bar))", ); assert_eq!( unsafe { copy_from_extensible_struct( &example as *const Struct, bytemuck::offset_of!(Struct, baz), ) } .expect("copy_from_extensible_struct with size=offsetof(struct.baz)"), Struct { foo: example.foo, bar: example.bar, ..Default::default() }, "copy_from_extensible_struct(struct, offsetof(struct, bar))", ); } #[test] fn extensible_struct_long() { #[repr(C)] #[derive(PartialEq, Eq, Default, Debug, Clone, Copy, Pod, Zeroable)] struct StructV2 { inner: Struct, extra: u64, } let example_compatible = StructV2 { inner: Struct { foo: 0xdeadbeeff00dcafe, bar: 0x01234567, baz: 0x89abcdef, }, extra: 0, }; assert_eq!( unsafe { copy_from_extensible_struct( &example_compatible as *const StructV2 as *const Struct, mem::size_of::(), ) } .expect("copy_from_extensible_struct with size=sizeof(structv2)"), example_compatible.inner, "copy_from_extensible_struct(structv2, sizeof(structv2)) with only trailing zero bytes", ); let example_compatible = StructV2 { inner: Struct { foo: 0xdeadbeeff00dcafe, bar: 0x01234567, baz: 0x89abcdef, }, extra: 0x1, }; assert_eq!( unsafe { copy_from_extensible_struct( &example_compatible as *const StructV2 as *const Struct, mem::size_of::(), ) } .map_err(|err| err.kind()), Err(ErrorKind::UnsupportedStructureData), "copy_from_extensible_struct(structv2, sizeof(structv2)) with trailing non-zero bytes", ); } } pathrs-0.2.1/src/capi.rs000064400000000000000000000033051046102023000131770ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ // We need to permit unsafe code because we are exposing C APIs over FFI and // thus need to interact with C callers. #![allow(unsafe_code)] /// Core pathrs function wrappers. pub mod core; /// procfs-related function wrappers. pub mod procfs; /// C-friendly [`Error`](crate::error::Error) representation and helpers. pub mod error; /// Helpers for converting [`Result`] into C-style int returns. pub mod ret; mod utils; pathrs-0.2.1/src/error.rs000064400000000000000000000247761046102023000134330ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #![forbid(unsafe_code)] //! Error types for libpathrs. // NOTE: This module is mostly a workaround until several issues have been // resolved: // // * `std::error::Error::chain` is stabilised. // * I figure out a nice way to implement GlobalBacktrace... use crate::{resolvers::opath::SymlinkStackError, syscalls::Error as SyscallError}; use std::{borrow::Cow, io::Error as IOError}; // TODO: Add a backtrace to Error. We would just need to add an automatic // Backtrace::capture() in From. But it's not clear whether we want to // export the crate types here without std::backtrace::Backtrace. // MSRV(1.65): Use std::backtrace::Backtrace. /// Opaque error type for libpathrs. /// /// If you wish to do non-trivial error handling with libpathrs errors, use /// [`Error::kind`] to get an [`ErrorKind`] you can handle programmatically. #[derive(thiserror::Error, Debug)] #[error(transparent)] pub struct Error(#[from] Box); impl> From for Error { // TODO: Is there a way to make this not be exported at all? #[doc(hidden)] fn from(err: E) -> Self { Self(Box::new(err.into())) } } impl Error { /// Get the [`ErrorKind`] of this error. pub fn kind(&self) -> ErrorKind { self.0.kind() } /// Shorthand for [`.kind().can_retry()`](ErrorKind::can_retry). pub fn can_retry(&self) -> bool { self.0.kind().can_retry() } pub(crate) fn is_safety_violation(&self) -> bool { self.0.is_safety_violation() } #[cfg(test)] pub(crate) fn into_inner(self) -> ErrorImpl { *self.0 } } #[derive(thiserror::Error, Debug)] pub(crate) enum ErrorImpl { #[allow(dead_code)] #[error("feature {feature} is not implemented")] NotImplemented { feature: Cow<'static, str> }, #[error("feature {feature} not supported on this kernel")] NotSupported { feature: Cow<'static, str> }, #[error("invalid {name} argument: {description}")] InvalidArgument { name: Cow<'static, str>, description: Cow<'static, str>, }, #[cfg(feature = "capi")] #[error("invalid {name} structure: extra non-zero trailing bytes found")] UnsupportedStructureData { name: Cow<'static, str> }, #[error("violation of safety requirement: {description}")] SafetyViolation { description: Cow<'static, str> }, #[error("broken symlink stack during iteration: {description}")] BadSymlinkStackError { description: Cow<'static, str>, source: SymlinkStackError, }, #[error("{operation} failed")] OsError { operation: Cow<'static, str>, source: IOError, }, #[error("{operation} failed")] RawOsError { operation: Cow<'static, str>, source: SyscallError, }, #[cfg(feature = "capi")] #[error("error while parsing c struct: {description}")] BytemuckPodCastError { description: Cow<'static, str>, source: bytemuck::PodCastError, }, #[error("integer parsing failed")] ParseIntError(#[from] std::num::ParseIntError), // This should never actually get constructed in practice, but is needed so // that you can have From work for the no-op FromStr, // which in turn is needed for our nice generic str::parse-wrapping APIs. #[error("impossible error: infallible error failed")] InfallibleError(#[from] std::convert::Infallible), #[error("{context}")] Wrapped { context: Cow<'static, str>, source: Box, }, } /// Underlying error class for libpathrs errors. /// /// This is similar in concept to [`std::io::ErrorKind`]. Note that the #[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub enum ErrorKind { /// The requested feature is not implemented in libpathrs. NotImplemented, /// The requested feature is not supported by the system. NotSupported, /// The provided arguments to libpathrs were invalid. InvalidArgument, /// The provided extensible structure argument to the libpathrs C API /// contained trailing non-zero data which was not supported. #[cfg(feature = "capi")] UnsupportedStructureData, /// libpaths encountered a state where the safety of the operation could not /// be guaranteeed. This is usually the result of an attack by a malicious /// program. SafetyViolation, /// Some internal error occurred. For more information, see the string /// description of the original [`Error`]. InternalError, /// The underlying error came from a system call. The provided /// [`std::io::RawOsError`] is the numerical value of the `errno` number, if /// available. // TODO: We might want to use Option? OsError(Option), } impl ErrorImpl { pub(crate) fn kind(&self) -> ErrorKind { match self { Self::NotImplemented { .. } => ErrorKind::NotImplemented, Self::NotSupported { .. } => ErrorKind::NotSupported, Self::InvalidArgument { .. } => ErrorKind::InvalidArgument, #[cfg(feature = "capi")] Self::UnsupportedStructureData { .. } => ErrorKind::UnsupportedStructureData, Self::SafetyViolation { .. } => ErrorKind::SafetyViolation, // Any syscall-related errors get mapped to an OsError, since the // distinction doesn't matter to users checking error values. Self::OsError { source, .. } => ErrorKind::OsError(source.raw_os_error()), Self::RawOsError { source, .. } => { ErrorKind::OsError(source.root_cause().raw_os_error()) } // These errors are internal error types that we don't want to // expose outside of the crate. All that matters to users is that // there was some internal error. Self::BadSymlinkStackError { .. } | Self::ParseIntError(_) | Self::InfallibleError(_) => ErrorKind::InternalError, #[cfg(feature = "capi")] Self::BytemuckPodCastError { .. } => ErrorKind::InternalError, Self::Wrapped { source, .. } => source.kind(), } } pub(crate) fn is_safety_violation(&self) -> bool { self.kind().is_safety_violation() } } impl ErrorKind { /// Return a C-like errno for the [`ErrorKind`]. /// /// Aside from fetching the errno represented by standard /// [`ErrorKind::OsError`] errors, pure-Rust errors are also mapped to C /// errno values where appropriate. pub(crate) fn errno(&self) -> Option { match self { ErrorKind::NotImplemented => Some(libc::ENOSYS), ErrorKind::InvalidArgument => Some(libc::EINVAL), #[cfg(feature = "capi")] ErrorKind::UnsupportedStructureData => Some(libc::E2BIG), ErrorKind::SafetyViolation => Some(libc::EXDEV), ErrorKind::OsError(errno) => *errno, _ => None, } } /// Indicates whether an [`ErrorKind`] was associated with a transient error /// and that the operation might succeed if retried. /// /// Callers can make use of this if they wish to have custom retry logic. pub fn can_retry(&self) -> bool { matches!(self.errno(), Some(libc::EAGAIN) | Some(libc::EINTR)) } pub(crate) fn is_safety_violation(&self) -> bool { self.errno() == Self::SafetyViolation.errno() } } // Private trait necessary to work around the "orphan trait" restriction. pub(crate) trait ErrorExt: Sized { /// Wrap a `Result<..., Error>` with an additional context string. fn wrap>(self, context: S) -> Self { self.with_wrap(|| context.into()) } /// Wrap a `Result<..., Error>` with an additional context string created by /// a closure. fn with_wrap(self, context_fn: F) -> Self where F: FnOnce() -> String; } impl ErrorExt for ErrorImpl { fn with_wrap(self, context_fn: F) -> Self where F: FnOnce() -> String, { Self::Wrapped { context: context_fn().into(), source: self.into(), } } } impl ErrorExt for Error { fn with_wrap(self, context_fn: F) -> Self where F: FnOnce() -> String, { self.0.with_wrap(context_fn).into() } } impl ErrorExt for Result { fn with_wrap(self, context_fn: F) -> Self where F: FnOnce() -> String, { self.map_err(|err| err.with_wrap(context_fn)) } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; #[test] fn error_kind_errno() { assert_eq!( ErrorKind::InvalidArgument.errno(), Some(libc::EINVAL), "ErrorKind::InvalidArgument is equivalent to EINVAL" ); assert_eq!( ErrorKind::NotImplemented.errno(), Some(libc::ENOSYS), "ErrorKind::NotImplemented is equivalent to ENOSYS" ); assert_eq!( ErrorKind::SafetyViolation.errno(), Some(libc::EXDEV), "ErrorKind::SafetyViolation is equivalent to EXDEV" ); assert_eq!( ErrorKind::OsError(Some(libc::ENOANO)).errno(), Some(libc::ENOANO), "ErrorKind::OsError(...)::errno() returns the inner errno" ); } } pathrs-0.2.1/src/flags.rs000064400000000000000000000272211046102023000133620ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #![forbid(unsafe_code)] //! Bit-flags for modifying the behaviour of libpathrs. use crate::syscalls; use bitflags::bitflags; bitflags! { /// Wrapper for the underlying `libc`'s `O_*` flags. /// /// The flag values and their meaning is identical to the description in the /// `open(2)` man page. /// /// # Caveats /// /// For historical reasons, the first three bits of `open(2)`'s flags are /// for the access mode and are actually treated as a 2-bit number. So, it /// is incorrect to attempt to do any checks on the access mode without /// masking it correctly. So some helpers were added to make usage more /// ergonomic. /// /// ``` /// # use pathrs::flags::OpenFlags; /// // Using .contains() can lead to confusing behaviour: /// # let ret = /// OpenFlags::O_WRONLY.contains(OpenFlags::O_RDONLY); // returns true! /// # assert!(ret); /// # let ret = /// OpenFlags::O_RDWR.contains(OpenFlags::O_RDONLY); // returns true! /// # assert!(ret); /// # let ret = /// OpenFlags::O_RDWR.contains(OpenFlags::O_WRONLY); // returns false! /// # assert!(!ret); /// // But using the .wants_write() and .wants_read() helpers works: /// assert_eq!(OpenFlags::O_WRONLY.wants_read(), false); /// # #[allow(clippy::bool_assert_comparison)] /// assert_eq!(OpenFlags::O_RDONLY.wants_read(), true); /// # #[allow(clippy::bool_assert_comparison)] /// assert_eq!(OpenFlags::O_RDWR.wants_write(), true); /// // And we also correctly handle O_PATH as being "neither read nor write". /// assert_eq!((OpenFlags::O_PATH | OpenFlags::O_RDWR).access_mode(), None); /// assert_eq!((OpenFlags::O_PATH | OpenFlags::O_RDWR).wants_read(), false); /// assert_eq!((OpenFlags::O_PATH | OpenFlags::O_RDWR).wants_write(), false); /// // As well as the sneaky "implied write" cases. /// assert_eq!((OpenFlags::O_CREAT|OpenFlags::O_RDONLY).wants_write(), true); /// assert_eq!((OpenFlags::O_TRUNC|OpenFlags::O_RDONLY).wants_write(), true); /// ``` /// /// Also, if you wish to check for `O_TMPFILE`, make sure to use `contains`. /// `O_TMPFILE` includes `O_DIRECTORY`, so doing `intersection` will match /// `O_DIRECTORY` as well. /// /// ``` /// # use pathrs::flags::OpenFlags; /// // O_TMPFILE contains O_DIRECTORY (as a kernel implementation detail). /// # let ret = /// OpenFlags::O_DIRECTORY.intersection(OpenFlags::O_TMPFILE).is_empty(); // returns false! /// # assert!(!ret); /// // Instead, use contains: /// assert_eq!(OpenFlags::O_DIRECTORY.contains(OpenFlags::O_TMPFILE), false); /// ``` #[derive(Default, PartialEq, Eq, Debug, Clone, Copy)] pub struct OpenFlags: libc::c_int { // Access modes (including O_PATH). const O_RDWR = libc::O_RDWR; const O_RDONLY = libc::O_RDONLY; const O_WRONLY = libc::O_WRONLY; const O_PATH = libc::O_PATH; // Fd flags. const O_CLOEXEC = libc::O_CLOEXEC; // Control lookups. const O_NOFOLLOW = libc::O_NOFOLLOW; const O_DIRECTORY = libc::O_DIRECTORY; const O_NOCTTY = libc::O_NOCTTY; // NOTE: This flag contains O_DIRECTORY! const O_TMPFILE = libc::O_TMPFILE; // File creation. const O_CREAT = libc::O_CREAT; const O_EXCL = libc::O_EXCL; const O_TRUNC = libc::O_TRUNC; const O_APPEND = libc::O_APPEND; // Sync. const O_SYNC = libc::O_SYNC; const O_ASYNC = libc::O_ASYNC; const O_DSYNC = libc::O_DSYNC; #[cfg(not(target_env = "musl"))] // musl doesn't provide FSYNC const O_FSYNC = libc::O_FSYNC; const O_RSYNC = libc::O_RSYNC; const O_DIRECT = libc::O_DIRECT; const O_NDELAY = libc::O_NDELAY; const O_NOATIME = libc::O_NOATIME; const O_NONBLOCK = libc::O_NONBLOCK; // NOTE: This is effectively a kernel-internal flag (auto-set on systems // with large offset support). glibc defines it as 0, and it is // also architecture-specific. //const O_LARGEFILE = libc::O_LARGEFILE; // Don't clobber unknown O_* bits. const _ = !0; } } impl From for rustix::fs::OFlags { fn from(flags: OpenFlags) -> Self { Self::from_bits_retain(flags.bits() as u32) } } impl OpenFlags { /// Grab the access mode bits from the flags. /// /// If the flags contain `O_PATH`, this returns `None`. #[inline] pub fn access_mode(self) -> Option { if self.contains(OpenFlags::O_PATH) { None } else { Some(self.bits() & libc::O_ACCMODE) } } /// Does the access mode imply read access? /// /// Returns false for `O_PATH`. #[inline] pub fn wants_read(self) -> bool { match self.access_mode() { None => false, // O_PATH Some(acc) => acc == libc::O_RDONLY || acc == libc::O_RDWR, } } /// Does the access mode imply write access? Note that there are several /// other bits in OpenFlags that imply write access other than `O_WRONLY` /// and `O_RDWR`. This function checks those bits as well. /// /// Returns false for `O_PATH`. #[inline] pub fn wants_write(self) -> bool { match self.access_mode() { None => false, // O_PATH Some(acc) => { acc == libc::O_WRONLY || acc == libc::O_RDWR || !self // O_CREAT and O_TRUNC are silently ignored with O_PATH. .intersection(OpenFlags::O_TRUNC | OpenFlags::O_CREAT) .is_empty() } } } } bitflags! { /// Wrapper for the underlying `libc`'s `RENAME_*` flags. /// /// The flag values and their meaning is identical to the description in the /// [`renameat2(2)`] man page. /// /// [`renameat2(2)`] might not not be supported on your kernel -- in which /// case [`Root::rename`] will fail if you specify any RenameFlags. You can /// verify whether [`renameat2(2)`] flags are supported by calling /// [`RenameFlags::supported`]. /// /// [`renameat2(2)`]: http://man7.org/linux/man-pages/man2/rename.2.html /// [`Root::rename`]: crate::Root::rename #[derive(Default, PartialEq, Eq, Debug, Clone, Copy)] pub struct RenameFlags: libc::c_uint { const RENAME_EXCHANGE = libc::RENAME_EXCHANGE; const RENAME_NOREPLACE = libc::RENAME_NOREPLACE; const RENAME_WHITEOUT = libc::RENAME_WHITEOUT; // Don't clobber unknown RENAME_* bits. const _ = !0; } } impl From for rustix::fs::RenameFlags { fn from(flags: RenameFlags) -> Self { Self::from_bits_retain(flags.bits()) } } impl RenameFlags { /// Is this set of RenameFlags supported by the running kernel? pub fn is_supported(self) -> bool { // TODO: This check won't work once new RENAME_* flags are added. self.is_empty() || *syscalls::RENAME_FLAGS_SUPPORTED } } #[cfg(test)] mod tests { use crate::{ flags::{OpenFlags, RenameFlags}, syscalls, }; macro_rules! openflags_tests { ($($test_name:ident ( $($flag:ident)|+ ) == {accmode: $accmode:expr, read: $wants_read:expr, write: $wants_write:expr} );+ $(;)?) => { $( paste::paste! { #[test] fn []() { let flags = $(OpenFlags::$flag)|*; let accmode: Option = $accmode; assert_eq!(flags.access_mode(), accmode, "{flags:?} access mode should be {:?}", accmode.map(OpenFlags::from_bits_retain)); } #[test] fn []() { let flags = $(OpenFlags::$flag)|*; assert_eq!(flags.wants_read(), $wants_read, "{flags:?} wants_read should be {:?}", $wants_read); } #[test] fn []() { let flags = $(OpenFlags::$flag)|*; assert_eq!(flags.wants_write(), $wants_write, "{flags:?} wants_write should be {:?}", $wants_write); } } )* } } openflags_tests! { plain_rdonly(O_RDONLY) == {accmode: Some(libc::O_RDONLY), read: true, write: false}; plain_wronly(O_WRONLY) == {accmode: Some(libc::O_WRONLY), read: false, write: true}; plain_rdwr(O_RDWR) == {accmode: Some(libc::O_RDWR), read: true, write: true}; plain_opath(O_PATH) == {accmode: None, read: false, write: false}; rdwr_opath(O_RDWR|O_PATH) == {accmode: None, read: false, write: false}; wronly_opath(O_WRONLY|O_PATH) == {accmode: None, read: false, write: false}; trunc_rdonly(O_RDONLY|O_TRUNC) == {accmode: Some(libc::O_RDONLY), read: true, write: true}; trunc_wronly(O_WRONLY|O_TRUNC) == {accmode: Some(libc::O_WRONLY), read: false, write: true}; trunc_rdwr(O_RDWR|O_TRUNC) == {accmode: Some(libc::O_RDWR), read: true, write: true}; trunc_path(O_PATH|O_TRUNC) == {accmode: None, read: false, write: false}; creat_rdonly(O_RDONLY|O_CREAT) == {accmode: Some(libc::O_RDONLY), read: true, write: true}; creat_wronly(O_WRONLY|O_CREAT) == {accmode: Some(libc::O_WRONLY), read: false, write: true}; creat_rdwr(O_RDWR|O_CREAT) == {accmode: Some(libc::O_RDWR), read: true, write: true}; creat_path(O_PATH|O_CREAT) == {accmode: None, read: false, write: false}; } #[test] fn rename_flags_is_supported() { assert!( RenameFlags::empty().is_supported(), "empty flags should be supported" ); assert_eq!( RenameFlags::RENAME_EXCHANGE.is_supported(), *syscalls::RENAME_FLAGS_SUPPORTED, "rename flags being supported should be identical to RENAME_FLAGS_SUPPORTED" ); } } bitflags! { /// Optional flags to modify the resolution of paths inside a [`Root`]. /// /// [`Root`]: crate::Root #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] pub struct ResolverFlags: u64 { // TODO: We should probably have our own bits... const NO_SYMLINKS = libc::RESOLVE_NO_SYMLINKS; } } pathrs-0.2.1/src/handle.rs000064400000000000000000000237261046102023000135270ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #![forbid(unsafe_code)] use crate::{ error::{Error, ErrorImpl}, flags::OpenFlags, procfs::ProcfsHandle, utils::FdExt, }; use std::{ fs::File, os::unix::io::{AsFd, BorrowedFd, OwnedFd}, }; /// A handle to an existing inode within a [`Root`]. /// /// This handle references an already-resolved path which can be used for the /// purpose of "re-opening" the handle and get an actual [`File`] which can be /// used for ordinary operations. /// /// # Safety /// /// It is critical for the safety of this library that **at no point** do you /// use interfaces like [`libc::openat`] directly on the [`OwnedFd`] you can /// extract from this [`Handle`]. **You must always do operations through a /// valid [`Root`].** /// /// [`RawFd`]: std::os::unix::io::RawFd /// [`Root`]: crate::Root #[derive(Debug)] pub struct Handle { inner: OwnedFd, } impl Handle { /// Wrap an [`OwnedFd`] into a [`Handle`]. #[inline] pub fn from_fd(fd: impl Into) -> Self { Self { inner: fd.into() } } /// Borrow this [`Handle`] as a [`HandleRef`]. // XXX: We can't use Borrow/Deref for this because HandleRef takes a // lifetime rather than being a pure reference. Ideally we would use // Deref but it seems that won't be possible in standard Rust for a // long time, if ever... #[inline] pub fn as_ref(&self) -> HandleRef<'_> { HandleRef { inner: self.as_fd(), } } /// Create a copy of an existing [`Handle`]. /// /// The new handle is completely independent from the original, but /// references the same underlying file. #[inline] pub fn try_clone(&self) -> Result { self.as_ref().try_clone() } /// "Upgrade" the handle to a usable [`File`] handle. /// /// This new [`File`] handle is suitable for reading and writing. This does /// not consume the original handle (allowing for it to be used many times). /// /// The [`File`] handle will be opened with `O_NOCTTY` and `O_CLOEXEC` set, /// regardless of whether those flags are present in the `flags` argument. /// You can correct these yourself if these defaults are not ideal for you: /// /// 1. `fcntl(fd, F_SETFD, 0)` will let you unset `O_CLOEXEC`. /// 2. `ioctl(fd, TIOCSCTTY, 0)` will set the fd as the controlling terminal /// (if you don't have one already, and the fd references a TTY). /// /// [`Root::create`]: crate::Root::create #[doc(alias = "pathrs_reopen")] #[inline] pub fn reopen(&self, flags: impl Into) -> Result { self.as_ref().reopen(flags) } } impl From for Handle { /// Shorthand for [`Handle::from_fd`]. fn from(fd: OwnedFd) -> Self { Self::from_fd(fd) } } impl From for OwnedFd { /// Unwrap a [`Handle`] to reveal the underlying [`OwnedFd`]. /// /// **Note**: This method is primarily intended to allow for file descriptor /// passing or otherwise transmitting file descriptor information. If you /// want to get a [`File`] handle for general use, please use /// [`Handle::reopen`] instead. #[inline] fn from(handle: Handle) -> Self { handle.inner } } impl AsFd for Handle { /// Access the underlying file descriptor for a [`Handle`]. /// /// **Note**: This method is primarily intended to allow for tests and other /// code to check the status of the underlying [`OwnedFd`] without having to /// use [`OwnedFd::from`]. It is not safe to use this [`BorrowedFd`] /// directly to do filesystem operations. Please use the provided /// [`HandleRef`] methods. #[inline] fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } /// Borrowed version of [`Handle`]. /// /// Unlike [`Handle`], when [`HandleRef`] is dropped the underlying file /// descriptor is *not* closed. This is mainly useful for programs and libraries /// that have to do operations on [`&File`][File]s and [`BorrowedFd`]s passed /// from elsewhere. /// /// [File]: std::fs::File // TODO: Is there any way we can restructure this to use Deref so that we don't // need to copy all of the methods into Handle? Probably not... Maybe GATs // will eventually support this but we'd still need a GAT-friendly Deref. #[derive(Copy, Clone, Debug)] pub struct HandleRef<'fd> { inner: BorrowedFd<'fd>, } impl HandleRef<'_> { /// Wrap a [`BorrowedFd`] into a [`HandleRef`]. pub fn from_fd(inner: BorrowedFd<'_>) -> HandleRef<'_> { HandleRef { inner } } /// Create a copy of a [`HandleRef`]. /// /// Note that (unlike [`BorrowedFd::clone`]) this method creates a full copy /// of the underlying file descriptor and thus is more equivalent to /// [`BorrowedFd::try_clone_to_owned`]. /// /// To create a shallow copy of a [`HandleRef`], you can use /// [`Clone::clone`] (or just [`Copy`]). // TODO: We might need to call this something other than try_clone(), since // it's a little too easy to confuse with Clone::clone() but we also // really want to have Copy. pub fn try_clone(&self) -> Result { self.as_fd() .try_clone_to_owned() .map_err(|err| { ErrorImpl::OsError { operation: "clone underlying handle file".into(), source: err, } .into() }) .map(Handle::from_fd) } /// "Upgrade" the handle to a usable [`File`] handle. /// /// This new [`File`] handle is suitable for reading and writing. This does /// not consume the original handle (allowing for it to be used many times). /// /// The [`File`] handle will be opened with `O_NOCTTY` and `O_CLOEXEC` set, /// regardless of whether those flags are present in the `flags` argument. /// You can correct these yourself if these defaults are not ideal for you: /// /// 1. `fcntl(fd, F_SETFD, 0)` will let you unset `O_CLOEXEC`. /// 2. `ioctl(fd, TIOCSCTTY, 0)` will set the fd as the controlling terminal /// (if you don't have one already, and the fd references a TTY). /// /// [`Root::create`]: crate::Root::create #[doc(alias = "pathrs_reopen")] pub fn reopen(&self, flags: impl Into) -> Result { self.inner .reopen(&ProcfsHandle::new()?, flags.into()) .map(File::from) } // TODO: All the different stat* interfaces? // TODO: bind(). This might be safe to do (set the socket path to // /proc/self/fd/...) but I'm a bit sad it'd be separate from // Handle::reopen(). } impl<'fd> From> for HandleRef<'fd> { /// Shorthand for [`HandleRef::from_fd`]. fn from(fd: BorrowedFd<'fd>) -> Self { Self::from_fd(fd) } } impl AsFd for HandleRef<'_> { /// Access the underlying file descriptor for a [`HandleRef`]. /// /// **Note**: This method is primarily intended to allow for tests and other /// code to check the status of the underlying file descriptor. It is not /// safe to use this [`BorrowedFd`] directly to do filesystem operations. /// Please use the provided [`HandleRef`] methods. #[inline] fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } #[cfg(test)] mod tests { use crate::{Handle, HandleRef, Root}; use std::os::unix::io::{AsFd, AsRawFd, OwnedFd}; use anyhow::Error; use pretty_assertions::assert_eq; #[test] fn from_fd() -> Result<(), Error> { let handle = Root::open(".")?.resolve(".")?; let handle_ref1 = handle.as_ref(); let handle_ref2 = HandleRef::from_fd(handle.as_fd()); assert_eq!( handle.as_fd().as_raw_fd(), handle_ref1.as_fd().as_raw_fd(), "Handle::as_ref should have the same underlying fd" ); assert_eq!( handle.as_fd().as_raw_fd(), handle_ref2.as_fd().as_raw_fd(), "HandleRef::from_fd should have the same underlying fd" ); Ok(()) } #[test] fn into_from_ownedfd() -> Result<(), Error> { let handle = Root::open(".")?.resolve(".")?; let handle_fd = handle.as_fd().as_raw_fd(); let owned: OwnedFd = handle.into(); let owned_fd = owned.as_fd().as_raw_fd(); let handle2: Handle = owned.into(); let handle2_fd = handle2.as_fd().as_raw_fd(); assert_eq!( handle_fd, owned_fd, "OwnedFd::from(handle) should have same underlying fd", ); assert_eq!( handle_fd, handle2_fd, "Handle -> OwnedFd -> Handle roundtrip should have same underlying fd", ); Ok(()) } } pathrs-0.2.1/src/lib.rs000064400000000000000000000233201046102023000130300ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ //! libpathrs provides a series of primitives for Linux programs to safely //! handle path operations inside an untrusted directory tree. //! //! The idea is that a [`Root`] handle is like a handle for resolution inside a //! [`chroot(2)`], with [`Handle`] being an `O_PATH` descriptor which you can //! "upgrade" to a proper [`File`]. However this library acts far more //! efficiently than spawning a new process and doing a full [`chroot(2)`] for //! every operation. //! //! # Example //! //! The recommended usage of libpathrs looks something like this: //! //! ``` //! # use pathrs::{error::Error, flags::OpenFlags, Root}; //! # fn main() -> Result<(), Error> { //! let (root_path, unsafe_path) = ("/path/to/root", "/etc/passwd"); //! # let root_path = "/"; //! // Get a root handle for resolution. //! let root = Root::open(root_path)?; //! // Resolve the path. //! let handle = root.resolve(unsafe_path)?; //! // Upgrade the handle to a full std::fs::File. //! let file = handle.reopen(OpenFlags::O_RDONLY)?; //! //! // Or, in one line: //! let file = root.resolve(unsafe_path)? //! .reopen(OpenFlags::O_RDONLY)?; //! # Ok(()) //! # } //! ``` //! //! # C API //! //! In order to ensure the maximum possible number of people can make us of this //! library to increase the overall security of Linux tooling, it is written in //! Rust (to be memory-safe) and produces C dylibs for usage with any language //! that supports C-based FFI. To further help expand how many folks can use //! libpathrs, libpathrs's MSRV is Rust 1.63, to allow us to build on more //! stable operating systems (such as Debian Buster, which provides Rust 1.63). //! //! A C example corresponding to the above Rust code would look like: //! //! ```c //! #include //! //! int get_my_fd(void) //! { //! const char *root_path = "/path/to/root"; //! const char *unsafe_path = "/etc/passwd"; //! //! int liberr = 0; //! int root = -EBADF, //! handle = -EBADF, //! fd = -EBADF; //! //! root = pathrs_open_root(root_path); //! if (IS_PATHRS_ERR(root)) { //! liberr = root; //! goto err; //! } //! //! handle = pathrs_inroot_resolve(root, unsafe_path); //! if (IS_PATHRS_ERR(handle)) { //! liberr = handle; //! goto err; //! } //! //! fd = pathrs_reopen(handle, O_RDONLY); //! if (IS_PATHRS_ERR(fd)) { //! liberr = fd; //! goto err; //! } //! //! err: //! if (IS_PATHRS_ERR(liberr)) { //! pathrs_error_t *error = pathrs_errorinfo(liberr); //! fprintf(stderr, "Uh-oh: %s (errno=%d)\n", error->description, error->saved_errno); //! pathrs_errorinfo_free(error); //! } //! close(root); //! close(handle); //! return fd; //! } //! ``` //! //! # Kernel Support //! //! libpathrs is designed to only work with Linux, as it uses several Linux-only //! APIs. //! //! libpathrs was designed alongside [`openat2(2)`] (available since Linux 5.6) //! and dynamically tries to use the latest kernel features to provide the //! maximum possible protection against racing attackers. However, it also //! provides support for older kernel versions (in theory up to Linux //! 2.6.39 but we do not currently test this) by emulating newer kernel features //! in userspace. //! //! However, we strongly recommend you use at least Linux 5.6 to get a //! reasonable amount of protection against various attacks, and ideally at //! least Linux 6.8 to make use of all of the protections we have implemented. //! See the following table for what kernel features we optionally support and //! what they are used for. //! //! | Feature | Minimum Kernel Version | Description | Fallback | //! | --------------------- | ----------------------- | ----------- | -------- | //! | `/proc/thread-self` | Linux 3.17 (2014-10-05) | Used when operating on the current thread's `/proc` directory for use with `PATHRS_PROC_THREAD_SELF`. | `/proc/self/task/$tid` is used, but this might not be available in some edge cases so `/proc/self` is used as a final fallback. | //! | [`open_tree(2)`] | Linux 5.2 (2018-07-07) | Used to create a private procfs handle when operating on `/proc` (this is a copy of the host `/proc` -- in most cases this will also strip any overmounts). Requires `CAP_SYS_ADMIN` privileges. | Open a regular handle to `/proc`. This can lead to certain race attacks if the attacker can dynamically create mounts. | //! | [`fsopen(2)`] | Linux 5.2 (2019-07-07) | Used to create a private procfs handle when operating on `/proc` (with a completely fresh copy of `/proc` -- in some cases this operation will fail if there are locked overmounts on top of `/proc`). Requires `CAP_SYS_ADMIN` privileges. | Try to use [`open_tree(2)`] instead -- in the case of errors due to locked overmounts, [`open_tree(2)`] will be used to create a recursive copy that preserves the overmounts. This means that an attacker would not be able to actively change the mounts on top of `/proc` but there might be some overmounts that libpathrs will detect (and reject). | //! | [`openat2(2)`] | Linux 5.6 (2020-03-29) | In-kernel restrictions of path lookup. This is used extensively by `libpathrs` to safely do path lookups. | Userspace emulated path lookups. | //! | `subset=pid` | Linux 5.8 (2020-08-02) | Allows for a `procfs` handle created with [`fsopen(2)`][] to not contain any global procfs files that would be dangerous for an attacker to write to. Detached `procfs` mounts with `subset=pid` are deemed safe(r) to leak into containers and so libpathrs will internally cache `subset=pid` [`ProcfsHandle`]s. | libpathrs's [`ProcfsHandle`]s will have global files and thus libpathrs will not cache a copy of the file descriptor for each operation (possibly causing substantially higher syscall usage as a result -- our testing found that this can have a performance impact in some cases). | //! | `STATX_MNT_ID` | Linux 5.8 (2020-08-02) | Used to verify whether there are bind-mounts on top of `/proc` that could result in insecure operations (on systems with `fsopen(2)` or `open_tree(2)` this protection is somewhat redundant for privileged programs -- those kinds of `procfs` handles will typically not have overmounts.) | Parse the `/proc/thread-self/fdinfo/$fd` directly -- for systems with `openat2(2)`, this is guaranteed to be safe against attacks. For systems without `openat2(2)`, we have to fallback to unsafe opens that could be fooled by bind-mounts -- however, we believe that exploitation of this would be difficult in practice (even with an attacker that has the persistent ability to mount to arbitrary paths) due to the way we verify `procfs` accesses. | //! | `STATX_MNT_ID_UNIQUE` | Linux 6.8 (2024-03-10) | Used for the same reason as `STATX_MNT_ID`, but allows us to protect against mount ID recycling. This is effectively a safer version of `STATX_MNT_ID`. | `STATX_MNT_ID` is used (see the `STATX_MNT_ID` fallback if it's not available either). | //! //! For more information about the work behind `openat2(2)`, you can read the //! following LWN articles (note that the merged version of `openat2(2)` is //! different to the version described by LWN): //! //! * [New AT_ flags for restricting pathname lookup][lwn-atflags] //! * [Restricting path name lookup with openat2()][lwn-openat2] //! //! [`openat2(2)`]: https://www.man7.org/linux/man-pages/man2/openat2.2.html //! [lwn-atflags]: https://lwn.net/Articles/767547/ //! [lwn-openat2]: https://lwn.net/Articles/796868/ //! [`File`]: std::fs::File //! [`chroot(2)`]: http://man7.org/linux/man-pages/man2/chroot.2.html //! [`open_tree(2)`]: https://github.com/brauner/man-pages-md/blob/main/open_tree.md //! [`fsopen(2)`]: https://github.com/brauner/man-pages-md/blob/main/fsopen.md //! [`ProcfsHandle`]: crate::procfs::ProcfsHandle // libpathrs only supports Linux at the moment. #![cfg(target_os = "linux")] #![deny(rustdoc::broken_intra_doc_links)] #![deny(clippy::all)] #![deny(missing_debug_implementations)] // We use this the coverage_attribute when doing coverage runs. // #![cfg_attr(coverage, feature(coverage_attribute))] // `Handle` implementation. mod handle; #[doc(inline)] pub use handle::*; // `Root` implementation. mod root; #[doc(inline)] pub use root::*; pub mod error; pub mod flags; pub mod procfs; // Resolver backend implementations. mod resolvers; // C API. #[cfg(feature = "capi")] mod capi; // Internally used helpers. mod syscalls; mod utils; // Library tests. #[cfg(test)] mod tests; pathrs-0.2.1/src/procfs.rs000064400000000000000000001440361046102023000135660ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #![forbid(unsafe_code)] //! Helpers to operate on `procfs` safely. //! //! The primary interface most users will interact with is [`ProcfsHandle`], //! with most usage looking something like: //! //! ```rust //! # use pathrs::flags::OpenFlags; //! # use pathrs::procfs::{ProcfsBase, ProcfsHandle}; //! let proc = ProcfsHandle::new()?; // usually cached by libpathrs //! //! // Open regular procfs files (ProcfsBase indicates the base subpath of /proc //! // the operation is acting on, effectively acting like a prefix). //! let status = proc.open(ProcfsBase::ProcThreadSelf, "status", OpenFlags::O_RDONLY)?; //! # let _ = status; //! //! // Open a magic-link safely. This even protects against bind-mounts on top //! // of the magic-link! //! let exe = proc.open_follow(ProcfsBase::ProcSelf, "exe", OpenFlags::O_PATH)?; //! # let _ = exe; //! //! // Do a safe readlink. //! let stdin_path = proc.readlink(ProcfsBase::ProcThreadSelf, "fd/0")?; //! println!("stdin: {stdin_path:?}"); //! # Ok::<(), anyhow::Error>(()) //! ``` use crate::{ error::{Error, ErrorExt, ErrorImpl, ErrorKind}, flags::{OpenFlags, ResolverFlags}, resolvers::procfs::ProcfsResolver, syscalls, utils::{self, FdExt, MaybeOwnedFd, RawProcfsRoot}, }; use std::{ fs::File, io::Error as IOError, os::unix::{ fs::MetadataExt, io::{AsFd, BorrowedFd, OwnedFd}, }, path::{Path, PathBuf}, }; use once_cell::sync::OnceCell as OnceLock; use rustix::{ fs::{self as rustix_fs, Access, AtFlags}, mount::{FsMountFlags, FsOpenFlags, MountAttrFlags, OpenTreeFlags}, }; /// Indicate what base directory should be used when doing `/proc/...` /// operations with a [`ProcfsHandle`]. /// /// Most users should use [`ProcSelf`], but certain users (such as /// multi-threaded programs where you really want thread-specific information) /// may want to use [`ProcThreadSelf`]. /// /// [`ProcSelf`]: Self::ProcSelf /// [`ProcThreadSelf`]: Self::ProcThreadSelf #[doc(alias = "pathrs_proc_base_t")] #[derive(Eq, PartialEq, Debug, Clone, Copy)] #[non_exhaustive] pub enum ProcfsBase { /// Use `/proc`. As this requires us to disable any masking of our internal /// procfs mount, any file handles returned from [`ProcfsHandle::open`] /// using `ProcRoot` should be treated with extra care to ensure you do not /// leak them into containers. Ideally users should use [`ProcSelf`] if /// possible. /// /// [`ProcSelf`]: Self::ProcSelf ProcRoot, /// Use `/proc/`. This is useful shorthand when looking up information /// about other processes (the alternative being passing the PID as a string /// component with [`ProcRoot`][`Self::ProcRoot`] manually). /// /// Note that this operation is inherently racy -- the process referenced by /// this PID may have died and the PID recycled with a different process. In /// principle, this means that it is only really safe to use this with: /// /// * PID 1 (the init process), as that PID cannot ever get recycled. /// * Your current PID (though you should just use [`ProcSelf`]). /// * Your current TID (though you should just use [`ProcThreadSelf`]), or /// _possibly_ other TIDs in your thread-group if you are absolutely sure /// they have not been reaped (typically with [`JoinHandle::join`], /// though there are other ways). /// * PIDs of child processes (as long as you are sure that no other part /// of your program incorrectly catches or ignores `SIGCHLD`, and that /// you do it *before* you call [`wait(2)`] or any equivalent method that /// could reap zombies). /// /// Outside of those specific uses, users should probably avoid using this. // TODO: Add support for pidfds, to resolve the race issue. /// /// [`ProcRoot`]: Self::ProcRoot /// [`ProcSelf`]: Self::ProcSelf /// [`ProcThreadSelf`]: Self::ProcThreadSelf /// [`JoinHandle::join`]: https://doc.rust-lang.org/std/thread/struct.JoinHandle.html#method.join /// [`pthread_join(3)`]: https://man7.org/linux/man-pages/man3/pthread_join.3.html /// [`wait(2)`]: https://man7.org/linux/man-pages/man2/wait.2.html // NOTE: It seems incredibly unlikely that this will ever need to be // expanded beyond u32. glibc has always used u16 for pid_t, and the // kernel itself (even at time of writing) only supports a maximum of // 2^22 PIDs internally. Even the newest pid-related APIs // (PIDFD_GET_INFO for instance) only allocate a u32 for pids. By // making this a u32 we can easily pack it inside a u64 for the C API. ProcPid(u32), /// Use `/proc/self`. For most programs, this is the standard choice. ProcSelf, /// Use `/proc/thread-self`. In multi-threaded programs, it is possible for /// `/proc/self` to point a different thread than the currently-executing /// thread. For programs which make use of [`unshare(2)`] or are interacting /// with strictly thread-specific structures (such as `/proc/self/stack`) /// may prefer to use `ProcThreadSelf` to avoid strange behaviour. /// /// However, if you pass a handle returned or derived from /// [`ProcfsHandle::open`] between threads (this can happen implicitly when /// using green-thread systems such as Go), you must take care to ensure the /// original thread stays alive until you stop using the handle. If the /// thread dies, the handle may start returning invalid data or errors /// because it refers to a specific thread that no longer exists. For /// correctness reasons you probably want to also actually lock execution to /// the thread while using the handle. This drawback does not apply to /// [`ProcSelf`]. /// /// # Compatibility /// `/proc/thread-self` was added in Linux 3.17 (in 2014), so all modern /// systems -- with the notable exception of RHEL 7 -- have support for it. /// For older kernels, `ProcThreadSelf` will emulate `/proc/thread-self` /// support via other means (namely `/proc/self/task/$tid`), which should /// work in almost all cases. As a final fallback (for the very few programs /// that interact heavily with PID namespaces), we will silently fallback to /// [`ProcSelf`] (this may become an error in future versions). /// /// [`unshare(2)`]: https://www.man7.org/linux/man-pages/man2/unshare.2.html /// [`ProcSelf`]: Self::ProcSelf /// [runc]: https://github.com/opencontainers/runc ProcThreadSelf, } impl ProcfsBase { pub(crate) fn into_path(self, proc_rootfd: RawProcfsRoot<'_>) -> PathBuf { match self { Self::ProcRoot => PathBuf::from("."), Self::ProcSelf => PathBuf::from("self"), Self::ProcPid(pid) => PathBuf::from(pid.to_string()), Self::ProcThreadSelf => [ // /proc/thread-self was added in Linux 3.17. "thread-self".into(), // For pre-3.17 kernels we use the fully-expanded version. format!("self/task/{}", syscalls::gettid()).into(), // However, if the proc root is not using our pid namespace, the // tid in /proc/self/task/... will be wrong and we need to fall // back to /proc/self. This is technically incorrect but we have // no other choice -- and this is needed for runc (mainly // because of RHEL 7 which has a 3.10 kernel). // TODO: Remove this and just return an error so callers can // make their own fallback decisions... "self".into(), ] .into_iter() // Return the first option that exists in proc_rootfd. .find(|base| proc_rootfd.exists_unchecked(base).is_ok()) .expect("at least one candidate /proc/thread-self path should work"), } } // TODO: Add into_raw_path() that doesn't use symlinks? } /// Builder for [`ProcfsHandle`]. /// /// This is mainly intended for users that have specific requirements for the /// `/proc` they need to operate on. For the most part this would be users that /// need to frequently operate on global `/proc` files and thus need to have a /// non-`subset=pid` [`ProcfsHandle`] to use multiple times. /// /// ```rust /// # use pathrs::flags::OpenFlags; /// # use pathrs::procfs::{ProcfsBase, ProcfsHandleBuilder}; /// # use std::io::Read; /// let procfs = ProcfsHandleBuilder::new() /// .unmasked() /// .build()?; /// let mut uptime = String::new(); /// procfs.open(ProcfsBase::ProcRoot, "uptime", OpenFlags::O_RDONLY)? /// .read_to_string(&mut uptime)?; /// println!("uptime: {uptime}"); /// # Ok::<(), anyhow::Error>(()) /// ``` /// /// Most users should just use [`ProcfsHandle::new`] or the default /// configuration of [`ProcfsHandleBuilder`], as it provides the safest /// configuration without performance penalties for most users. #[derive(Clone, Debug)] pub struct ProcfsHandleBuilder { subset_pid: bool, } // MSRV(1.70): Use std::sync::OnceLock. static CACHED_PROCFS_HANDLE: OnceLock = OnceLock::new(); impl Default for ProcfsHandleBuilder { fn default() -> Self { Self::new() } } impl ProcfsHandleBuilder { /// Construct a new [`ProcfsHandleBuilder`] with the recommended /// configuration. #[inline] pub fn new() -> Self { Self { subset_pid: true } } // TODO: use_cached() -- allow users to control whether they get a cached // handle (if it is cacheable). // TODO: allow_global() -- allow users to control whether they want to allow // the usage of the global (non-detached) "/proc" as a final fallback. /// Specify whether to try to set `subset=pid` on the [`ProcfsHandle`]. /// /// `subset=pid` (available since Linux 5.8) disables all global procfs /// files (the vast majority of which are the main causes for concern if an /// attacker can write to them). Only [`ProcfsHandle`] instances with /// `subset=pid` configured can be cached (in addition to some other /// requirements). /// /// As a result, leaking the file descriptor of a [`ProcfsHandle`] with /// `subset=pid` *disabled* is **very** dangerous and so this method should /// be used sparingly -- ideally only temporarily by users which need to do /// a series of operations on global procfs files (such as sysctls). /// /// Most users can just use [`ProcfsHandle::new`] and then do operations on /// [`ProcfsBase::ProcRoot`]. In this case, a no-`subset=pid` /// [`ProcfsHandle`] will be created internally and will only be used *for /// that operation*, reducing the risk of leaks. #[inline] pub fn subset_pid(mut self, subset_pid: bool) -> Self { self.set_subset_pid(subset_pid); self } /// Setter form of [`ProcfsHandleBuilder::subset_pid`]. #[inline] pub fn set_subset_pid(&mut self, subset_pid: bool) -> &mut Self { self.subset_pid = subset_pid; self } /// Do not require any restrictions for the procfs handle. /// /// Unlike standalone methods for each configuration setting of /// [`ProcfsHandle`], this method will always clear all restrictions /// supported by [`ProcfsHandleBuilder`]. #[inline] pub fn unmasked(mut self) -> Self { self.set_unmasked(); self } /// Setter form of [`ProcfsHandleBuilder::unmasked`]. #[inline] pub fn set_unmasked(&mut self) -> &mut Self { self.subset_pid = false; self } /// Returns whether this [`ProcfsHandleBuilder`] will request a cacheable /// [`ProcfsHandle`]. #[inline] fn is_cache_friendly(&self) -> bool { self.subset_pid } /// Build the [`ProcfsHandle`]. /// /// For privileged users (those that have the ability to create mounts) on /// new enough kernels (Linux 5.2 or later), this created handle will be /// safe against racing attackers that have the ability to configure the /// mount table. /// /// For unprivileged users (or those on pre-5.2 kernels), this handle will /// only be safe against attackers that cannot actively modify the mount /// table while we are operating on it (which is usually more than enough /// protection -- after all, most attackers cannot mount anything in the /// first place -- but it is a notable limitation). /// /// # Caching /// /// For some configurations (namely, with `subset=pid` enabled), /// [`ProcfsHandleBuilder`] will internally cache the created /// [`ProcfsHandle`] and future requests with the same configuration will /// return a copy of the cached [`ProcfsHandle`]. /// /// As the cached [`ProcfsHandle`] will always have the same file /// descriptor, this means that you should not modify or close the /// underlying file descriptor for a [`ProcfsHandle`] returned by this /// method. /// /// # Panics /// /// If the cached [`ProcfsHandle`] has been invalidated, this method will /// panic as this is not a state that should be possible to reach in regular /// program execution. pub fn build(self) -> Result { // MSRV(1.85): Use let chain here (Rust 2024). if self.is_cache_friendly() { // If there is already a cached filesystem available, use that. if let Some(fd) = CACHED_PROCFS_HANDLE.get() { let procfs = ProcfsHandle::try_from_borrowed_fd(fd.as_fd()) .expect("cached procfs handle should be valid"); debug_assert!( procfs.is_subset && procfs.is_detached, "cached procfs handle should be subset=pid and detached" ); return Ok(procfs); } } let procfs = ProcfsHandle::new_fsopen(self.subset_pid) .or_else(|_| ProcfsHandle::new_open_tree(OpenTreeFlags::empty())) .or_else(|_| ProcfsHandle::new_open_tree(OpenTreeFlags::AT_RECURSIVE)) .or_else(|_| ProcfsHandle::new_unsafe_open()) .wrap("get safe procfs handle")?; // TODO: Add a way to require/verify that the requested properties will // be set, and then check them here before returning. match procfs { ProcfsHandle { inner: MaybeOwnedFd::OwnedFd(inner), is_subset: true, // must be subset=pid to cache (risk: dangerous files) is_detached: true, // must be detached to cache (risk: escape to host) .. } => { // Try to cache our new handle -- if another thread beat us to // it, just use the handle that they cached and drop the one we // created. let cached_inner = match CACHED_PROCFS_HANDLE.try_insert(inner) { Ok(inner) => MaybeOwnedFd::BorrowedFd(inner.as_fd()), Err((inner, _)) => MaybeOwnedFd::BorrowedFd(inner.as_fd()), }; // Do not return an error here -- it should be impossible for // this validation to fail after we get here. Ok(ProcfsHandle::try_from_maybe_owned_fd(cached_inner) .expect("cached procfs handle should be valid")) } procfs => Ok(procfs), } } } /// A borrowed version of [`ProcfsHandle`]. /// /// > NOTE: Actually, [`ProcfsHandle`] is an alias to this, but from an API /// > perspective it's probably easier to think of [`ProcfsHandleRef`] as being /// > derivative of [`ProcfsHandle`] -- as most users will probably use /// > [`ProcfsHandle::new`]. #[derive(Debug)] pub struct ProcfsHandleRef<'fd> { inner: MaybeOwnedFd<'fd, OwnedFd>, mnt_id: u64, is_subset: bool, is_detached: bool, pub(crate) resolver: ProcfsResolver, } /// > **NOTE**: Take great care when using this file descriptor -- it is very /// > easy to get attacked with a malicious procfs mount when using the file /// > descriptor directly. This should only be used in circumstances where you /// > cannot achieve your goal using `libpathrs` (in which case, please open an /// > issue to help us improve the API). impl<'fd> AsFd for ProcfsHandleRef<'fd> { fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } impl<'fd> ProcfsHandleRef<'fd> { // This is part of Linux's ABI. const PROC_ROOT_INO: u64 = 1; /// Convert an owned [`ProcfsHandle`] to the underlying [`OwnedFd`]. /// /// If the handle is internally a shared reference (i.e., it was constructed /// using [`ProcfsHandle::try_from_borrowed_fd`] or is using the global /// cached [`ProcfsHandle`]), this method will return `None`. /// /// > **NOTE**: Take great care when using this file descriptor -- it is /// > very easy to get attacked with a malicious procfs mount when using the /// > file descriptor directly. This should only be used in circumstances /// > where you cannot achieve your goal using `libpathrs` (in which case, /// > please open an issue to help us improve the API). // TODO: We probably should have a Result version. pub fn into_owned_fd(self) -> Option { self.inner.into_owned() } pub(crate) fn as_raw_procfs(&self) -> RawProcfsRoot<'_> { RawProcfsRoot::UnsafeFd(self.as_fd()) } /// Do `openat(2)` inside the procfs, but safely. fn openat_raw( &self, dirfd: BorrowedFd<'_>, subpath: &Path, oflags: OpenFlags, ) -> Result { let fd = self.resolver.resolve( self.as_raw_procfs(), dirfd, subpath, oflags, ResolverFlags::empty(), )?; self.verify_same_procfs_mnt(&fd)?; Ok(fd) } /// Open `ProcfsBase` inside the procfs. fn open_base(&self, base: ProcfsBase) -> Result { self.openat_raw( self.as_fd(), &base.into_path(self.as_raw_procfs()), OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, ) // TODO: For ProcfsBase::ProcPid, should ENOENT here be converted to // ESRCH to be more "semantically correct"? } /// Safely open a magic-link inside `procfs`. /// /// The semantics of this method are very similar to [`ProcfsHandle::open`], /// with the following differences: /// /// - The final component of the path will be opened with very minimal /// protections. This is necessary because magic-links by design involve /// mountpoint crossings and cannot be confined. This method does verify /// that the symlink itself doesn't have any overmounts, but this /// verification is only safe against races for [`ProcfsHandle`]s created /// by privileged users. /// /// - A trailing `/` at the end of `subpath` implies `O_DIRECTORY`. /// /// Most users should use [`ProcfsHandle::open`]. This method should only be /// used to open magic-links like `/proc/self/exe` or `/proc/self/fd/$n`. /// /// In addition (like [`ProcfsHandle::open`]), `open_follow` will not permit /// a magic-link to be a path component (ie. `/proc/self/root/etc/passwd`). /// This method *only* permits *trailing* symlinks. #[doc(alias = "pathrs_proc_open")] pub fn open_follow( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { let subpath = subpath.as_ref(); let mut oflags = oflags.into(); // Drop any trailing /-es. let (subpath, trailing_slash) = utils::path_strip_trailing_slash(subpath); if trailing_slash { // A trailing / implies we want O_DIRECTORY. oflags.insert(OpenFlags::O_DIRECTORY); } // If the target is not actually a magic-link, we are able to just use // the regular resolver to open the target (this even includes actual // symlinks) which is much safer. This also defends against C user // forgetting to set O_NOFOLLOW. // // This also gives us a chance to check if the target path is not // present because of subset=pid and retry (for magic-links we need to // operate on the target path more than once, which makes the retry // logic easier to do upfront here). let basedir = self.open_base(base)?; match self.openat_raw(basedir.as_fd(), subpath, oflags) { Ok(file) => return Ok(file.into()), Err(err) => { if self.is_subset && err.kind() == ErrorKind::OsError(Some(libc::ENOENT)) { // If the trial lookup failed due to ENOENT and the current // procfs handle is "masked" in some way, try to create a // temporary unmasked handle and retry the operation. return ProcfsHandleBuilder::new() .unmasked() .build() // Use the old error if creating a new handle failed. .or(Err(err))? .open_follow(base, subpath, oflags); } // If the error is ELOOP then the resolver probably hit a // magic-link, and so we have a reason to allow the // no-validation open we do later. // NOTE: Of course, this is not safe against races -- an // attacker could bind-mount a magic-link over a regular symlink // to trigger ELOOP and then unmount it after this point. As // always, fsopen(2) is needed for true safety here. if err.kind() != ErrorKind::OsError(Some(libc::ELOOP)) { return Err(err)?; } } } // Get a no-follow handle to the parent of the magic-link. let (parent, trailing) = utils::path_split(subpath)?; let trailing = trailing.ok_or_else(|| ErrorImpl::InvalidArgument { name: "path".into(), description: "proc_open_follow path has trailing slash".into(), })?; let parentdir = self.openat_raw( self.open_base(base)?.as_fd(), parent, OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, )?; // Rather than using self.mnt_id for the following check, we use the // mount ID from parent. This is necessary because ProcfsHandle::open // might create a brand-new procfs handle with a different mount ID. // However, ProcfsHandle::open already checks that the mount ID and // fstype are safe, so we can just reuse the mount ID we get without // issue. let parent_mnt_id = utils::fetch_mnt_id(self.as_raw_procfs(), &parentdir, "")?; // Detect if the magic-link we are about to open is actually a // bind-mount. There is no "statfsat" so we can't check that the f_type // is PROC_SUPER_MAGIC. However, an attacker can construct any // magic-link they like with procfs (as well as files that contain any // data they like and are no-op writeable), so it seems unlikely that // such a check would do anything in this case. // // NOTE: This check is only safe if there are no racing mounts, so only // for the ProcfsHandle::{new_fsopen,new_open_tree} cases. verify_same_mnt(self.as_raw_procfs(), parent_mnt_id, &parentdir, trailing)?; syscalls::openat_follow(parentdir, trailing, oflags, 0) .map(File::from) .map_err(|err| { ErrorImpl::RawOsError { operation: "open final magiclink component".into(), source: err, } .into() }) } /// Safely open a path inside `procfs`. /// /// The provided `subpath` is relative to the [`ProcfsBase`] (and must not /// contain `..` components -- [`openat2(2)`] permits `..` in some cases but /// the restricted `O_PATH` resolver for older kernels doesn't and thus /// using `..` could result in application errors when running on pre-5.6 /// kernels). /// /// The provided `OpenFlags` apply to the returned [`File`]. However, note /// that the following flags are not allowed and using them will result in /// an error: /// /// - `O_CREAT` /// - `O_EXCL` /// - `O_TMPFILE` /// /// # Symlinks /// /// This method *will not follow any magic links*, and also implies /// `O_NOFOLLOW` so *trailing symlinks will also not be followed* /// (regardless of type). Regular symlink path components are followed /// however (though lookups are forced to stay inside the `procfs` /// referenced by `ProcfsHandle`). /// /// If you wish to open a magic-link (such as `/proc/self/fd/$n` or /// `/proc/self/exe`), use [`ProcfsHandle::open_follow`] instead. /// /// # Mountpoint Crossings /// /// All mount point crossings are also forbidden (including bind-mounts), /// meaning that this method implies [`RESOLVE_NO_XDEV`][`openat2(2)`]. /// /// [`openat2(2)`]: https://www.man7.org/linux/man-pages/man2/openat2.2.html #[doc(alias = "pathrs_proc_open")] pub fn open( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { let mut oflags = oflags.into(); // Force-set O_NOFOLLOW. oflags.insert(OpenFlags::O_NOFOLLOW); // Do a basic lookup. let basedir = self.open_base(base)?; let subpath = subpath.as_ref(); let fd = self .openat_raw(basedir.as_fd(), subpath, oflags) .or_else(|err| { if self.is_subset && err.kind() == ErrorKind::OsError(Some(libc::ENOENT)) { // If the lookup failed due to ENOENT, and the current // procfs handle is "masked" in some way, try to create a // temporary unmasked handle and retry the operation. ProcfsHandleBuilder::new() .unmasked() .build() // Use the old error if creating a new handle failed. .or(Err(err))? .open(base, subpath, oflags) .map(OwnedFd::from) } else { Err(err) } })?; Ok(fd.into()) } /// Safely read the contents of a symlink inside `procfs`. /// /// This method is effectively shorthand for doing [`readlinkat(2)`] on the /// handle you'd get from `ProcfsHandle::open(..., OpenFlags::O_PATH)`. So /// all of the caveats from [`ProcfsHandle::open`] apply to this method as /// well. /// /// [`readlinkat(2)`]: https://www.man7.org/linux/man-pages/man2/readlinkat.2.html #[doc(alias = "pathrs_proc_readlink")] pub fn readlink(&self, base: ProcfsBase, subpath: impl AsRef) -> Result { let link = self.open(base, subpath, OpenFlags::O_PATH)?; syscalls::readlinkat(link, "").map_err(|err| { ErrorImpl::RawOsError { operation: "read procfs magiclink".into(), source: err, } .into() }) } fn verify_same_procfs_mnt(&self, fd: impl AsFd) -> Result<(), Error> { // Detect if the file we landed on is from a bind-mount. verify_same_mnt(self.as_raw_procfs(), self.mnt_id, &fd, "")?; // For pre-5.8 kernels there is no STATX_MNT_ID, so the best we can // do is check the fs_type to avoid mounts non-procfs filesystems. // Unfortunately, attackers can bind-mount procfs files and still // cause damage so this protection is marginal at best. verify_is_procfs(&fd) } /// Try to convert a [`BorrowedFd`] into a [`ProcfsHandle`] with the same /// lifetime. This method will return an error if the file handle is not /// actually the root of a procfs mount. pub fn try_from_borrowed_fd>>(inner: Fd) -> Result { Self::try_from_maybe_owned_fd(inner.into().into()) } fn try_from_maybe_owned_fd(inner: MaybeOwnedFd<'fd, OwnedFd>) -> Result { // Make sure the file is actually a procfs root. verify_is_procfs_root(inner.as_fd())?; let proc_rootfd = RawProcfsRoot::UnsafeFd(inner.as_fd()); let mnt_id = utils::fetch_mnt_id(proc_rootfd, inner.as_fd(), "")?; let resolver = ProcfsResolver::default(); // Figure out if the mount we have is subset=pid or hidepid=. For // hidepid we check if we can resolve /proc/1 -- if we can access it // then hidepid is probably not relevant. let is_subset = [/* subset=pid */ "stat", /* hidepid=n */ "1"] .iter() .any(|&subpath| { syscalls::accessat( inner.as_fd(), subpath, Access::EXISTS, AtFlags::SYMLINK_NOFOLLOW, ) .is_err() }); // Figure out if this file descriptor is a detached mount (i.e., from // fsmount(2) or OPEN_TREE_CLONE) by checking if ".." lets you get out // the procfs mount. Detached mounts take place in an anonymous mount // namespace rooted at the detached mount (so ".." is a no-op), while // regular opens will take place in a regular host filesystem where. // // If the handle is a detached mount, then ".." should be a procfs root // with the same mount ID. let is_detached = verify_same_mnt(proc_rootfd, mnt_id, inner.as_fd(), "..") .and_then(|_| { verify_is_procfs_root( syscalls::openat( inner.as_fd(), "..", OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, 0, ) .map_err(|err| ErrorImpl::RawOsError { operation: "get parent directory of procfs handle".into(), source: err, })?, ) }) .is_ok(); Ok(Self { inner, mnt_id, is_subset, is_detached, resolver, }) } } /// A wrapper around a handle to `/proc` that is designed to be safe against /// various attacks. /// /// Unlike most regular filesystems, `/proc` serves several important purposes /// for system administration programs: /// /// 1. As a mechanism for doing certain filesystem operations through /// `/proc/self/fd/...` (and other similar magic-links) that cannot be done /// by other means. /// 2. As a source of true information about processes and the general system. /// 3. As an administrative tool for managing other processes (such as setting /// LSM labels). /// /// libpathrs uses `/proc` internally for the first purpose and many libpathrs /// users use `/proc` for all three. As such, it is not sufficient that /// operations on `/proc` paths do not escape the `/proc` filesystem -- it is /// absolutely critical that operations through `/proc` operate **on the exact /// subpath that the caller requested**. /// /// This might seem like an esoteric concern, but there have been several /// security vulnerabilities where a maliciously configured `/proc` could be /// used to trick administrative processes into doing unexpected operations (for /// example, [CVE-2019-16884][] and [CVE-2019-19921][]). See [this /// video][lca2020] for a longer explanation of the many other issues that /// `/proc`-based checking is needed to protect against and [this other /// video][lpc2022] for some other procfs challenges libpathrs has to contend /// with. /// /// It should be noted that there is interest in Linux upstream to block certain /// classes of procfs overmounts entirely. Linux 6.12 notably introduced /// [several restrictions on such mounts][linux612-procfs-overmounts], [with /// plans to eventually block most-if-not-all overmounts inside /// `/proc/self`][lwn-procfs-overmounts]. `ProcfsHandle` is still useful for /// older kernels, as well as verifying that there aren't any tricky overmounts /// anywhere else in the procfs path (such as on top of `/proc/self`). /// /// NOTE: Users of `ProcfsHandle` should be aware that sometimes `/proc` /// overmounting is a feature -- tools like [lxcfs] provide better compatibility /// for system tools by overmounting global procfs files (notably /// `/proc/meminfo` and `/proc/cpuinfo` to emulate cgroup-aware support for /// containerisation in procfs). This means that using [`ProcfsBase::ProcRoot`] /// may result in errors on such systems for non-privileged users, even in the /// absence of an active attack. This is an intentional feature of libpathrs, /// but it may be unexpected. Note that (to the best of our knowledge), there /// are no benevolent tools which create mounts in `/proc/self` or /// `/proc/thread-self` (mainly due to scaling and correctness issues that would /// make production usage of such a tool impractical, even if such behaviour may /// be desirable). As a result, we would only expect [`ProcfsBase::ProcSelf`] /// and [`ProcfsBase::ProcThreadSelf`] operations to produce errors when you are /// actually being attacked. /// /// [cve-2019-16884]: https://nvd.nist.gov/vuln/detail/CVE-2019-16884 /// [cve-2019-19921]: https://nvd.nist.gov/vuln/detail/CVE-2019-19921 /// [lca2020]: https://youtu.be/tGseJW_uBB8 /// [lpc2022]: https://youtu.be/y1PaBzxwRWQ /// [lxcfs]: https://github.com/lxc/lxcfs /// [linux612-procfs-overmounts]: https://lore.kernel.org/all/20240806-work-procfs-v1-0-fb04e1d09f0c@kernel.org/ /// [lwn-procfs-overmounts]: https://lwn.net/Articles/934460/ pub type ProcfsHandle = ProcfsHandleRef<'static>; impl ProcfsHandle { /// Create a new `fsopen(2)`-based [`ProcfsHandle`]. This handle is safe /// against racing attackers changing the mount table and is guaranteed to /// have no overmounts because it is a brand-new procfs. pub(crate) fn new_fsopen(subset: bool) -> Result { let sfd = syscalls::fsopen("proc", FsOpenFlags::FSOPEN_CLOEXEC).map_err(|err| { ErrorImpl::RawOsError { operation: "create procfs suberblock".into(), source: err, } })?; if subset { // Try to configure hidepid=ptraceable,subset=pid if possible, but // ignore errors. let _ = syscalls::fsconfig_set_string(&sfd, "hidepid", "ptraceable"); let _ = syscalls::fsconfig_set_string(&sfd, "subset", "pid"); } syscalls::fsconfig_create(&sfd).map_err(|err| ErrorImpl::RawOsError { operation: "instantiate procfs superblock".into(), source: err, })?; syscalls::fsmount( &sfd, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::MOUNT_ATTR_NODEV | MountAttrFlags::MOUNT_ATTR_NOEXEC | MountAttrFlags::MOUNT_ATTR_NOSUID, ) .map_err(|err| { ErrorImpl::RawOsError { operation: "mount new private procfs".into(), source: err, } .into() }) // NOTE: try_from_fd checks this is an actual procfs root. .and_then(Self::try_from_fd) } /// Create a new `open_tree(2)`-based [`ProcfsHandle`]. This handle is /// guaranteed to be safe against racing attackers, and will not have /// overmounts unless `flags` contains `OpenTreeFlags::AT_RECURSIVE`. pub(crate) fn new_open_tree(flags: OpenTreeFlags) -> Result { syscalls::open_tree( syscalls::BADFD, "/proc", OpenTreeFlags::OPEN_TREE_CLONE | flags, ) .map_err(|err| { ErrorImpl::RawOsError { operation: "create private /proc bind-mount".into(), source: err, } .into() }) // NOTE: try_from_fd checks this is an actual procfs root. .and_then(Self::try_from_fd) } /// Create a plain `open(2)`-style [`ProcfsHandle`]. /// /// This handle is NOT safe against racing attackers and overmounts. pub(crate) fn new_unsafe_open() -> Result { syscalls::openat( syscalls::BADFD, "/proc", OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, 0, ) .map_err(|err| { ErrorImpl::RawOsError { operation: "open /proc handle".into(), source: err, } .into() }) // NOTE: try_from_fd checks this is an actual procfs root. .and_then(Self::try_from_fd) } /// Create a new handle that references a safe `/proc`. /// /// This method is just short-hand for: /// /// ```rust /// # use pathrs::procfs::ProcfsHandleBuilder; /// let procfs = ProcfsHandleBuilder::new().build()?; /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn new() -> Result { ProcfsHandleBuilder::new().subset_pid(true).build() } /// Try to convert a regular [`File`] handle to a [`ProcfsHandle`]. This /// method will return an error if the file handle is not actually the root /// of a procfs mount. pub fn try_from_fd>(inner: Fd) -> Result { Self::try_from_maybe_owned_fd(inner.into().into()) } } pub(crate) fn verify_is_procfs(fd: impl AsFd) -> Result<(), Error> { let fs_type = syscalls::fstatfs(fd) .map_err(|err| ErrorImpl::RawOsError { operation: "fstatfs proc handle".into(), source: err, })? .f_type; if fs_type != rustix_fs::PROC_SUPER_MAGIC { Err(ErrorImpl::OsError { operation: "verify lookup is still on a procfs mount".into(), source: IOError::from_raw_os_error(libc::EXDEV), }) .wrap(format!( "fstype mismatch in restricted procfs resolver (f_type is 0x{fs_type:X}, not 0x{:X})", rustix_fs::PROC_SUPER_MAGIC, ))? } Ok(()) } pub(crate) fn verify_is_procfs_root(fd: impl AsFd) -> Result<(), Error> { let fd = fd.as_fd(); // Make sure the file is actually a procfs handle. verify_is_procfs(fd)?; // And make sure it's the root of procfs. The root directory is // guaranteed to have an inode number of PROC_ROOT_INO. If this check // ever stops working, it's a kernel regression. let ino = fd.metadata().expect("fstat(/proc) should work").ino(); if ino != ProcfsHandle::PROC_ROOT_INO { Err(ErrorImpl::SafetyViolation { description: format!( "/proc is not root of a procfs mount (ino is 0x{ino:X}, not 0x{:X})", ProcfsHandle::PROC_ROOT_INO, ) .into(), })?; } Ok(()) } pub(crate) fn verify_same_mnt( proc_rootfd: RawProcfsRoot<'_>, root_mnt_id: u64, dirfd: impl AsFd, path: impl AsRef, ) -> Result<(), Error> { let mnt_id = utils::fetch_mnt_id(proc_rootfd, dirfd, path)?; // We the file we landed on a bind-mount / other procfs? if root_mnt_id != mnt_id { // Emulate RESOLVE_NO_XDEV's errors so that any failure looks like an // openat2(2) failure, as this function is used by the emulated procfs // resolver as well. Err(ErrorImpl::OsError { operation: "verify lookup is still in the same mount".into(), source: IOError::from_raw_os_error(libc::EXDEV), }) .wrap(format!( "mount id mismatch in restricted procfs resolver (mnt_id is {mnt_id:?}, not procfs {root_mnt_id:?})", ))? } Ok(()) } #[cfg(test)] mod tests { use super::*; use std::{fs::File, os::unix::io::AsRawFd}; use pretty_assertions::assert_eq; #[test] fn bad_root() { let file = File::open("/").expect("open root"); let procfs = ProcfsHandle::try_from_fd(file); assert!( procfs.is_err(), "creating a procfs handle from the wrong filesystem should return an error" ); } #[test] fn bad_tmpfs() { let file = File::open("/tmp").expect("open tmpfs"); let procfs = ProcfsHandle::try_from_fd(file); assert!( procfs.is_err(), "creating a procfs handle from the wrong filesystem should return an error" ); } #[test] fn bad_proc_nonroot() { let file = File::open("/proc/tty").expect("open tmpfs"); let procfs = ProcfsHandle::try_from_fd(file); assert!( procfs.is_err(), "creating a procfs handle from non-root of procfs should return an error" ); } #[test] fn builder_props() { assert_eq!( ProcfsHandleBuilder::new().subset_pid, true, "ProcfsHandleBuilder::new() should have subset_pid = true" ); assert_eq!( ProcfsHandleBuilder::default().subset_pid, true, "ProcfsHandleBuilder::default() should have subset_pid = true" ); assert_eq!( ProcfsHandleBuilder::new().subset_pid(true).subset_pid, true, "ProcfsHandleBuilder::subset_pid(true) should give subset_pid = true" ); let mut builder = ProcfsHandleBuilder::new(); builder.set_subset_pid(true); assert_eq!( builder.subset_pid, true, "ProcfsHandleBuilder::set_subset_pid(true) should give subset_pid = true" ); assert_eq!( ProcfsHandleBuilder::new().subset_pid(false).subset_pid, false, "ProcfsHandleBuilder::subset_pid(true) should give subset_pid = false" ); let mut builder = ProcfsHandleBuilder::new(); builder.set_subset_pid(false); assert_eq!( builder.subset_pid, false, "ProcfsHandleBuilder::set_subset_pid(false) should give subset_pid = false" ); assert_eq!( ProcfsHandleBuilder::new().unmasked().subset_pid, false, "ProcfsHandleBuilder::unmasked() should have subset_pid = false" ); let mut builder = ProcfsHandleBuilder::new(); builder.set_unmasked(); assert_eq!( builder.subset_pid, false, "ProcfsHandleBuilder::set_unmasked() should have subset_pid = false" ); } #[test] fn new() { let procfs = ProcfsHandle::new(); assert!( procfs.is_ok(), "new procfs handle should succeed, got {procfs:?}", ); } #[test] fn builder_build() { let procfs = ProcfsHandleBuilder::new().build(); assert!( procfs.is_ok(), "new procfs handle should succeed, got {procfs:?}", ); } #[test] fn builder_unmasked_build() { let procfs = ProcfsHandleBuilder::new() .unmasked() .build() .expect("should be able to get unmasked procfs handle"); assert!( !procfs.is_subset, "new unmasked procfs handle should have !subset=pid", ); } #[test] fn builder_unmasked_build_not_cached() { let procfs1 = ProcfsHandleBuilder::new() .unmasked() .build() .expect("should be able to get unmasked procfs handle"); let procfs2 = ProcfsHandleBuilder::new() .unmasked() .build() .expect("should be able to get unmasked procfs handle"); assert!( !procfs1.is_subset, "new unmasked procfs handle should have !subset=pid", ); assert!( !procfs2.is_subset, "new unmasked procfs handle should have !subset=pid", ); assert_eq!( procfs1.is_detached, procfs2.is_detached, "is_detached should be the same for both handles" ); assert_ne!( procfs1.as_fd().as_raw_fd(), procfs2.as_fd().as_raw_fd(), "unmasked procfs handles should NOT be cached and thus have different fds" ); } #[test] fn new_fsopen() { if let Ok(procfs) = ProcfsHandle::new_fsopen(false) { assert!( !procfs.is_subset, "ProcfsHandle::new_fsopen(false) should be !subset=pid" ); assert!( procfs.is_detached, "ProcfsHandle::new_fsopen(false) should be detached" ); } } #[test] fn new_fsopen_subset() { if let Ok(procfs) = ProcfsHandle::new_fsopen(true) { assert!( procfs.is_subset, "ProcfsHandle::new_fsopen(true) should be subset=pid" ); assert!( procfs.is_detached, "ProcfsHandle::new_fsopen(true) should be detached" ); } } #[test] fn new_open_tree() { if let Ok(procfs) = ProcfsHandle::new_open_tree(OpenTreeFlags::empty()) { assert!( !procfs.is_subset, "ProcfsHandle::new_open_tree() should be !subset=pid (same as host)" ); assert!( procfs.is_detached, "ProcfsHandle::new_open_tree() should be detached" ); } if let Ok(procfs) = ProcfsHandle::new_open_tree(OpenTreeFlags::AT_RECURSIVE) { assert!( !procfs.is_subset, "ProcfsHandle::new_open_tree(AT_RECURSIVE) should be !subset=pid (same as host)" ); assert!( procfs.is_detached, "ProcfsHandle::new_open_tree(AT_RECURSIVE) should be detached" ); } } #[test] fn new_unsafe_open() { let procfs = ProcfsHandle::new_unsafe_open() .expect("ProcfsHandle::new_unsafe_open should always work"); assert!( !procfs.is_subset, "ProcfsHandle::new_unsafe_open() should be !subset=pid" ); assert!( !procfs.is_detached, "ProcfsHandle::new_unsafe_open() should not be detached" ); } #[test] fn new_cached() { // Make sure the cache is filled (with nextest, each test is a separate // process and so gets a new fsopen(2) for the first ProcfsHandle::new // invocation). std::thread::spawn(|| { let _ = ProcfsHandle::new().expect("should be able to get ProcfsHandle in thread"); }) .join() .expect("ProcfsHandle::new thread should succeed"); let procfs1 = ProcfsHandle::new().expect("get procfs handle"); let procfs2 = ProcfsHandle::new().expect("get procfs handle"); assert_eq!( procfs1.is_subset, procfs2.is_subset, "subset=pid should be the same for both handles" ); assert_eq!( procfs1.is_detached, procfs2.is_detached, "is_detached should be the same for both handles" ); if procfs1.is_subset && procfs1.is_detached { assert_eq!( procfs1.as_fd().as_raw_fd(), procfs2.as_fd().as_raw_fd(), "subset=pid handles should be cached and thus have the same fd" ); } else { assert_ne!( procfs1.as_fd().as_raw_fd(), procfs2.as_fd().as_raw_fd(), "!subset=pid handles should NOT be cached and thus have different fds" ); } } #[test] fn builder_build_cached() { // Make sure the cache is filled (with nextest, each test is a separate // process and so gets a new fsopen(2) for the first ProcfsHandle::new // invocation). std::thread::spawn(|| { let _ = ProcfsHandleBuilder::new() .build() .expect("should be able to get ProcfsHandle in thread"); }) .join() .expect("ProcfsHandle::new thread should succeed"); let procfs1 = ProcfsHandleBuilder::new() .build() .expect("get procfs handle"); let procfs2 = ProcfsHandleBuilder::new() .build() .expect("get procfs handle"); assert_eq!( procfs1.is_subset, procfs2.is_subset, "subset=pid should be the same for both handles" ); assert_eq!( procfs1.is_detached, procfs2.is_detached, "is_detached should be the same for both handles" ); if procfs1.is_subset && procfs1.is_detached { assert_eq!( procfs1.as_fd().as_raw_fd(), procfs2.as_fd().as_raw_fd(), "subset=pid handles should be cached and thus have the same fd" ); } else { assert_ne!( procfs1.as_fd().as_raw_fd(), procfs2.as_fd().as_raw_fd(), "!subset=pid handles should NOT be cached and thus have different fds" ); } } } pathrs-0.2.1/src/resolvers/opath/imp.rs000064400000000000000000000574671046102023000162310ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ //! libpathrs::opath implements an emulated version of openat2(RESOLVE_IN_ROOT). //! The primary method by which this is done is through shameless abuse of //! procfs and O_PATH magic-links. The basic idea is that we need to perform all //! of the path resolution steps (walking down the set of components, handling //! the effect of symlinks on the resolution, etc). //! //! In order to do this safely we need to verify after the walk is done whether //! the path of the final file descriptor is what we expected (most importantly, //! is it inside the root which we started the walk with?). This check is done //! through readlink(/proc/self/fd/$n), which is a magic kernel interface which //! gives you the kernel's view of the path -- and in cases where the kernel is //! unsure or otherwise unhappy you get "/". //! //! If the check fails, we assume we are being attacked and return an error (and //! the caller can decide to re-try if they want). The kernel implementation //! will fail in fewer cases because it has access to in-kernel locks and other //! measures, but the final check through procfs should block all attack //! attempts. use crate::{ error::{Error, ErrorExt, ErrorImpl}, flags::{OpenFlags, ResolverFlags}, procfs::ProcfsHandle, resolvers::{opath::SymlinkStack, PartialLookup, MAX_SYMLINK_TRAVERSALS}, syscalls, utils::{self, FdExt, PathIterExt}, Handle, }; use std::{ collections::VecDeque, ffi::{OsStr, OsString}, io::Error as IOError, iter, os::unix::{ ffi::OsStrExt, fs::MetadataExt, io::{AsFd, OwnedFd}, }, path::{Path, PathBuf}, rc::Rc, }; use itertools::Itertools; use once_cell::sync::Lazy; /// Ensure that the expected path within the root matches the current fd. fn check_current( procfs: &ProcfsHandle, current: impl AsFd, root: impl AsFd, expected: impl AsRef, ) -> Result<(), Error> { // SAFETY: as_unsafe_path is safe here since we're using it to build a path // for a string-based check as part of a larger safety setup. This // path will be re-checked after the unsafe "current_path" is // generated. let root_path = root .as_unsafe_path(procfs) .wrap("get root path to construct expected path")?; // Combine the root path and our expected_path to get the full path to // compare current against. let full_path: PathBuf = root_path.join( // Path::join() has the unfortunate behaviour that a leading "/" will // result in the prefix path being removed. In practice we don't ever // hit this case (probably because RawComponents doesn't explicitly have // an equivalent of Components::RootDir), but just to be sure prepend a // "." component anyway. iter::once(OsStr::from_bytes(b".")) .chain(expected.as_ref().raw_components()) // NOTE: PathBuf::push() does not normalise components. .collect::(), ); // Does /proc/self/fd agree with us? There are several circumstances where // this check might give a false positive (namely, if the kernel decides // that the path is not ordinarily resolveable). But if this check passes, // then we can be fairly sure (barring kernel bugs) that the path was safe // at least one point in time. // SAFETY: as_unsafe_path is safe here since we're explicitly doing a // string-based check to see whether the path we want is correct. let current_path = current .as_unsafe_path(procfs) .wrap("check fd against expected path")?; // The paths should be identical. if current_path != full_path { Err(ErrorImpl::SafetyViolation { description: format!( "fd doesn't match expected path ({} != {})", current_path.display(), full_path.display() ) .into(), })? } // And the root should not have moved. Note that this check could (in // theory) be bypassed by an attacker -- so it important that users be aware // that allowing roots to be moved by an attacker is a very bad idea. // SAFETY: as_unsafe_path path is safe here because it's just used in a // string check -- and it's known that this check isn't perfect. let new_root_path = root .as_unsafe_path(procfs) .wrap("get root path to double-check it hasn't moved")?; if root_path != new_root_path { Err(ErrorImpl::SafetyViolation { description: "root moved during lookup".into(), })? } Ok(()) } /// Cached copy of `fs.protected_symlinks` sysctl. // TODO: In theory this value could change during the lifetime of the // program, but there's no nice way of detecting that, and the overhead of // checking this for every symlink lookup is more likely to be an issue. // MSRV(1.80): Use LazyLock. static PROTECTED_SYMLINKS_SYSCTL: Lazy = Lazy::new(|| { let procfs = ProcfsHandle::new().expect("should be able to get a procfs handle"); utils::sysctl_read_parse(&procfs, "fs.protected_symlinks") .expect("should be able to parse fs.protected_symlinks") }); /// Verify that we should follow the symlink as per `fs.protected_symlinks`. /// /// Because we emulate symlink following in userspace, the kernel cannot apply /// `fs.protected_symlinks` restrictions so we need to emulate them ourselves. fn may_follow_link(dir: impl AsFd, link: impl AsFd) -> Result<(), Error> { let link = link.as_fd(); // Not exposed by rustix. rustix::fs::StatVfs has a proper bitflags type but // StatVfsMountFlags doesn't provide ST_NOSYMFOLLOW because it's // Linux-specific. // // NOTE: We also can't use a const here because the exact type depends on // both the architecture and the backend used by rustix -- it's simpler to // just let Rust pick the right integer size. It would be really nice if we // could do something like "const A: typeof = foo". #[allow(non_snake_case)] let ST_NOSYMFOLLOW = 0x2000; // From . // If the symlink is on an MS_NOSYMFOLLOW mountpoint, we should block // resolution to match the behaviour of openat2. let link_statfs = syscalls::fstatfs(link).map_err(|err| ErrorImpl::RawOsError { operation: "fetch mount flags of symlink".into(), source: err, })?; if link_statfs.f_flags & ST_NOSYMFOLLOW == ST_NOSYMFOLLOW { Err(ErrorImpl::OsError { operation: "emulated MS_NOSYMFOLLOW".into(), source: IOError::from_raw_os_error(libc::ELOOP), })? } // Check that we aren't violating fs.protected_symlinks. let fsuid = syscalls::geteuid(); let dir_meta = dir.metadata().wrap("fetch directory metadata")?; let link_meta = link.metadata().wrap("fetch symlink metadata")?; const STICKY_WRITABLE: libc::mode_t = libc::S_ISVTX | libc::S_IWOTH; // We only do this if fs.protected_symlinks is enabled. if *PROTECTED_SYMLINKS_SYSCTL == 0 || // Allowed if owner and follower match. link_meta.uid() == fsuid || // Allowed if the directory is not sticky and world-writable. dir_meta.mode() & STICKY_WRITABLE != STICKY_WRITABLE || // Allowed if parent directory and link owner match. link_meta.uid() == dir_meta.uid() { Ok(()) } else { Err(ErrorImpl::OsError { operation: "emulated fs.protected_symlinks".into(), source: IOError::from_raw_os_error(libc::EACCES), } .into()) } } /// Common implementation used by `resolve_partial()` and `resolve()`. The main /// difference is that if `symlink_stack` is `true`, the returned paths // TODO: Make (flags, no_follow_trailing, symlink_stack) a single struct to // avoid possible issues with passing a bool to the wrong argument. fn do_resolve( root: impl AsFd, path: impl AsRef, flags: ResolverFlags, no_follow_trailing: bool, mut symlink_stack: Option<&mut SymlinkStack>, ) -> Result>, Error> { // We always need procfs for validation when using this resolver. let procfs = ProcfsHandle::new()?; // What is the final path we expect to get after we do the final open? This // allows us to track any attacker moving path components around and we can // sanity-check at the very end. This does not include rootpath. let mut expected_path = PathBuf::from("/"); // We only need to keep track of our current dirfd, since we are applying // the components one-by-one, and can always switch back to the root // if we hit an absolute symlink. let root = Rc::new( root.as_fd() .try_clone_to_owned() .map_err(|err| ErrorImpl::OsError { operation: "dup root handle as starting point of resolution".into(), source: err, })?, ); let mut current = Rc::clone(&root); // Get initial set of components from the passed path. We remove components // as we do the path walk, and update them with the contents of any symlinks // we encounter. Path walking terminates when there are no components left. let mut remaining_components = path .raw_components() .map(|p| p.to_os_string()) .collect::>(); let mut symlink_traversals = 0; while let Some(part) = remaining_components.pop_front() { // Stash a copy of the real remaining path. We can't just use // ::collect because we might have "" components, which // std::path::PathBuf don't like. let remaining: PathBuf = Itertools::intersperse( iter::once(&part) .chain(remaining_components.iter()) .map(OsString::as_os_str), OsStr::new("/"), ) .collect::() .into(); let part = match part.as_bytes() { // If we hit an empty component, we need to treat it as though it is // "." so that trailing "/" and "//" components on a non-directory // correctly return the right error code. b"" => ".".into(), // For "." component we don't touch expected_path, but we do try to // do the open (to return the correct openat2-compliant error if the // current path is a not directory). b"." => part, b".." => { // All of expected_path is non-symlinks, so we can treat ".." // lexically. If pop() fails, then we are at the root. // should . if !expected_path.pop() { // If we hit ".." due to the symlink we need to drop it from // the stack like we would if we walked into a real // component. Otherwise walking into ".." will result in a // broken symlink stack error. if let Some(ref mut stack) = symlink_stack { stack .pop_part(&part) .map_err(|err| ErrorImpl::BadSymlinkStackError { description: "walking into component".into(), source: err, })?; } current = Rc::clone(&root); continue; } part } _ => { // This part might be a symlink, but we clean that up later. expected_path.push(&part); // Ensure that part doesn't contain any "/"s. It's critical we // are only touching the final component in the path. If there // are any other path components we must bail. This shouldn't // ever happen, but it's better to be safe. if part.as_bytes().contains(&b'/') { Err(ErrorImpl::SafetyViolation { description: "component of path resolution contains '/'".into(), })? } part } }; // Get our next element. // MSRV(1.69): Remove &*. match syscalls::openat( &*current, &part, OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW, 0, ) .map_err(|err| { ErrorImpl::RawOsError { operation: "open next component of resolution".into(), source: err, } .into() }) { Err(err) => { return Ok(PartialLookup::Partial { handle: current, remaining, last_error: err, }); } Ok(next) => { // Make sure that the path is what we expect. If not, there was // a racing rename and we should bail out here -- otherwise we // might be tricked into revealing information outside the // rootfs through error or timing-related attacks. // // The safety argument for only needing to check ".." is // identical to the kernel implementation (namely, walking down // is safe by-definition). However, unlike the in-kernel version // we don't have the luxury of only doing this check when there // was a racing rename -- we have to do it every time. if part.as_bytes() == b".." { // MSRV(1.69): Remove &*. check_current(&procfs, &next, &*root, &expected_path) .wrap("check next '..' component didn't escape")?; } // Is the next dirfd a symlink or an ordinary path? If we're an // ordinary dirent, we just update current and move on to the // next component. Nothing special here. if !next .metadata() .wrap("fstat of next component")? .is_symlink() { // We hit a non-symlink component, so clear it from the // symlink stack. if let Some(ref mut stack) = symlink_stack { stack .pop_part(&part) .map_err(|err| ErrorImpl::BadSymlinkStackError { description: "walking into component".into(), source: err, })?; } // Just keep walking. current = next.into(); continue; } else { // If we hit the last component and we were told to not follow // the trailing symlink, just return the link we have. if remaining_components.is_empty() && no_follow_trailing { current = next.into(); break; } // Don't continue walking if user asked for no symlinks. if flags.contains(ResolverFlags::NO_SYMLINKS) { return Ok(PartialLookup::Partial { handle: current, remaining, // Construct a fake OS error containing ELOOP. last_error: ErrorImpl::OsError { operation: "emulated symlink resolution".into(), source: IOError::from_raw_os_error(libc::ELOOP), } .wrap(format!( "component {part:?} is a symlink but symlink resolution is disabled", )) .into(), }); } // Verify that we can follow the link. // MSRV(1.69): Remove &*. if let Err(err) = may_follow_link(&*current, &next) { return Ok(PartialLookup::Partial { handle: current, remaining, last_error: err .wrap(format!("component {part:?} is a symlink we cannot follow")), }); } // We need a limit on the number of symlinks we traverse to // avoid hitting filesystem loops and DoSing. symlink_traversals += 1; if symlink_traversals >= MAX_SYMLINK_TRAVERSALS { return Ok(PartialLookup::Partial { handle: current, remaining, // Construct a fake OS error containing ELOOP. last_error: ErrorImpl::OsError { operation: "emulated symlink resolution".into(), source: IOError::from_raw_os_error(libc::ELOOP), } .wrap("exceeded symlink limit") .into(), }); } let link_target = syscalls::readlinkat(&next, "").map_err(|err| ErrorImpl::RawOsError { operation: "readlink next symlink component".into(), source: err, })?; // Check if it's a good idea to walk this symlink. If we are on // a filesystem that supports magic-links and we've hit an // absolute symlink, it is incredibly likely that this component // is a magic-link and it makes no sense to try to resolve it in // userspace. // // NOTE: There are some pseudo-magic-links like /proc/self // (which dynamically generates the symlink contents but doesn't // use nd_jump_link). In the case of procfs, these are always // relative, and they are reasonable for us to walk. // // In procfs, all magic-links use d_path() to generate // readlink() and thus are all absolute paths. (Unfortunately, // apparmorfs uses nd_jump_link to make // /sys/kernel/security/apparmor/policy dynamic using actual // nd_jump_link() and their readlink give us a dummy relative // path like "apparmorfs:[123]". But in that case we will just // get an error.) if link_target.is_absolute() && next .is_magiclink_filesystem() .wrap("check if next is on a dangerous filesystem")? { Err(ErrorImpl::OsError { operation: "emulated RESOLVE_NO_MAGICLINKS".into(), source: IOError::from_raw_os_error(libc::ELOOP), }) .wrap("walked into a potential magic-link")? } // Swap out the symlink component in the symlink stack with // a new entry for the link target. if let Some(ref mut stack) = symlink_stack { stack .swap_link(&part, (¤t, remaining), link_target.clone()) .map_err(|err| ErrorImpl::BadSymlinkStackError { description: "walking into symlink".into(), source: err, })?; } // Remove the link component from our expectex path. expected_path.pop(); // Add contents of the symlink to the set of components we are // looping over. link_target .raw_components() .prepend(&mut remaining_components); // Absolute symlinks reset our current state back to /. if link_target.is_absolute() { current = Rc::clone(&root); expected_path = PathBuf::from("/"); } } } } } // Make sure that the path is what we expect... // MSRV(1.69): Remove &*. check_current(&procfs, &*current, &*root, &expected_path) .wrap("check final handle didn't escape")?; // We finished the lookup with no remaining components. Ok(PartialLookup::Complete(current)) } /// Resolve as many components as possible in `path` within `root` through /// user-space emulation. pub(crate) fn resolve_partial( root: impl AsFd, path: impl AsRef, flags: ResolverFlags, no_follow_trailing: bool, ) -> Result>, Error> { // For partial lookups, we need to use a SymlinkStack to match openat2. let mut symlink_stack = SymlinkStack::new(); match do_resolve( root, path, flags, no_follow_trailing, Some(&mut symlink_stack), ) { // For complete and error paths, just return what we got. ret @ Ok(PartialLookup::Complete(_)) => ret, err @ Err(_) => err, // If the lookup failed part-way through, modify the (handle, remaining) // based on the symlink stack if applicable. Ok(PartialLookup::Partial { handle, remaining, last_error, }) => match symlink_stack.pop_top_symlink() { // We were in the middle of symlink resolution, so return the error // from the context of the top symlink in the resolution, to match // openat2(2). Some((handle, remaining)) => Ok(PartialLookup::Partial { handle, remaining, last_error, }), // Nothing in the symlink stack, return what we got. None => Ok(PartialLookup::Partial { handle, remaining, last_error, }), }, } } /// Resolve `path` within `root` through user-space emulation. pub(crate) fn resolve( root: impl AsFd, path: impl AsRef, flags: ResolverFlags, no_follow_trailing: bool, ) -> Result { do_resolve(root, path, flags, no_follow_trailing, None).and_then(TryInto::try_into) } pathrs-0.2.1/src/resolvers/opath/symlink_stack.rs000064400000000000000000000516771046102023000203140ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ //! SymlinkStack is used to emulate how `openat2::resolve_partial` treats //! dangling symlinks. //! //! If we hit a non-existent path while resolving a symlink, we need to return //! the `(current: Rc, remaining_components: PathBuf)` we had when we //! hit the symlink (effectively making the symlink resolution all-or-nothing). //! The set of `(current, remaining_components)` set is stored within the //! SymlinkStack and we add and or remove parts when we hit symlink and //! non-symlink components respectively. This needs to be implemented as a stack //! because of nested symlinks (if there is a dangling symlink 10 levels deep //! into lookup, we need to return the *first* symlink we walked into to match //! `openat2::resolve_partial`). //! //! Note that the stack is ONLY used for book-keeping to adjust what we *return* //! in case of lookup errors. All of the path walking logic is still based on //! remaining_components and expected_path! use crate::utils::PathIterExt; use std::{ collections::VecDeque, ffi::{OsStr, OsString}, fmt, os::unix::ffi::OsStrExt, path::PathBuf, rc::Rc, }; #[derive(thiserror::Error, Debug, PartialEq)] pub(crate) enum SymlinkStackError { #[error("[internal] empty stack")] EmptyStack, #[error("[internal error] broken symlink stack: trying to pop component {part:?} from an empty stack entry")] BrokenStackEmpty { part: OsString }, #[error("[internal error] broken symlink stack: trying to pop component {part:?} but expected {expected:?}")] BrokenStackWrongComponent { part: OsString, expected: OsString }, } #[derive(Debug)] struct SymlinkStackEntry { /// The current directory and remaining path at the point where we entered /// this symlink. state: (Rc, PathBuf), /// The remaining path components we have to walk from the symlink that lead /// us here. Once we finish walking these components, this symlink has been /// fully resolved and can be dropped from the stack (unless the trailing /// component was a symlink, see `swap_link` for details). unwalked_link_parts: VecDeque, } #[derive(Debug)] pub(crate) struct SymlinkStack(VecDeque>); impl SymlinkStack { fn do_push(&mut self, (dir, remaining): (&Rc, PathBuf), link_target: PathBuf) { // Get a proper Rc. let dir = Rc::clone(dir); // Split the link target and clean up any "" parts. let link_parts = link_target .raw_components() .map(OsString::from) // Drop any "" or "." no-op components. .filter(|part| !part.is_empty() && part.as_bytes() != b".") .collect::>(); self.0.push_back(SymlinkStackEntry { state: (dir, remaining), unwalked_link_parts: link_parts, }) } fn do_pop(&mut self, part: &OsStr) -> Result<(), SymlinkStackError> { if part.as_bytes() == b"." { // "." components are no-ops -- we drop them in do_push(). return Ok(()); } let tail_entry = match self.0.len() { 0 => return Err(SymlinkStackError::EmptyStack), n => self .0 .get_mut(n - 1) .expect("VecDeque.get(len-1) should work"), }; // Pop the next unwalked link component, but make sure the component // matches what we expect. match tail_entry.unwalked_link_parts.front() { None => return Err(SymlinkStackError::BrokenStackEmpty { part: part.into() }), Some(expected) => { if expected != part { return Err(SymlinkStackError::BrokenStackWrongComponent { part: part.into(), expected: expected.into(), }); } } }; // Drop the component. let _ = tail_entry.unwalked_link_parts.pop_front(); // If that was the last unwalked link component, we *do not* remove the // entry here. That's done by pop_part() if we are dealing with a // non-symlink path component. swap_link() needs to keep this entry so // that if we we are in a "tail-chained" symlink and we hit a // non-existent path we return the right value from pop_top_symlink(). Ok(()) } pub(crate) fn pop_part(&mut self, part: &OsStr) -> Result<(), SymlinkStackError> { match self.do_pop(part) { Err(SymlinkStackError::EmptyStack) => return Ok(()), Err(err) => return Err(err), Ok(_) => (), }; // Since this was a regular path component, clean up any "tail-chained" // symlinks in the stack (those without any remaining unwalked link // parts). // TODO: Use && let once // is stabilised. while !self.0.is_empty() { let entry = self .0 .back() .expect("should be able to get last element in non-empty stack"); if entry.unwalked_link_parts.is_empty() { self.0.pop_back(); } else { // Quit once we hit a non-empty entry. break; } } Ok(()) } pub(crate) fn swap_link( &mut self, link_part: &OsStr, (dir, remaining): (&Rc, PathBuf), link_target: PathBuf, ) -> Result<(), SymlinkStackError> { // If we are currently inside a symlink resolution, remove the symlink // component from the last symlink entry, but don't remove the entry // itself even if it's empty. If we are a "tail-chained" symlink (a // trailing symlink we hit during a symlink resolution) we need to keep // the original symlink until we finish the resolution to return the // right result if this link chain turns out to be dangling. match self.do_pop(link_part) { Err(SymlinkStackError::EmptyStack) | Ok(_) => { // Push the component regardless of whether the stack was empty. self.do_push((dir, remaining), link_target); Ok(()) } Err(err) => Err(err), } } pub(crate) fn pop_top_symlink(&mut self) -> Option<(Rc, PathBuf)> { self.0.pop_front().map(|entry| entry.state) } pub(crate) fn new() -> Self { Self(VecDeque::new()) } } #[cfg(test)] mod tests { use super::SymlinkStackError; use std::{ path::{Path, PathBuf}, rc::Rc, }; use pretty_assertions::assert_eq; // Use strings rather than actual files for the symlink stack tests. type SymlinkStack = super::SymlinkStack; fn dump_stack(stack: &SymlinkStack) { for (idx, entry) in stack.0.iter().enumerate() { println!( "ss[{idx}]: <{}>/{:?} [->{:?}]", entry.state.0, entry.state.1, entry.unwalked_link_parts ); } } macro_rules! stack_ops { ($ss:ident @impl $do:block => $expected_result:expr) => { println!("> before operation"); dump_stack(&$ss); let res = $do; println!("> after operation"); dump_stack(&$ss); assert_eq!(res, $expected_result, "unexpected result"); }; ($ss:ident @fn swap_link($link_part:expr, $dir:expr, $remaining:expr, $link_target:expr) => $expected_result:expr) => { stack_ops! { $ss @impl { let link_part = Path::new($link_part).as_os_str(); let dir = Rc::new($dir.into()); let remaining = PathBuf::from($remaining); let link_target = PathBuf::from($link_target); $ss.swap_link(link_part, (&dir, remaining), link_target) } => $expected_result } }; ($ss:ident @fn pop_part($part:expr) => $expected_result:expr) => { stack_ops! { $ss @impl { let part = Path::new($part).as_os_str(); $ss.pop_part(part) } => $expected_result } }; ($ss:ident @fn pop_top_symlink() => $expected_result:expr) => { let expected_result: Option<(String, PathBuf)> = $expected_result .map(|(current, remaining)| (current.into(), remaining.into())); stack_ops! { $ss @impl { $ss.pop_top_symlink() .map(|(dir, current)| (String::from(&*dir), current)) } => expected_result } }; ([$ss:ident] { $( $op:ident ( $($args:tt)* ) => $expected_result:expr );* $(;)? }) => { $( { println!("-- operation {}{:?}", stringify!($op), ($($args)*)); stack_ops! { $ss @fn $op ( $($args)* ) => $expected_result } } )* } } macro_rules! stack_content { ([$ss:ident] == { $((($current:expr, $remaining:expr), {$($unwalked_parts:expr),* $(,)?})),* $(,)? }) => { { let stack_contents = $ss. 0 .iter() .map(|entry| {( (String::from(&*entry.state.0), entry.state.1.clone()), entry.unwalked_link_parts.iter().cloned().collect::>(), )}) .collect::>(); let expected = vec![ $( ((String::from($current), $remaining.into()), vec![$($unwalked_parts.into()),*]) ),* ]; assert_eq!(stack_contents, expected, "stack content mismatch") } } } #[test] fn basic() { let mut stack = SymlinkStack::new(); stack_ops! { [stack] { swap_link("foo", "A", "anotherbit", "bar/baz") => Ok(()); swap_link("bar", "B", "baz", "abcd") => Ok(()); pop_part("abcd") => Ok(()); swap_link("baz", "C", "", "taillink") => Ok(()); pop_part("taillink") => Ok(()); } }; assert!(stack.0.is_empty(), "stack should be empty"); assert_eq!( stack.pop_top_symlink(), None, "pop_top_symlink should give None with empty stack" ); stack_ops! { [stack] { pop_part("anotherbit") => Ok(()); } }; assert!(stack.0.is_empty(), "stack should be empty"); assert_eq!( stack.pop_top_symlink(), None, "pop_top_symlink should give None with empty stack" ); } #[test] fn basic_pop_top_symlink() { let mut stack = SymlinkStack::new(); stack_ops! { [stack] { swap_link("foo", "A", "anotherbit", "bar/baz") => Ok(()); swap_link("bar", "B", "baz", "abcd") => Ok(()); pop_part("abcd") => Ok(()); swap_link("baz", "C", "", "taillink") => Ok(()); pop_top_symlink() => Some(("A", "anotherbit")); } }; } #[test] fn bad_pop_part() { let mut stack = SymlinkStack::new(); stack_ops! { [stack] { swap_link("foo", "A", "anotherbit", "bar/baz") => Ok(()); swap_link("bar", "B", "baz", "abcd") => Ok(()); swap_link("bad", "C", "", "taillink") => Err(SymlinkStackError::BrokenStackWrongComponent { part: "bad".into(), expected: "abcd".into(), }); pop_part("abcd") => Ok(()); swap_link("baz", "C", "", "taillink") => Ok(()); pop_part("bad") => Err(SymlinkStackError::BrokenStackWrongComponent { part: "bad".into(), expected: "taillink".into(), }); pop_part("taillink") => Ok(()); } }; assert!(stack.0.is_empty(), "stack should be empty"); stack_ops! { [stack] { pop_part("anotherbit") => Ok(()); } }; assert!(stack.0.is_empty(), "stack should be empty"); } #[test] fn basic_tail_chain() { let mut stack = SymlinkStack::new(); stack_ops! { [stack] { swap_link("foo", "A", "", "tailA") => Ok(()); swap_link("tailA", "B", "", "tailB") => Ok(()); swap_link("tailB", "C", "", "tailC") => Ok(()); swap_link("tailC", "D", "", "tailD") => Ok(()); swap_link("tailD", "E", "", "foo/taillink") => Ok(()); } }; stack_content! { [stack] == { // The top 4 entries should have no unwalked links. (("A", ""), {}), (("B", ""), {}), (("C", ""), {}), (("D", ""), {}), // Final entry should be foo/taillink. (("E", ""), {"foo", "taillink"}), } }; // Popping "foo" should keep the tail-chain. stack_ops! { [stack] { pop_part("foo") => Ok(()); } }; stack_content! { [stack] == { // The top 4 entries should have no unwalked links. (("A", ""), {}), (("B", ""), {}), (("C", ""), {}), (("D", ""), {}), // Final entry should be just taillink. (("E", ""), {"taillink"}), } }; // Popping "taillink" should empty the stack. stack_ops! { [stack] { pop_part("taillink") => Ok(()); } }; assert!(stack.0.is_empty(), "stack should be empty"); } #[test] fn stacked_tail_chain() { let mut stack = SymlinkStack::new(); stack_ops! { [stack] { swap_link("foo", "A", "", "tailA/subdir") => Ok(()); // First tail-chain. swap_link("tailA", "B", "", "tailB") => Ok(()); swap_link("tailB", "C", "", "tailC") => Ok(()); swap_link("tailC", "D", "", "tailD") => Ok(()); swap_link("tailD", "E", "", "taillink1/subdir") => Ok(()); // Second tail-chain. swap_link("taillink1", "F", "", "tailE") => Ok(()); swap_link("tailE", "G", "", "tailF") => Ok(()); swap_link("tailF", "H", "", "tailG") => Ok(()); swap_link("tailG", "I", "", "tailH") => Ok(()); swap_link("tailH", "J", "", "tailI") => Ok(()); swap_link("tailI", "K", "", "taillink2/..") => Ok(()); } }; stack_content! { [stack] == { // The top entry is not a tail-chain. (("A", ""), {"subdir"}), // The first tail-chain should have no unwalked links. (("B", ""), {}), (("C", ""), {}), (("D", ""), {}), // Final entry in the first tail-chain. (("E", ""), {"subdir"}), // The second tail-chain should have no unwalked links. (("F", ""), {}), (("G", ""), {}), (("H", ""), {}), (("I", ""), {}), (("J", ""), {}), // Final entry in the second tail-chain. (("K", ""), {"taillink2", ".."}), } }; // Check that nonsense operations don't break the stack. stack_ops! { [stack] { // Trying to pop "." should do nothing. pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); pop_part(".") => Ok(()); // Popping any of the early tail chain entries must fail. pop_part("subdir") => Err(SymlinkStackError::BrokenStackWrongComponent { part: "subdir".into(), expected: "taillink2".into(), }); pop_part("..") => Err(SymlinkStackError::BrokenStackWrongComponent { part: "..".into(), expected: "taillink2".into(), }); } }; // NOTE: Same contents as above. stack_content! { [stack] == { // The top entry is not a tail-chain. (("A", ""), {"subdir"}), // The first tail-chain should have no unwalked links. (("B", ""), {}), (("C", ""), {}), (("D", ""), {}), // Final entry in the first tail-chain. (("E", ""), {"subdir"}), // The second tail-chain should have no unwalked links. (("F", ""), {}), (("G", ""), {}), (("H", ""), {}), (("I", ""), {}), (("J", ""), {}), // Final entry in the second tail-chain. (("K", ""), {"taillink2", ".."}), } }; // Popping part of the last chain should keep both tail-chains. stack_ops! { [stack] { pop_part("taillink2") => Ok(()); } } stack_content! { [stack] == { // The top entry is not a tail-chain. (("A", ""), {"subdir"}), // The first tail-chain should have no unwalked links. (("B", ""), {}), (("C", ""), {}), (("D", ""), {}), // Final entry in the first tail-chain. (("E", ""), {"subdir"}), // The second tail-chain should have no unwalked links. (("F", ""), {}), (("G", ""), {}), (("H", ""), {}), (("I", ""), {}), (("J", ""), {}), // Final entry in the second tail-chain. (("K", ""), {".."}), } }; // Popping the last entry should only drop the final tail-chain. stack_ops! { [stack] { pop_part("..") => Ok(()); } } stack_content! { [stack] == { // The top entry is not a tail-chain. (("A", ""), {"subdir"}), // The first tail-chain should have no unwalked links. (("B", ""), {}), (("C", ""), {}), (("D", ""), {}), // Final entry in the first tail-chain. (("E", ""), {"subdir"}), } }; // Popping the last entry should only drop the tail-chain. stack_ops! { [stack] { pop_part("subdir") => Ok(()); } } stack_content! { [stack] == { // The top entry is not a tail-chain. (("A", ""), {"subdir"}), } }; // Popping "subdir" should empty the stack. stack_ops! { [stack] { pop_part("subdir") => Ok(()); } }; assert!(stack.0.is_empty(), "stack should be empty"); } } pathrs-0.2.1/src/resolvers/openat2.rs000064400000000000000000000145111046102023000156600ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::{Error, ErrorImpl}, flags::{OpenFlags, ResolverFlags}, resolvers::PartialLookup, syscalls::{self, OpenHow}, utils::PathIterExt, Handle, }; use std::{ fs::File, os::unix::io::AsFd, path::{Path, PathBuf}, }; /// Open `path` within `root` through `openat(2)`. /// /// This is an optimised version of `resolve(root, path, ...)?.reopen(flags)`. pub(crate) fn open( root: impl AsFd, path: impl AsRef, rflags: ResolverFlags, oflags: OpenFlags, ) -> Result { if !*syscalls::OPENAT2_IS_SUPPORTED { Err(ErrorImpl::NotSupported { feature: "openat2".into(), })? } let rflags = libc::RESOLVE_IN_ROOT | libc::RESOLVE_NO_MAGICLINKS | rflags.bits(); let how = OpenHow { flags: oflags.bits() as u64, resolve: rflags, ..Default::default() }; syscalls::openat2_follow(&root, path.as_ref(), how) .map(File::from) .map_err(|err| { ErrorImpl::RawOsError { operation: "openat2 one-shot open".into(), source: err, } .into() }) } /// Resolve `path` within `root` through `openat2(2)`. pub(crate) fn resolve( root: impl AsFd, path: impl AsRef, rflags: ResolverFlags, no_follow_trailing: bool, ) -> Result { if !*syscalls::OPENAT2_IS_SUPPORTED { Err(ErrorImpl::NotSupported { feature: "openat2".into(), })? } // Copy the O_NOFOLLOW and RESOLVE_NO_SYMLINKS bits from flags. let mut oflags = OpenFlags::O_PATH; if no_follow_trailing { oflags.insert(OpenFlags::O_NOFOLLOW); } let rflags = libc::RESOLVE_IN_ROOT | libc::RESOLVE_NO_MAGICLINKS | rflags.bits(); let how = OpenHow { flags: oflags.bits() as u64, resolve: rflags, ..Default::default() }; // openat2(2) can fail with -EAGAIN if there was a racing rename or mount // *anywhere on the system*. This can happen pretty frequently, so what we // do is attempt the openat2(2) a couple of times. // // Based on some fairly extensive tests, with 128 retries you only have a // ~0.1% chance of hitting the error path (even with an attacker pounding on // rename on all cores). Users that need stricter retry requirements can do // their own higher-level retry loop based on the errno. const MAX_RETRIES: u8 = 128; let mut tries = 0u8; loop { tries += 1; match syscalls::openat2_follow(&root, path.as_ref(), how) { Ok(file) => return Ok(Handle::from_fd(file)), Err(err) => match (tries, err.root_cause().raw_os_error()) { // MSRV(1.66): Use ..=MAX_RETRIES (half_open_range_patterns). (0..=MAX_RETRIES, Some(libc::EAGAIN)) => continue, (_, Some(libc::ENOSYS)) => { // shouldn't happen Err(ErrorImpl::NotSupported { feature: "openat2".into(), })? } // TODO: Add wrapper for known-bad openat2 return codes. //Some(libc::EXDEV) | Some(libc::ELOOP) => { ... } _ => Err(ErrorImpl::RawOsError { operation: "openat2 subpath".into(), source: err, })?, }, } } } /// Resolve as many components as possible in `path` within `root` using /// `openat2(2)`. pub(crate) fn resolve_partial( root: impl AsFd, path: impl AsRef, rflags: ResolverFlags, no_follow_trailing: bool, ) -> Result, Error> { let root = root.as_fd(); let path = path.as_ref(); let mut last_error = match resolve(root, path, rflags, no_follow_trailing) { Ok(handle) => return Ok(PartialLookup::Complete(handle)), Err(err) => err, }; // TODO: We probably want to do a git-bisect-like binary-search here. For // paths with a large number of components this could make a // significant difference, though in practice you'll only see fairly // short paths so the implementation complexity might not be worth it. for (path, remaining) in path.partial_ancestors() { if last_error.is_safety_violation() { // If we hit a safety violation, we return an error instead of a // partial resolution to match the behaviour of the O_PATH // resolver (and to avoid some possible weird bug in libpathrs // being exploited to return some result to Root::mkdir_all). return Err(last_error); } match resolve(root, path, rflags, no_follow_trailing) { Ok(handle) => { return Ok(PartialLookup::Partial { handle, remaining: remaining.map(PathBuf::from).unwrap_or("".into()), last_error, }) } Err(err) => last_error = err, } } unreachable!("partial_ancestors should include root path which must be resolvable"); } pathrs-0.2.1/src/resolvers/procfs.rs000064400000000000000000000744401046102023000156130ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ //! //! [`ProcfsResolver`](crate::resolvers::procfs::ProcfsResolver) is a very //! minimal resolver that doesn't allow: //! //! 1. Any ".." components (with `openat2` this is slightly relaxed). //! 2. Any absolute symlinks. //! 3. (If `statx` or `openat2` is supported), any mount-point crossings. //! //! This allows us to avoid using any `/proc` checks, and thus this resolver can //! be used within the `pathrs::procfs` helpers that are used by other parts of //! libpathrs. use crate::{ error::{Error, ErrorExt, ErrorImpl}, flags::{OpenFlags, ResolverFlags}, procfs, resolvers::MAX_SYMLINK_TRAVERSALS, syscalls::{self, OpenHow}, utils::{self, FdExt, PathIterExt, RawProcfsRoot}, }; use std::{ collections::VecDeque, io::Error as IOError, os::unix::{ ffi::OsStrExt, io::{AsFd, OwnedFd}, }, path::Path, }; /// Used internally for tests to force the usage of a specific resolver. You /// should always use the default. #[derive(Debug, PartialEq, Eq)] pub(crate) enum ProcfsResolver { Openat2, RestrictedOpath, } impl Default for ProcfsResolver { fn default() -> Self { if *syscalls::OPENAT2_IS_SUPPORTED { Self::Openat2 } else { Self::RestrictedOpath } } } impl ProcfsResolver { pub(crate) fn resolve( &self, proc_rootfd: RawProcfsRoot<'_>, root: impl AsFd, path: impl AsRef, oflags: OpenFlags, rflags: ResolverFlags, ) -> Result { // These flags don't make sense for procfs and will just result in // confusing errors during lookup. O_TMPFILE contains multiple flags // (including O_DIRECTORY!) so we have to check it separately. let invalid_flags = OpenFlags::O_CREAT | OpenFlags::O_EXCL; if !oflags.intersection(invalid_flags).is_empty() || oflags.contains(OpenFlags::O_TMPFILE) { Err(ErrorImpl::InvalidArgument { name: "flags".into(), description: format!( "invalid flags {:?} specified", oflags.intersection(invalid_flags) ) .into(), })? } match *self { Self::Openat2 => openat2_resolve(root, path, oflags, rflags), Self::RestrictedOpath => opath_resolve(proc_rootfd, root, path, oflags, rflags), } } } /// [`openat2`][openat2.2]-based implementation of [`ProcfsResolver`]. /// /// [openat2.2]: https://www.man7.org/linux/man-pages/man2/openat2.2.html fn openat2_resolve( root: impl AsFd, path: impl AsRef, oflags: OpenFlags, rflags: ResolverFlags, ) -> Result { if !*syscalls::OPENAT2_IS_SUPPORTED { Err(ErrorImpl::NotSupported { feature: "openat2".into(), })? } // Copy the O_NOFOLLOW and RESOLVE_NO_SYMLINKS bits from rflags. let oflags = oflags.bits() as u64; let rflags = libc::RESOLVE_BENEATH | libc::RESOLVE_NO_MAGICLINKS | libc::RESOLVE_NO_XDEV | rflags.bits(); syscalls::openat2_follow( root, path, OpenHow { flags: oflags, resolve: rflags, ..Default::default() }, ) .map_err(|err| { ErrorImpl::RawOsError { operation: "open subpath in procfs".into(), source: err, } .into() }) } /// Returns whether the provided string plausibly looks like a magic-link /// `readlink(2)` target. fn check_possible_magic_link(link_target: &Path) -> Result<(), Error> { // This resolver only deals with procfs paths, which means that we can // restrict how we handle symlinks. procfs does not (and cannot) contain // regular absolute symlinks to paths within procfs, and so we can assume // any absolute paths are magic-links to regular files or would otherwise // trigger EXDEV with openat2. (Note that all procfs magic-links use // `d_path` as the readlink(2) pseudo-target.) if link_target.is_absolute() { Err(ErrorImpl::OsError { operation: "emulated RESOLVE_NO_MAGICLINKS".into(), source: IOError::from_raw_os_error(libc::ELOOP), }) .wrap(format!("step into absolute symlink {link_target:?}"))? } // However, some magic-links appear as relative paths because they reference // custom anon-inodes or other objects with custom `d_path` callbacks (and // thus custom names). Without openat2(2) there isn't an obvious way to // detect this with 100% accuracy, but we can safely assume that no regular // symlink will have names that look like these special symlinks (they // typically look like "foo:[bar]"). // // For reference, at time of writing (Linux 6.17), all of the regular // symlinks in stock procfs (and their corresponding readlink targets) // are listed below. // // * /proc/self -> "" (auto-generated) // * /proc/thread-self -> "/task/" (auto-generated) // * /proc/net -> "self/net" // * /proc/mounts -> "self/mounts" // // Followed by the following procfs symlinks defined by other modules // (using proc_symlink()): // // * /proc/ppc64 -> "powerpc" (on ppc64) // * /proc/rtas -> "powerpc/rtas" (on ppc) // * /proc/device-tree -> "/sys/firmware/devicetree/base" // * /proc/fs/afs -> "../self/net/afs" (afs) // * /proc/fs/fscache -> "netfs" (netfs) // * /proc/fs/nfsfs/servers -> "../../net/nfsfs/servers" (nfs) // * /proc/fs/nfsfs/volumes -> "../../net/nfsfs/volumes" (nfs) // * /proc/fs/xfs/stat -> "/sys/fs/xfs/stat/stats" (xfs) // * /proc/asound/ -> "" (sound) // // As you can see, none of them match the format of anon-inodes and so // blocking symlinks that look like that is reasonable. It is possible for // /proc/asound/* symlinks to have arbitrary data, but it seems very // unlikely for a card to have a name that looks like "foo:[bar]". // The regex crate is too heavy for us to use it for such a simple string // match. Instead, let's just do a quick-and-dirty search to see if the // characters ":[]" are present in the string and are in the right order. // MSRV(1.65): Switch to regex-lite? if link_target .as_os_str() .to_string_lossy() .chars() .filter(|&c| c == ':' || c == '[' || c == ']') .collect::() == ":[]" { Err(ErrorImpl::OsError { operation: "emulated RESOLVE_NO_MAGICLINKS".into(), source: IOError::from_raw_os_error(libc::ELOOP), }) .wrap(format!("step into likely magiclink {link_target:?}"))? } Ok(()) } /// `O_PATH`-based implementation of [`ProcfsResolver`]. fn opath_resolve( proc_rootfd: RawProcfsRoot<'_>, root: impl AsFd, path: impl AsRef, oflags: OpenFlags, rflags: ResolverFlags, ) -> Result { let root = root.as_fd(); let root_mnt_id = utils::fetch_mnt_id(proc_rootfd, root, "")?; // We only need to keep track of our current dirfd, since we are applying // the components one-by-one. let mut current = root .try_clone_to_owned() .map_err(|err| ErrorImpl::OsError { operation: "dup root handle as starting point of resolution".into(), source: err, })?; // In order to match the behaviour of RESOLVE_BENEATH, we need to error out // if we get asked to resolve an absolute path. let path = path.as_ref(); if path.is_absolute() { Err(ErrorImpl::OsError { operation: "emulated RESOLVE_BENEATH".into(), source: IOError::from_raw_os_error(libc::EXDEV), }) .wrap(format!( "requested subpath {path:?} is absolute but this is forbidden by RESOLVE_BENEATH", ))? } // Get initial set of components from the passed path. We remove components // as we do the path walk, and update them with the contents of any symlinks // we encounter. Path walking terminates when there are no components left. let mut remaining_components = path .raw_components() .map(|p| p.to_os_string()) .collect::>(); let mut symlink_traversals = 0; while let Some(part) = remaining_components .pop_front() // If we hit an empty component, we need to treat it as though it is // "." so that trailing "/" and "//" components on a non-directory // correctly return the right error code. .map(|part| if part.is_empty() { ".".into() } else { part }) { // We cannot walk into ".." without checking if there was a breakout // with /proc (a-la opath::resolve) so return an error if we hit "..". if part.as_bytes() == b".." { Err(ErrorImpl::OsError { operation: "step into '..'".into(), source: IOError::from_raw_os_error(libc::EXDEV), }) .wrap("cannot walk into '..' with restricted procfs resolver")? } // Get our next element. let next = syscalls::openat( ¤t, &part, OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW, 0, ) .map_err(|err| ErrorImpl::RawOsError { operation: "open next component of resolution".into(), source: err, })?; // Check that the next component is on the same mountpoint. // NOTE: If the root is the host /proc mount, this is only safe if there // are no racing mounts. procfs::verify_same_mnt(proc_rootfd, root_mnt_id, &next, "") .with_wrap(|| format!("open next component {part:?}")) .wrap("emulated procfs resolver RESOLVE_NO_XDEV")?; let next_meta = next.metadata().wrap("fstat of next component")?; // If this is the last component, try to open the same component again // with with the requested flags. Unlike the other Handle resolvers, we // can't re-open the file through procfs (since this is the resolver // used for procfs lookups) so we need to do it this way. // // Because we force O_NOFOLLOW for safety reasons, we can't just blindly // return the error we get from openat here (in particular, if the user // specifies O_PATH or O_DIRECTORY without O_NOFOLLOW, you will get the // wrong results). The following is a table of the relevant cases. // // Each entry of the form [a](b) means that the user expects [a] to // happen but because of O_NOFOLLOW we get (b). **These are the cases // which we need to handle with care.** // // symlink directory other-file // // OPATH [cont](ret-sym) *1 ret ret // ODIR [cont](ENOTDIR) *2 ret ENOTDIR // OPATH|ODIR [cont](ENOTDIR) *3 ret ENOTDIR // ONF ELOOP ret ret // ONF|OPATH ret-sym *4 ret ret // ONF|ODIR ENOTDIR ret ENOTDIR // ONF|OPATH|ODIR ENOTDIR ret EDOTDIR // // Legend: // - Flags: // - OPATH = O_PATH, ODIR = O_DIRECTORY, ONF = O_NOFOLLOW // - Actions: // - ret = return this handle as the final component // - ret-sym = return this *symlink* handle as the final component // - cont = continue iterating (for symlinks) // - EFOO = returns an error EFOO // // Unfortunately, note that you -ENOTDIR for most of the file and // symlink cases, but we need to differentiate between them. That's why // we need to do the O_PATH|O_NOFOLLOW first -- we need to figure out // whether we are dealing with a symlink or not. If we are dealing with // a symlink, we want to continue walking in all cases (except plain // O_NOFOLLOW and O_DIRECTORY|O_NOFOLLOW). // // NOTE: There is a possible race here -- the file type might've changed // after we opened it. This is unlikely under procfs because the // structure is basically static (an attacker could bind-mount something // but we detect bind-mounts already), but even if it did happen the // worst case result is that we return an error. // // NOTE: Most of these cases don't apply to the ProcfsResolver because // it handles trailing-symlink follows manually and auto-applies // O_NOFOLLOW if the trailing component is not a symlink. However, we // handle them all for correctness reasons (and we have tests for the // resolver itself to verify the behaviour). if remaining_components.is_empty() // Case (*1): // If the user specified *just* O_PATH (without O_NOFOLLOW nor // O_DIRECTORY), we can continue to parse as normal (if next_type is // a non-symlink we will return it, if it is a symlink we will // continue walking). && oflags.intersection(OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW | OpenFlags::O_DIRECTORY) != OpenFlags::O_PATH { match syscalls::openat(¤t, &part, oflags | OpenFlags::O_NOFOLLOW, 0) { // NOTE: This will silently add O_NOFOLLOW to the set of flags // you see in fcntl(F_GETFL). In practice this isn't an issue, // but it is a detectable difference between the O_PATH resolver // and openat2. Unfortunately F_SETFL silently ignores // O_NOFOLLOW so we cannot clear this flag (the only option // would be a procfs re-open -- but this *is* the procfs re-open // code!). Ok(final_reopen) => { // Re-verify the next component is on the same mount. procfs::verify_same_mnt(proc_rootfd, root_mnt_id, &final_reopen, "") .wrap("re-open final component") .wrap("emulated procfs resolver RESOLVE_NO_XDEV")?; return Ok(final_reopen); } Err(err) => { // Cases (*2) and (*3): // // If all of the following are true: // // 1. The user didn't ask for O_NOFOLLOW. // 2. The user did ask for O_DIRECTORY. // 3. The error is ENOTDIR. // 4. The next component was a symlink. // // We want to continue walking, rather than return an error. if oflags.contains(OpenFlags::O_NOFOLLOW) || !oflags.contains(OpenFlags::O_DIRECTORY) || err.root_cause().raw_os_error() != Some(libc::ENOTDIR) || !next_meta.is_symlink() { Err(ErrorImpl::RawOsError { operation: format!("open last component of resolution with {oflags:?}") .into(), source: err, })? } } } } // Is the next dirfd a symlink or an ordinary path? If we're an ordinary // dirent, we just update current and move on to the next component. // Nothing special here. if !next_meta.is_symlink() { current = next; continue; } // Don't continue walking if user asked for no symlinks. if rflags.contains(ResolverFlags::NO_SYMLINKS) { Err(ErrorImpl::OsError { operation: "emulated symlink resolution".into(), source: IOError::from_raw_os_error(libc::ELOOP), }) .wrap(format!( "component {part:?} is a symlink but symlink resolution is disabled", ))? } // We need a limit on the number of symlinks we traverse to avoid // hitting filesystem loops and DoSing. // // Given all of the other restrictions of this lookup code, it seems // unlikely that you could even run into a symlink loop (procfs doesn't // have regular symlink loops) but we should avoid it just in case. symlink_traversals += 1; if symlink_traversals >= MAX_SYMLINK_TRAVERSALS { Err(ErrorImpl::OsError { operation: "emulated symlink resolution".into(), source: IOError::from_raw_os_error(libc::ELOOP), }) .wrap("exceeded symlink limit")? } let link_target = syscalls::readlinkat(&next, "").map_err(|err| ErrorImpl::RawOsError { operation: "readlink next symlink component".into(), source: err, })?; // If this symlink is a magic-link, we will likely end up trying to walk // into a non-existent path (or possibly an attacker-controlled procfs // subpath) so we reject any link target that looks like a magic-link. check_possible_magic_link(&link_target) .wrap("cannot walk into potential magiclinks with restricted procfs resolver")?; link_target .raw_components() .prepend(&mut remaining_components); } Ok(current) } #[cfg(test)] mod tests { use crate::{ error::{Error as PathrsError, ErrorKind}, flags::{OpenFlags, ResolverFlags}, resolvers::procfs::ProcfsResolver, syscalls, tests::common as tests_common, utils::{FdExt, RawProcfsRoot}, }; use std::{ fs::File, path::{Path, PathBuf}, }; use anyhow::Error; use pretty_assertions::{assert_eq, assert_matches}; type ExpectedResult = Result; macro_rules! procfs_resolver_tests { ($($test_name:ident ($root:expr, $path:expr, $($oflag:ident)|+, $rflags:expr) == $expected_result:expr);* $(;)?) => { $( paste::paste! { #[test] fn []() -> Result<(), Error> { if !*syscalls::OPENAT2_IS_SUPPORTED { // skip test return Ok(()); } let root_dir: PathBuf = $root.into(); let root = File::open(&root_dir)?; let expected: ExpectedResult = $expected_result.map(|p: PathBuf| root_dir.join(p)); let oflags = $(OpenFlags::$oflag)|*; let res = ProcfsResolver::Openat2 .resolve(RawProcfsRoot::UnsafeGlobal, &root, $path, oflags, $rflags) .as_ref() .map(|f| { f.as_unsafe_path_unchecked() .expect("get actual path of resolved handle") }) .map_err(PathrsError::kind); assert_eq!( res, expected, "expected resolve({:?}, {:?}, {:?}, {:?}) to give {:?}, got {:?}", $root, $path, oflags, $rflags, expected, res ); Ok(()) } #[test] fn []() -> Result<(), Error> { let root_dir: PathBuf = $root.into(); let root = File::open(&root_dir)?; let expected: ExpectedResult = $expected_result.map(|p: PathBuf| root_dir.join(p)); let oflags = $(OpenFlags::$oflag)|*; let res = ProcfsResolver::RestrictedOpath .resolve(RawProcfsRoot::UnsafeGlobal, &root, $path, oflags, $rflags) .as_ref() .map(|f| { f.as_unsafe_path_unchecked() .expect("get actual path of resolved handle") }) .map_err(PathrsError::kind); assert_eq!( res, expected, "expected resolve({:?}, {:?}, {:?}, {:?}) to give {:?}, got {:?}", $root, $path, oflags, $rflags, expected, res ); Ok(()) } } )* }; } procfs_resolver_tests! { xdev("/", "proc", O_DIRECTORY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::EXDEV))); xdev_dotdot("/proc", "..", O_DIRECTORY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::EXDEV))); xdev_abs_slash("/proc", "/", O_DIRECTORY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::EXDEV))); xdev_abs_path("/proc", "/etc/passwd", O_DIRECTORY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::EXDEV))); bad_flag_ocreat("/tmp", "foobar", O_CREAT|O_RDWR, ResolverFlags::empty()) == Err(ErrorKind::InvalidArgument); bad_flag_otmpfile("/tmp", "foobar", O_TMPFILE|O_RDWR, ResolverFlags::empty()) == Err(ErrorKind::InvalidArgument); // Check RESOLVE_NO_SYMLINKS handling. resolve_no_symlinks1("/proc", "self", O_DIRECTORY, ResolverFlags::NO_SYMLINKS) == Err(ErrorKind::OsError(Some(libc::ELOOP))); resolve_no_symlinks2("/proc", "self/status", O_RDONLY, ResolverFlags::NO_SYMLINKS) == Err(ErrorKind::OsError(Some(libc::ELOOP))); resolve_no_symlinks3("/proc", "self/../cgroups", O_RDONLY, ResolverFlags::NO_SYMLINKS) == Err(ErrorKind::OsError(Some(libc::ELOOP))); // Check RESOLVE_NO_MAGICLINKS handling. symlink("/proc", "self", O_DIRECTORY, ResolverFlags::empty()) == Ok(format!("/proc/{}", syscalls::getpid()).into()); symlink_onofollow("/proc", "mounts", O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); symlink_opath_onofollow("/proc", "mounts", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok("mounts".into()); symlink_parent("/proc", "net/unix", O_RDONLY, ResolverFlags::empty()) == Ok(format!("/proc/{}/net/unix", syscalls::getpid()).into()); symlink_parent_onofollow("/proc", "net/unix", O_NOFOLLOW, ResolverFlags::empty()) == Ok(format!("/proc/{}/net/unix", syscalls::getpid()).into()); symlink_parent_opath_onofollow("/proc", "net/unix", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok(format!("/proc/{}/net/unix", syscalls::getpid()).into()); magiclink_absolute("/proc", "self/fd/0", O_RDWR, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_absolute_onofollow("/proc", "self/fd/0", O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_absolute_opath_onofollow("/proc", "self/fd/0", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok(format!("/proc/{}/fd/0", syscalls::getpid()).into()); magiclink_absolute_parent("/proc", "self/root/etc/passwd", O_RDONLY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_absolute_parent_onofollow("/proc", "self/cwd/foo", O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_absolute_parent_opath_onofollow("/proc", "self/cwd/abc", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_anoninode("/proc", "self/ns/pid", O_PATH, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_anoninode_onofollow("/proc", "self/ns/user", O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_anoninode_opath_nofollow("/proc", "self/ns/user", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok(format!("/proc/{}/ns/user", syscalls::getpid()).into()); magiclink_anoninode_parent("/proc", "self/ns/mnt/foo", O_RDONLY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_anoninode_parent_onofollow("/proc", "self/ns/mnt/foo", O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); magiclink_anoninode_parent_opath_onofollow("/proc", "self/ns/uts/foo", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); // Check symlink loops. symloop(tests_common::create_basic_tree()?.keep(), "loop/basic-loop1", O_PATH, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); symloop_opath_onofollow(tests_common::create_basic_tree()?.keep(), "loop/basic-loop1", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok("loop/basic-loop1".into()); // Check that our {O_PATH, O_NOFOLLOW, O_DIRECTORY} logic is correct, // based on the table in opath_resolve(). // OPATH [cont](ret) *1 ret ret sym_opath("/proc", "self", O_PATH, ResolverFlags::empty()) == Ok(format!("/proc/{}", syscalls::getpid()).into()); dir_opath("/proc", "tty", O_PATH, ResolverFlags::empty()) == Ok("/proc/tty".into()); file_opath("/proc", "filesystems", O_PATH, ResolverFlags::empty()) == Ok("/proc/filesystems".into()); // ODIR [cont](ENOTDIR) *2 ret ENOTDIR sym_odir("/proc", "self", O_DIRECTORY, ResolverFlags::empty()) == Ok(format!("/proc/{}", syscalls::getpid()).into()); dir_odir("/proc", "tty", O_DIRECTORY, ResolverFlags::empty()) == Ok("/proc/tty".into()); file_odir("/proc", "filesystems", O_DIRECTORY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ENOTDIR))); // OPATH|ODIR [cont](ENOTDIR) *3 ret ENOTDIR sym_opath_odir("/proc", "self", O_PATH|O_DIRECTORY, ResolverFlags::empty()) == Ok(format!("/proc/{}", syscalls::getpid()).into()); dir_opath_odir("/proc", "tty", O_PATH|O_DIRECTORY, ResolverFlags::empty()) == Ok("/proc/tty".into()); file_opath_odir("/proc", "filesystems", O_PATH|O_DIRECTORY, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ENOTDIR))); // ONF ELOOP ret ret sym_onofollow("/proc", "self", O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ELOOP))); dir_onofollow("/proc", "tty", O_NOFOLLOW, ResolverFlags::empty()) == Ok("/proc/tty".into()); file_onofollow("/proc", "filesystems", O_NOFOLLOW, ResolverFlags::empty()) == Ok("/proc/filesystems".into()); // ONF|OPATH ret-sym ret ret sym_opath_onofollow("/proc", "self", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok("/proc/self".into()); dir_opath_onofollow("/proc", "tty", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok("/proc/tty".into()); file_opath_onofollow("/proc", "filesystems", O_PATH|O_NOFOLLOW, ResolverFlags::empty()) == Ok("/proc/filesystems".into()); // ONF|ODIR ENOTDIR ret ENOTDIR sym_odir_onofollow("/proc", "self", O_DIRECTORY|O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dir_odir_onofollow("/proc", "tty", O_DIRECTORY|O_NOFOLLOW, ResolverFlags::empty()) == Ok("/proc/tty".into()); file_odir_onofollow("/proc", "filesystems", O_DIRECTORY|O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ENOTDIR))); // ONF|OPATH|ODIR ENOTDIR ret EDOTDIR sym_opath_odir_onofollow("/proc", "self", O_PATH|O_DIRECTORY|O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dir_opath_odir_onofollow("/proc", "tty", O_PATH|O_DIRECTORY|O_NOFOLLOW, ResolverFlags::empty()) == Ok("/proc/tty".into()); file_opath_odir_onofollow("/proc", "filesystems", O_PATH|O_DIRECTORY|O_NOFOLLOW, ResolverFlags::empty()) == Err(ErrorKind::OsError(Some(libc::ENOTDIR))); } #[test] fn check_possible_magic_link() { // Regular symlinks. assert_matches!(super::check_possible_magic_link(Path::new("foo")), Ok(_)); assert_matches!(super::check_possible_magic_link(Path::new("12345")), Ok(_)); assert_matches!( super::check_possible_magic_link(Path::new("12345/foo/bar/baz")), Ok(_) ); assert_matches!( super::check_possible_magic_link(Path::new("../../../../net/foo/bar")), Ok(_) ); // Absolute symlinks. assert_matches!(super::check_possible_magic_link(Path::new("/")), Err(_)); assert_matches!( super::check_possible_magic_link(Path::new("/foo/bar")), Err(_) ); // anon-inode-like symlinks. assert_matches!( super::check_possible_magic_link(Path::new("user:[123456123123]")), Err(_) ); assert_matches!( super::check_possible_magic_link(Path::new("pipe:[12345]")), Err(_) ); assert_matches!( super::check_possible_magic_link(Path::new("anon_inode:[pidfd]")), Err(_) ); } } pathrs-0.2.1/src/resolvers.rs000064400000000000000000000250631046102023000143140ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #![forbid(unsafe_code)] //! Resolver implementations for libpathrs. use crate::{ error::{Error, ErrorImpl, ErrorKind}, flags::{OpenFlags, ResolverFlags}, syscalls, utils::FdExt, Handle, }; use std::{ fs::File, io::Error as IOError, os::unix::io::{AsFd, OwnedFd}, path::{Path, PathBuf}, rc::Rc, }; use once_cell::sync::Lazy; /// `O_PATH`-based userspace resolver. pub(crate) mod opath { mod imp; pub(crate) use imp::*; mod symlink_stack; pub(crate) use symlink_stack::{SymlinkStack, SymlinkStackError}; } /// `openat2(2)`-based in-kernel resolver. pub(crate) mod openat2; /// A limited resolver only used for `/proc` lookups in `ProcfsHandle`. pub(crate) mod procfs; /// Maximum number of symlink traversals we will accept. const MAX_SYMLINK_TRAVERSALS: usize = 128; /// The backend used for path resolution within a [`Root`] to get a [`Handle`]. /// /// We don't generally recommend specifying this, since libpathrs will /// automatically detect the best backend for your platform (which is the value /// returned by `Resolver::default`). However, this can be useful for testing. /// /// [`Root`]: crate::Root /// [`Handle`]: crate::Handle #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub(crate) enum ResolverBackend { /// Use the native `openat2(2)` backend (requires kernel support). KernelOpenat2, /// Use the userspace "emulated" backend. EmulatedOpath, // TODO: Implement a HardcoreEmulated which does pivot_root(2) and all the // rest of it. It'd be useful to compare against and for some // hyper-concerned users. } // MSRV(1.80): Use LazyLock. static DEFAULT_RESOLVER_TYPE: Lazy = Lazy::new(|| { if *syscalls::OPENAT2_IS_SUPPORTED { ResolverBackend::KernelOpenat2 } else { ResolverBackend::EmulatedOpath } }); impl Default for ResolverBackend { fn default() -> Self { *DEFAULT_RESOLVER_TYPE } } impl ResolverBackend { /// Checks if the resolver is supported on the current platform. #[cfg(test)] pub(crate) fn supported(self) -> bool { match self { ResolverBackend::KernelOpenat2 => *syscalls::OPENAT2_IS_SUPPORTED, ResolverBackend::EmulatedOpath => true, } } } /// Resolover backend and its associated flags. /// /// This is the primary structure used to configure how a given [`Root`] will /// conduct path resolutions. /// /// [`Root`]: crate::Root #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Resolver { /// Underlying resolution backend used. pub(crate) backend: ResolverBackend, /// Flags to pass to the resolution backend. pub flags: ResolverFlags, } /// Only used for internal resolver implementations. #[derive(Debug)] pub(crate) enum PartialLookup { Complete(H), Partial { handle: H, remaining: PathBuf, last_error: E, }, } impl AsRef for PartialLookup { fn as_ref(&self) -> &H { match self { Self::Complete(handle) => handle, Self::Partial { handle, .. } => handle, } } } impl TryInto for PartialLookup { type Error = Error; fn try_into(self) -> Result { match self { Self::Complete(handle) => Ok(handle), Self::Partial { last_error, .. } => Err(last_error), } } } impl TryInto for PartialLookup> { type Error = Error; fn try_into(self) -> Result { PartialLookup::::from(self).try_into() } } impl TryInto<(Handle, Option)> for PartialLookup { type Error = Error; fn try_into(self) -> Result<(Handle, Option), Self::Error> { match self { Self::Complete(handle) => Ok((handle, None)), Self::Partial { handle, remaining, last_error, } => match last_error.kind() { ErrorKind::OsError(Some(libc::ENOENT)) => Ok((handle, Some(remaining))), _ => Err(last_error), }, } } } impl From>> for PartialLookup { fn from(result: PartialLookup>) -> Self { let (rc, partial) = match result { PartialLookup::Complete(rc) => (rc, None), PartialLookup::Partial { handle, remaining, last_error, } => (handle, Some((remaining, last_error))), }; // We are now sure that there is only a single reference to whatever // current points to. There is nowhere else we could've stashed a // reference, and we only do Rc::clone for root (which we've dropped). let handle = Handle::from_fd( // MSRV(1.70): Use Rc::into_inner(). Rc::try_unwrap(rc) .expect("current handle in lookup should only have a single Rc reference"), ); match partial { None => Self::Complete(handle), Some((remaining, last_error)) => Self::Partial { handle, remaining, last_error, }, } } } impl Resolver { pub(crate) fn open( &self, root: impl AsFd, path: impl AsRef, flags: impl Into, ) -> Result { let flags = flags.into(); // O_CREAT cannot be emulated by the O_PATH resolver (and in the // fallback case the flag gets silently ignored unless you also set // O_EXCL) so we need to explicitly return an error if it is provided. if flags.intersects(OpenFlags::O_CREAT | OpenFlags::O_EXCL) { Err(ErrorImpl::InvalidArgument { name: "oflags".into(), description: "open flags to one-shot open cannot contain O_CREAT or O_EXCL".into(), })? } match self.backend { // openat2 can do the lookup and open in one syscall. ResolverBackend::KernelOpenat2 => openat2::open(root, path.as_ref(), self.flags, flags), // For backends without an accelerated one-shot open() // implementation, we can just do the lookup+reopen thing in one go. // For cffi users, this makes plain "open" operations faster. _ => { let handle = self.resolve(root, path, flags.contains(OpenFlags::O_NOFOLLOW))?; // O_NOFOLLOW makes things a little tricky. Unlike // FdExt::reopen, we have to support O_NOFOLLOW|O_PATH of // symlinks, but that is easily emulated by returning the handle // directly without a reopen. if handle.metadata()?.is_symlink() { // If the user also asked for O_DIRECTORY, make sure we // return the right error. if flags.contains(OpenFlags::O_DIRECTORY) { Err(ErrorImpl::OsError { operation: "emulated openat2".into(), source: IOError::from_raw_os_error(libc::ENOTDIR), })?; } // If the user requested O_PATH|O_NOFOLLOW, then the only // option we have is to return the handle we got. Without // O_EMPTYPATH there is no easy way to apply any extra flags // a user might've requested. // TODO: Should we error out if the user asks for extra // flags that don't match the flags for our handles? if flags.contains(OpenFlags::O_PATH) { return Ok(OwnedFd::from(handle).into()); } // Otherwise, the user asked for O_NOFOLLOW and we saw a // symlink, so return ELOOP like openat2 would. Err(ErrorImpl::OsError { operation: "emulated openat2".into(), source: IOError::from_raw_os_error(libc::ELOOP), })?; } handle.reopen(flags) } } } #[inline] pub(crate) fn resolve( &self, root: impl AsFd, path: impl AsRef, no_follow_trailing: bool, ) -> Result { match self.backend { ResolverBackend::KernelOpenat2 => { openat2::resolve(root, path, self.flags, no_follow_trailing) } ResolverBackend::EmulatedOpath => { opath::resolve(root, path, self.flags, no_follow_trailing) } } } #[inline] pub(crate) fn resolve_partial( &self, root: impl AsFd, path: impl AsRef, no_follow_trailing: bool, ) -> Result, Error> { match self.backend { ResolverBackend::KernelOpenat2 => { openat2::resolve_partial(root, path.as_ref(), self.flags, no_follow_trailing) } ResolverBackend::EmulatedOpath => { opath::resolve_partial(root, path.as_ref(), self.flags, no_follow_trailing) // Rc -> Handle .map(Into::into) } } } } pathrs-0.2.1/src/root.rs000064400000000000000000001676621046102023000132670ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #![forbid(unsafe_code)] use crate::{ error::{Error, ErrorExt, ErrorImpl}, flags::{OpenFlags, RenameFlags, ResolverFlags}, resolvers::Resolver, syscalls::{self, FrozenFd}, utils::{self, PathIterExt}, Handle, }; use std::{ fs::{File, Permissions}, io::Error as IOError, os::unix::{ ffi::OsStrExt, fs::PermissionsExt, io::{AsFd, BorrowedFd, OwnedFd}, }, path::{Path, PathBuf}, }; use rustix::{ fs::{self as rustix_fs, AtFlags, FileType}, io::Errno, }; /// An inode type to be created with [`Root::create`]. #[derive(Clone, Debug)] #[non_exhaustive] pub enum InodeType { /// Ordinary file, as in [`creat(2)`]. /// /// [`creat(2)`]: http://man7.org/linux/man-pages/man2/creat.2.html // XXX: It is possible to support non-O_EXCL O_CREAT with the native // backend. But it's unclear whether we should expose it given it's // only supported on native-kernel systems. File(Permissions), /// Directory, as in [`mkdir(2)`]. /// /// [`mkdir(2)`]: http://man7.org/linux/man-pages/man2/mkdir.2.html Directory(Permissions), /// Symlink with the given path, as in [`symlinkat(2)`]. /// /// Note that symlinks can contain any arbitrary `CStr`-style string (it /// doesn't need to be a real pathname). We don't do any verification of the /// target name. /// /// [`symlinkat(2)`]: http://man7.org/linux/man-pages/man2/symlinkat.2.html Symlink(PathBuf), /// Hard-link to the given path, as in [`linkat(2)`]. /// /// The provided path is resolved within the [`Root`]. It is currently /// not supported to hardlink a file inside the [`Root`]'s tree to a file /// outside the [`Root`]'s tree. // XXX: Should we ever support that? /// /// [`linkat(2)`]: http://man7.org/linux/man-pages/man2/linkat.2.html Hardlink(PathBuf), /// Named pipe (aka FIFO), as in [`mkfifo(3)`]. /// /// [`mkfifo(3)`]: http://man7.org/linux/man-pages/man3/mkfifo.3.html Fifo(Permissions), /// Character device, as in [`mknod(2)`] with `S_IFCHR`. /// /// [`mknod(2)`]: http://man7.org/linux/man-pages/man2/mknod.2.html CharacterDevice(Permissions, rustix_fs::Dev), /// Block device, as in [`mknod(2)`] with `S_IFBLK`. /// /// [`mknod(2)`]: http://man7.org/linux/man-pages/man2/mknod.2.html BlockDevice(Permissions, rustix_fs::Dev), // XXX: Does this really make sense? //// "Detached" unix socket, as in [`mknod(2)`] with `S_IFSOCK`. //// //// [`mknod(2)`]: http://man7.org/linux/man-pages/man2/mknod.2.html //DetachedSocket(), } /// The inode type for [`RootRef::remove_inode`]. This only used internally /// within libpathrs. #[derive(Clone, Copy, Debug)] enum RemoveInodeType { Regular, // ~AT_REMOVEDIR Directory, // AT_REMOVEDIR } /// A handle to the root of a directory tree. /// /// # Safety /// /// At the time of writing, it is considered a **very bad idea** to open a /// [`Root`] inside a possibly-attacker-controlled directory tree. While we do /// have protections that should defend against it (for both drivers), it's far /// more dangerous than just opening a directory tree which is not inside a /// potentially-untrusted directory. /// /// # Errors /// /// If at any point an attack is detected during the execution of a [`Root`] /// method, an error will be returned. The method of attack detection is /// multi-layered and operates through explicit `/proc/self/fd` checks as well /// as (in the case of the native backend) kernel-space checks that will trigger /// `-EXDEV` in certain attack scenarios. /// /// Additionally, if this root directory is moved then any subsequent operations /// will fail with a `SafetyViolation` error since it's not obvious /// whether there is an attacker or if the path was moved innocently. This /// restriction might be relaxed in the future. // TODO: Fix the SafetyViolation link once we expose ErrorKind. #[derive(Debug)] pub struct Root { /// The underlying `O_PATH` [`OwnedFd`] for this root handle. inner: OwnedFd, /// The underlying [`Resolver`] to use for all operations underneath this /// root. This affects not just [`resolve`] but also all other methods which /// have to implicitly resolve a path underneath [`Root`]. /// /// [`resolve`]: Self::resolve resolver: Resolver, } impl Root { /// Open a [`Root`] handle. /// /// The resolver backend used by this handle is chosen at runtime based on /// which resolvers are supported by the running kernel. /// /// # Errors /// /// `path` must be an existing directory, and must (at the moment) be a /// fully-resolved pathname with no symlink components. This restriction /// might be relaxed in the future. #[doc(alias = "pathrs_open_root")] pub fn open(path: impl AsRef) -> Result { let file = syscalls::openat( syscalls::AT_FDCWD, path, OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, 0, ) .map_err(|err| ErrorImpl::RawOsError { operation: "open root handle".into(), source: err, })?; Ok(Self::from_fd(file)) } /// Wrap an [`OwnedFd`] into a [`Root`]. /// /// The [`OwnedFd`] should be a file descriptor referencing a directory, /// otherwise all [`Root`] operations will fail. /// /// The configuration is set to the system default and should be configured /// prior to usage, if appropriate. #[inline] pub fn from_fd(fd: impl Into) -> Self { Self { inner: fd.into(), resolver: Default::default(), } } /// Borrow this [`Root`] as a [`RootRef`]. /// /// The [`ResolverFlags`] of the [`Root`] are inherited by the [`RootRef`] /// byt are not shared ([`Root::set_resolver_flags`] does not affect /// existing [`RootRef`]s or [`RootRef`]s not created using /// [`Root::as_ref`]). // XXX: We can't use Borrow/Deref for this because HandleRef takes a // lifetime rather than being a pure reference. Ideally we would use // Deref but it seems that won't be possible in standard Rust for a // long time, if ever... #[inline] pub fn as_ref(&self) -> RootRef<'_> { RootRef { inner: self.as_fd(), resolver: self.resolver, } } /// Get the current [`ResolverFlags`] for this [`Root`]. #[inline] pub fn resolver_flags(&self) -> ResolverFlags { self.resolver.flags } /// Set the [`ResolverFlags`] for all operations in this [`Root`]. /// /// Note that this only affects this instance of [`Root`]. Neither other /// [`Root`] instances nor existing [`RootRef`] references to this [`Root`] /// will have their [`ResolverFlags`] unchanged. #[inline] pub fn set_resolver_flags(&mut self, flags: ResolverFlags) -> &mut Self { self.resolver.flags = flags; self } /// Set the [`ResolverFlags`] for all operations in this [`Root`]. /// /// This is identical to [`Root::set_resolver_flags`] except that it can /// more easily be used with chaining to configure a [`Root`] in a single /// line: /// /// ```rust /// # use pathrs::{Root, flags::ResolverFlags}; /// # let tmpdir = tempfile::TempDir::new()?; /// # let rootdir = &tmpdir; /// let root = Root::open(rootdir)?.with_resolver_flags(ResolverFlags::NO_SYMLINKS); /// // Continue to use root. /// # let _ = tmpdir; // make sure it is not dropped early /// # Ok::<(), anyhow::Error>(()) /// ``` /// /// If you want to temporarily set flags just for a small set of operations, /// you should use [`Root::as_ref`] to create a temporary [`RootRef`] and /// use [`RootRef::with_resolver_flags`] instead. #[inline] pub fn with_resolver_flags(mut self, flags: ResolverFlags) -> Self { self.set_resolver_flags(flags); self } /// Create a copy of an existing [`Root`]. /// /// The new handle is completely independent from the original, but /// references the same underlying file and has the same configuration. #[inline] pub fn try_clone(&self) -> Result { self.as_ref().try_clone() } /// Within the given [`Root`]'s tree, resolve `path` and return a /// [`Handle`]. /// /// All symlink path components are scoped to [`Root`]. Trailing symlinks /// *are* followed, if you want to get a handle to a symlink use /// [`resolve_nofollow`]. /// /// # Errors /// /// If `path` doesn't exist, or an attack was detected during resolution, a /// corresponding [`Error`] will be returned. If no error is returned, then /// the path is guaranteed to have been reachable from the root of the /// directory tree and thus have been inside the root at one point in the /// resolution. /// /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_resolve")] #[inline] pub fn resolve(&self, path: impl AsRef) -> Result { self.as_ref().resolve(path) } /// Identical to [`resolve`], except that *trailing* symlinks are *not* /// followed. /// /// If the trailing component is a symlink [`resolve_nofollow`] will return /// a handle to the symlink itself. This is effectively equivalent to /// `O_NOFOLLOW`. /// /// [`resolve`]: Self::resolve /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_resolve_nofollow")] #[inline] pub fn resolve_nofollow(&self, path: impl AsRef) -> Result { self.as_ref().resolve_nofollow(path) } /// Open a path without creating an intermediate [`Handle`] object. /// /// This is effectively just shorthand for [`resolve`] followed by /// [`Handle::reopen`]. However, some resolvers (such as the `openat2` /// resolver) can implement [`open_subpath`] slightly more efficiently than /// naively doing a two-step open operation with [`Handle::reopen`]. If you /// wish to create an [`OpenFlags::O_PATH`] file handle, it probably makes /// more sense to use [`resolve`] or [`resolve_nofollow`]. /// /// If `flags` contains [`OpenFlags::O_NOFOLLOW`] and the path refers to a /// symlink then this method will match the behaviour of [`openat2`] (this /// is in contrast to [`Handle::reopen`] which does not permit re-opening a /// handle to a symlink): /// /// * If `flags` also contains [`OpenFlags::O_PATH`] then the returned file /// is equivalent to the [`Handle`] that would've been returned from /// [`resolve_nofollow`]. /// * Otherwise, an error will be returned to match the behaviour of /// [`OpenFlags::O_NOFOLLOW`] when encountering a trailing symlink. /// /// [`openat2`]: https://man7.org/linux/man-pages/man2/openat2.2.html /// [`open_subpath`]: Self::open_subpath /// [`resolve`]: Self::resolve /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_open")] #[inline] pub fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { self.as_ref().open_subpath(path, flags) } /// Get the target of a symlink within a [`Root`]. /// /// **NOTE**: The returned path is not modified to be "safe" outside of the /// root. You should not use this path for doing further path lookups -- use /// [`resolve`] instead. /// /// This method is just shorthand for calling `readlinkat(2)` on the handle /// returned by [`resolve_nofollow`]. /// /// [`resolve`]: Self::resolve /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_readlink")] #[inline] pub fn readlink(&self, path: impl AsRef) -> Result { self.as_ref().readlink(path) } /// Within the [`Root`]'s tree, create an inode at `path` as specified by /// `inode_type`. /// /// # Errors /// /// If the path already exists (regardless of the type of the existing /// inode), an error is returned. #[doc(alias = "pathrs_inroot_mkdir")] #[doc(alias = "pathrs_inroot_mknod")] #[doc(alias = "pathrs_inroot_symlink")] #[doc(alias = "pathrs_inroot_hardlink")] #[inline] pub fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Error> { self.as_ref().create(path, inode_type) } /// Create an [`InodeType::File`] within the [`Root`]'s tree at `path` with /// the mode given by `perm`, and return a [`Handle`] to the newly-created /// file. /// /// However, unlike the trivial way of doing the above: /// /// ```dead_code /// root.create(path, inode_type)?; /// // What happens if the file is replaced here!? /// let handle = root.resolve(path, perm)?; /// ``` /// /// [`create_file`] guarantees that the returned [`Handle`] is the same as /// the file created by the operation. This is only possible to guarantee /// for ordinary files because there is no [`O_CREAT`]-equivalent for other /// inode types. /// /// # Errors /// /// Identical to [`create`]. /// /// [`create`]: Self::create /// [`create_file`]: Self::create_file /// [`O_CREAT`]: http://man7.org/linux/man-pages/man2/open.2.html #[doc(alias = "pathrs_inroot_creat")] #[doc(alias = "pathrs_inroot_create")] #[inline] pub fn create_file( &self, path: impl AsRef, flags: impl Into, perm: &Permissions, ) -> Result { self.as_ref().create_file(path, flags, perm) } /// Within the [`Root`]'s tree, create a directory and any of its parent /// component if they are missing. This is effectively equivalent to /// [`std::fs::create_dir_all`], Go's [`os.MkdirAll`], or Unix's `mkdir -p`. /// /// The provided set of [`Permissions`] only applies to path components /// created by this function, existing components will not have their /// permissions modified. In addition, if the provided path already exists /// and is a directory, this function will return successfully. /// /// The returned [`Handle`] is an `O_DIRECTORY` handle referencing the /// created directory (due to kernel limitations, we cannot guarantee that /// the handle is the exact directory created and not a similar-looking /// directory that was swapped in by an attacker, but we do as much /// validation as possible to make sure the directory is functionally /// identical to the directory we would've created). /// /// # Errors /// /// This method will return an error if any of the path components in the /// provided path were invalid (non-directory components or dangling symlink /// components) or if certain exchange attacks were detected. /// /// If an error occurs, it is possible for any number of the directories in /// `path` to have been created despite this method returning an error. /// /// [`os.MkdirAll`]: https://pkg.go.dev/os#MkdirAll #[doc(alias = "pathrs_inroot_mkdir_all")] #[inline] pub fn mkdir_all(&self, path: impl AsRef, perm: &Permissions) -> Result { self.as_ref().mkdir_all(path, perm) } /// Within the [`Root`]'s tree, remove the empty directory at `path`. /// /// Any existing [`Handle`]s to `path` will continue to work as before, /// since Linux does not invalidate file handles to unlinked files (though, /// directory handling is not as simple). /// /// # Errors /// /// If the path does not exist, was not actually a directory, or was a /// non-empty directory an error will be returned. In order to remove a /// directory and all of its children, you can use [`remove_all`]. /// /// [`remove_all`]: Self::remove_all #[doc(alias = "pathrs_inroot_rmdir")] #[inline] pub fn remove_dir(&self, path: impl AsRef) -> Result<(), Error> { self.as_ref().remove_dir(path) } /// Within the [`Root`]'s tree, remove the file (any non-directory inode) at /// `path`. /// /// Any existing [`Handle`]s to `path` will continue to work as before, /// since Linux does not invalidate file handles to unlinked files (though, /// directory handling is not as simple). /// /// # Errors /// /// If the path does not exist or was actually a directory an error will be /// returned. In order to remove a path regardless of its type (even if it /// is a non-empty directory), you can use [`remove_all`]. /// /// [`remove_all`]: Self::remove_all #[doc(alias = "pathrs_inroot_unlink")] #[inline] pub fn remove_file(&self, path: impl AsRef) -> Result<(), Error> { self.as_ref().remove_file(path) } /// Within the [`Root`]'s tree, recursively delete the provided `path` and /// any children it contains if it is a directory. This is effectively /// equivalent to [`std::fs::remove_dir_all`], Go's [`os.RemoveAll`], or /// Unix's `rm -r`. /// /// Any existing [`Handle`]s to paths within `path` will continue to work as /// before, since Linux does not invalidate file handles to unlinked files /// (though, directory handling is not as simple). /// /// # Errors /// /// If the path does not exist or some other error occurred during the /// deletion process an error will be returned. /// /// [`os.RemoveAll`]: https://pkg.go.dev/os#RemoveAll #[doc(alias = "pathrs_inroot_remove_all")] #[inline] pub fn remove_all(&self, path: impl AsRef) -> Result<(), Error> { self.as_ref().remove_all(path) } /// Within the [`Root`]'s tree, perform a rename with the given `source` and /// `directory`. The `flags` argument is passed directly to /// [`renameat2(2)`]. /// /// # Errors /// /// The error rules are identical to [`renameat2(2)`]. /// /// [`renameat2(2)`]: http://man7.org/linux/man-pages/man2/renameat2.2.html #[doc(alias = "pathrs_inroot_rename")] pub fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), Error> { self.as_ref().rename(source, destination, rflags) } } impl From for Root { /// Shorthand for [`Root::from_fd`]. fn from(fd: OwnedFd) -> Self { Self::from_fd(fd) } } impl From for OwnedFd { /// Unwrap a [`Root`] to reveal the underlying [`OwnedFd`]. /// /// **Note**: This method is primarily intended to allow for file descriptor /// passing or otherwise transmitting file descriptor information. It is not /// safe to use this [`OwnedFd`] directly to do filesystem operations. /// Please use the provided [`Root`] methods. fn from(root: Root) -> Self { root.inner } } impl AsFd for Root { /// Access the underlying file descriptor for a [`Root`]. /// /// **Note**: This method is primarily intended to allow for tests and other /// code to check the status of the underlying [`OwnedFd`] without having to /// use [`OwnedFd::from`]. It is not safe to use this [`BorrowedFd`] /// directly to do filesystem operations. Please use the provided [`Root`] /// methods. #[inline] fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } /// Borrowed version of [`Root`]. /// /// Unlike [`Root`], when [`RootRef`] is dropped the underlying file descriptor /// is *not* closed. This is mainly useful for programs and libraries that have /// to do operations on [`&File`][File]s and [`BorrowedFd`]s passed from /// elsewhere. /// /// [File]: std::fs::File // TODO: Is there any way we can restructure this to use Deref so that we don't // need to copy all of the methods into Handle? Probably not... Maybe GATs // will eventually support this but we'd still need a GAT-friendly Deref. #[derive(Copy, Clone, Debug)] pub struct RootRef<'fd> { inner: BorrowedFd<'fd>, // TODO: Drop this and switch to builder-pattern. resolver: Resolver, } impl RootRef<'_> { /// Wrap a [`BorrowedFd`] into a [`RootRef`]. /// /// The [`BorrowedFd`] should be a file descriptor referencing a directory, /// otherwise all [`Root`] operations will fail. /// /// The configuration is set to the system default and should be configured /// prior to usage, if appropriate. pub fn from_fd(inner: BorrowedFd<'_>) -> RootRef<'_> { RootRef { inner, resolver: Default::default(), } } /// Get the current [`ResolverFlags`] for this [`RootRef`]. #[inline] pub fn resolver_flags(&self) -> ResolverFlags { self.resolver.flags } /// Set the [`ResolverFlags`] for all operations in this [`RootRef`]. /// /// Note that this only affects this instance of [`RootRef`]. Neither the /// original [`Root`] nor any other [`RootRef`] references to the same /// underlying [`Root`] will have their [`ResolverFlags`] unchanged. /// /// [`RootRef::clone`] also copies the current [`ResolverFlags`] of the /// [`RootRef`]. #[inline] pub fn set_resolver_flags(&mut self, flags: ResolverFlags) -> &mut Self { self.resolver.flags = flags; self } /// Set the [`ResolverFlags`] for all operations in this [`RootRef`]. /// /// This is identical to [`RootRef::set_resolver_flags`] except that it can /// more easily be used with chaining to configure a [`RootRef`] in a single /// line: /// /// ```rust /// # use std::{ /// # fs::{Permissions, File}, /// # os::unix::{ /// # fs::PermissionsExt, /// # io::{AsFd, OwnedFd}, /// # }, /// # }; /// # use pathrs::{RootRef, flags::ResolverFlags, InodeType}; /// # let tmpdir = tempfile::TempDir::new()?; /// # let rootdir = &tmpdir; /// let fd: OwnedFd = File::open(rootdir)?.into(); /// let root = RootRef::from_fd(fd.as_fd()) /// .with_resolver_flags(ResolverFlags::NO_SYMLINKS); /// /// // Continue to use RootRef. /// # let perm = Permissions::from_mode(0o755); /// root.mkdir_all("foo/bar/baz", &perm)?; /// root.create("one", &InodeType::Directory(perm))?; /// root.remove_all("foo")?; /// # let _ = tmpdir; // make sure it is not dropped early /// # Ok::<(), anyhow::Error>(()) /// ``` /// /// The other primary usecase for [`RootRef::with_resolver_flags`] is with /// [`Root::as_ref`] to temporarily set some flags for a single one-line or /// a few operations without modifying the original [`Root`] resolver flags: /// /// ```rust /// # use std::{fs::Permissions, os::unix::fs::PermissionsExt}; /// # use pathrs::{Root, flags::ResolverFlags, InodeType}; /// # let tmpdir = tempfile::TempDir::new()?; /// # let rootdir = &tmpdir; /// let root = Root::open(rootdir)?; /// # let perm = Permissions::from_mode(0o755); /// /// // Apply ResolverFlags::NO_SYMLINKS for a single operation. /// root.as_ref() /// .with_resolver_flags(ResolverFlags::NO_SYMLINKS) /// .mkdir_all("foo/bar/baz", &perm)?; /// /// // Create a temporary RootRef to do multiple operations. /// let root2 = root /// .as_ref() /// .with_resolver_flags(ResolverFlags::NO_SYMLINKS); /// root2.create("one", &InodeType::Directory(perm))?; /// root2.remove_all("foo")?; /// # let _ = tmpdir; // make sure it is not dropped early /// # Ok::<(), anyhow::Error>(()) /// ``` #[inline] pub fn with_resolver_flags(mut self, flags: ResolverFlags) -> Self { self.set_resolver_flags(flags); self } /// Create a copy of a [`RootRef`]. /// /// Note that (unlike [`BorrowedFd::clone`]) this method creates a full copy /// of the underlying file descriptor and thus is more equivalent to /// [`BorrowedFd::try_clone_to_owned`]. /// /// To create a shallow copy of a [`RootRef`], you can use [`Clone::clone`] /// (or just [`Copy`]). // TODO: We might need to call this something other than try_clone(), since // it's a little too easy to confuse with Clone::clone() but we also // really want to have Copy. pub fn try_clone(&self) -> Result { Ok(Root { inner: self .as_fd() .try_clone_to_owned() .map_err(|err| ErrorImpl::OsError { operation: "clone underlying root file".into(), source: err, })?, resolver: self.resolver, }) } /// Within the given [`RootRef`]'s tree, resolve `path` and return a /// [`Handle`]. /// /// All symlink path components are scoped to [`RootRef`]. Trailing symlinks /// *are* followed, if you want to get a handle to a symlink use /// [`resolve_nofollow`]. /// /// # Errors /// /// If `path` doesn't exist, or an attack was detected during resolution, a /// corresponding [`Error`] will be returned. If no error is returned, then /// the path is guaranteed to have been reachable from the root of the /// directory tree and thus have been inside the root at one point in the /// resolution. /// /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_resolve")] #[inline] pub fn resolve(&self, path: impl AsRef) -> Result { self.resolver.resolve(self, path, false) } /// Identical to [`resolve`], except that *trailing* symlinks are *not* /// followed. /// /// If the trailing component is a symlink [`resolve_nofollow`] will return /// a handle to the symlink itself. This is effectively equivalent to /// `O_NOFOLLOW`. /// /// [`resolve`]: Self::resolve /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_resolve_nofollow")] #[inline] pub fn resolve_nofollow(&self, path: impl AsRef) -> Result { self.resolver.resolve(self, path, true) } /// Open a path without creating an intermediate [`Handle`] object. /// /// This is effectively just shorthand for [`resolve`] followed by /// [`Handle::reopen`]. However, some resolvers (such as the `openat2` /// resolver) can implement [`open_subpath`] slightly more efficiently than /// naively doing a two-step open operation with [`Handle::reopen`]. If you /// wish to create an [`OpenFlags::O_PATH`] file handle, it probably makes /// more sense to use [`resolve`] or [`resolve_nofollow`]. /// /// If `flags` contains [`OpenFlags::O_NOFOLLOW`] and the path refers to a /// symlink then this method will match the behaviour of [`openat2`] (this /// is in contrast to [`Handle::reopen`] which does not permit re-opening a /// handle to a symlink): /// /// * If `flags` also contains [`OpenFlags::O_PATH`] then the returned file /// is equivalent to the [`Handle`] that would've been returned from /// [`resolve_nofollow`]. /// * Otherwise, an error will be returned to match the behaviour of /// [`OpenFlags::O_NOFOLLOW`] when encountering a trailing symlink. /// /// [`openat2`]: https://man7.org/linux/man-pages/man2/openat2.2.html /// [`open_subpath`]: Self::open_subpath /// [`resolve`]: Self::resolve /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_open")] #[inline] pub fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { self.resolver.open(self, path, flags) } /// Resolve the parent of a given path. This is used internally when /// implementing operations that operate on the parent directory, such as /// operations that: /// /// * Create new inodes (i.e. the target path doesn't already exist). /// * Are implemented with syscalls that do not allow us to use /// `AT_EMPTY_PATH` or an equivalent (such as [`rename`]) even though the /// path exists. /// /// The returned tuple is `(dir, name, trailing_slash)` (i.e. a handle to /// *parent* directory, basename of the path, and a `bool` indicating /// whether the original path had a trailing slash). It is up to the caller /// to decide what the correct handling of `trailing_slash` is (but if it is /// ignored by the caller then trailing slashes will be ignored wholesale). /// /// If you expect the target path to exist already, then /// [`resolve_exists_parent`] may be more suitable. /// /// [`rename`]: Self::rename /// [`resolve_exists_parent`]: Self::resolve_exists_parent fn resolve_parent<'p>( &self, path: &'p Path, ) -> Result<(OwnedFd, Option<&'p Path>, bool), Error> { let (path, trailing_slash) = utils::path_strip_trailing_slash(path); let (parent, name) = utils::path_split(path).wrap("split path into (parent, name)")?; // All users of resolve_parent require the basename to be an actual // pathname (as opposed to being empty or a special name like "." or // ".."), so return an error here if that is the path we got. let name = match name.map(|p| (p, p.as_os_str().as_bytes())) { // We stripped any trailing slashes, so there cannot be an empty // basename. None | Some((_, b"." | b"..")) => { return Ok(( self.inner .try_clone_to_owned() .map_err(|err| ErrorImpl::OsError { operation: "clone root".into(), source: err, })?, None, trailing_slash, )); } Some((name, _)) => name, }; let dir = self .resolve(parent) .wrap("resolve parent directory")? .into(); Ok((dir, Some(name), trailing_slash)) } /// Equivalent to [`resolve_parent`], except that we assume that the target /// exists and so if `path` has a trailing slash we verify that the target /// path is actually a directory. /// /// If you need more complicated handling, use [`resolve_parent`]. /// /// [`resolve_parent`]: Self::resolve_parent fn resolve_exists_parent<'p>( &self, path: &'p Path, ) -> Result<(OwnedFd, Option<&'p Path>), Error> { let (dir, name, trailing_slash) = self.resolve_parent(path)?; if trailing_slash { let stat = syscalls::fstatat(&dir, name.unwrap_or(Path::new(""))).map_err(|err| { ErrorImpl::RawOsError { operation: "check trailing slash path is a directory".into(), source: err, } })?; if !FileType::from_raw_mode(stat.st_mode).is_dir() { Err(ErrorImpl::OsError { operation: "verify trailing slash path".into(), source: IOError::from_raw_os_error(libc::ENOTDIR), })?; } } Ok((dir, name)) } /// Get the target of a symlink within a [`RootRef`]. /// /// **NOTE**: The returned path is not modified to be "safe" outside of the /// root. You should not use this path for doing further path lookups -- use /// [`resolve`] instead. /// /// This method is just shorthand for calling `readlinkat(2)` on the handle /// returned by [`resolve_nofollow`]. /// /// [`resolve`]: Self::resolve /// [`resolve_nofollow`]: Self::resolve_nofollow #[doc(alias = "pathrs_inroot_readlink")] pub fn readlink(&self, path: impl AsRef) -> Result { let link = self .resolve_nofollow(path) .wrap("resolve symlink O_NOFOLLOW for readlink")?; syscalls::readlinkat(link, "").map_err(|err| { ErrorImpl::RawOsError { operation: "readlink resolve symlink".into(), source: err, } .into() }) } /// Within the [`RootRef`]'s tree, create an inode at `path` as specified by /// `inode_type`. /// /// # Errors /// /// If the path already exists (regardless of the type of the existing /// inode), an error is returned. #[doc(alias = "pathrs_inroot_mkdir")] #[doc(alias = "pathrs_inroot_mknod")] #[doc(alias = "pathrs_inroot_symlink")] #[doc(alias = "pathrs_inroot_hardlink")] pub fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Error> { // The path doesn't exist yet, so we need to get a safe reference to the // parent and just operate on the final (slashless) component. let (dir, name, trailing_slash) = self .resolve_parent(path.as_ref()) .wrap("resolve file creation path")?; let name = name.ok_or_else(|| ErrorImpl::InvalidArgument { name: "path".into(), description: "file creation path is /".into(), })?; // The trailing slash behaviour depends on what inode type we are making // (to mirror Linux's behaviour). In particular, mkdir("non-exist/") is // legal. if trailing_slash && !matches!(inode_type, InodeType::Directory(_)) { Err(ErrorImpl::InvalidArgument { name: "path".into(), description: "file creation path has trailing slash".into(), })?; } match inode_type { InodeType::File(perm) => { let mode = perm.mode() & !libc::S_IFMT; syscalls::mknodat(dir, name, libc::S_IFREG | mode, 0) } InodeType::Directory(perm) => { let mode = perm.mode() & !libc::S_IFMT; syscalls::mkdirat(dir, name, mode) } InodeType::Symlink(target) => { // No need to touch target. syscalls::symlinkat(target, dir, name) } InodeType::Hardlink(target) => { let (olddir, oldname, trailing_slash) = self .resolve_parent(target) .wrap("resolve hardlink source path")?; let oldname = oldname.ok_or_else(|| ErrorImpl::InvalidArgument { name: "target".into(), description: "hardlink source path is /".into(), })?; // Directories cannot be a hardlink target, so indiscriminately // block trailing slashes for the target path. if trailing_slash { Err(ErrorImpl::InvalidArgument { name: "target".into(), description: "hardlink target path has trailing slash".into(), })?; } syscalls::linkat(olddir, oldname, dir, name, AtFlags::empty()) } InodeType::Fifo(perm) => { let mode = perm.mode() & !libc::S_IFMT; syscalls::mknodat(dir, name, libc::S_IFIFO | mode, 0) } InodeType::CharacterDevice(perm, dev) => { let mode = perm.mode() & !libc::S_IFMT; syscalls::mknodat(dir, name, libc::S_IFCHR | mode, *dev) } InodeType::BlockDevice(perm, dev) => { let mode = perm.mode() & !libc::S_IFMT; syscalls::mknodat(dir, name, libc::S_IFBLK | mode, *dev) } } .map_err(|err| { ErrorImpl::RawOsError { operation: "pathrs create".into(), source: err, } .into() }) } /// Create an [`InodeType::File`] within the [`RootRef`]'s tree at `path` /// with the mode given by `perm`, and return a [`Handle`] to the /// newly-created file. /// /// However, unlike the trivial way of doing the above: /// /// ```dead_code /// root.create(path, inode_type)?; /// // What happens if the file is replaced here!? /// let handle = root.resolve(path, perm)?; /// ``` /// /// [`create_file`] guarantees that the returned [`Handle`] is the same as /// the file created by the operation. This is only possible to guarantee /// for ordinary files because there is no [`O_CREAT`]-equivalent for other /// inode types. /// /// # Errors /// /// Identical to [`create`]. /// /// [`create`]: Self::create /// [`create_file`]: Self::create_file /// [`O_CREAT`]: http://man7.org/linux/man-pages/man2/open.2.html #[doc(alias = "pathrs_inroot_creat")] #[doc(alias = "pathrs_inroot_create")] pub fn create_file( &self, path: impl AsRef, flags: impl Into, perm: &Permissions, ) -> Result { let mut flags = flags.into(); // O_TMPFILE needs special handling -- the pathname is not the name of a // new file (thus requiring us to resolve the parent). Instead, it names // a directory on a filesystem where an unnamed temporary file should be // created. if flags.contains(OpenFlags::O_TMPFILE) { // TODO: Ideally we could just do this with open_subpath() or // resolver.open() but they don't take Permissions... let dir = self.resolve(path.as_ref())?; return syscalls::openat(dir, ".", flags, perm.mode()) .map_err(|err| { { ErrorImpl::RawOsError { operation: "pathrs create tmpfile".into(), source: err, } } .into() }) .map(Into::into); } // The path doesn't exist yet, so we need to get a safe reference to the // parent and just operate on the final (slashless) component. let (dir, name, trailing_slash) = self .resolve_parent(path.as_ref()) .wrap("resolve O_CREAT file creation path")?; let name = name.ok_or_else(|| ErrorImpl::InvalidArgument { name: "path".into(), description: "file creation path is /".into(), })?; // For obvious reasons, we cannot O_CREAT a directory. if trailing_slash { Err(ErrorImpl::InvalidArgument { name: "path".into(), description: "O_CREAT file creation path has trailing slash".into(), })?; } // XXX: openat2(2) supports doing O_CREAT on trailing symlinks without // O_NOFOLLOW. We might want to expose that here, though because it // can't be done with the emulated backend that might be a bad idea. flags.insert(OpenFlags::O_CREAT); let fd = syscalls::openat(dir, name, flags, perm.mode()).map_err(|err| { ErrorImpl::RawOsError { operation: "pathrs create_file".into(), source: err, } })?; Ok(fd.into()) } /// Within the [`RootRef`]'s tree, create a directory and any of its parent /// component if they are missing. /// /// This is effectively equivalent to [`std::fs::create_dir_all`], Go's /// [`os.MkdirAll`], or Unix's `mkdir -p`. /// /// The provided set of [`Permissions`] only applies to path components /// created by this function, existing components will not have their /// permissions modified. In addition, if the provided path already exists /// and is a directory, this function will return successfully. /// /// The returned [`Handle`] is an `O_DIRECTORY` handle referencing the /// created directory (due to kernel limitations, we cannot guarantee that /// the handle is the exact directory created and not a similar-looking /// directory that was swapped in by an attacker, but we do as much /// validation as possible to make sure the directory is functionally /// identical to the directory we would've created). /// /// # Errors /// /// This method will return an error if any of the path components in the /// provided path were invalid (non-directory components or dangling symlink /// components) or if certain exchange attacks were detected. /// /// If an error occurs, it is possible for any number of the directories in /// `path` to have been created despite this method returning an error. /// /// [`os.MkdirAll`]: https://pkg.go.dev/os#MkdirAll #[doc(alias = "pathrs_inroot_mkdir_all")] pub fn mkdir_all(&self, path: impl AsRef, perm: &Permissions) -> Result { if perm.mode() & !0o7777 != 0 { Err(ErrorImpl::InvalidArgument { name: "perm".into(), description: "mode cannot contain non-0o7777 bits".into(), })? } // Linux silently ignores S_IS[UG]ID if passed to mkdirat(2), and a lot // of libraries just ignore these flags. However, ignoring them as a new // library seems less than ideal -- users shouldn't set flags that are // no-ops because they might not notice they are no-ops. if perm.mode() & !0o1777 != 0 { Err(ErrorImpl::InvalidArgument { name: "perm".into(), description: "mode contains setuid or setgid bits that are silently ignored by mkdirat" .into(), })? } let (handle, remaining) = self .resolver .resolve_partial(self, path.as_ref(), false) .and_then(TryInto::try_into)?; // Re-open the handle with O_DIRECTORY to make sure it's a directory we // can use as well as to make sure we return an O_DIRECTORY regardless // of whether there are any remaining components (for consistency). let mut current = handle .reopen(OpenFlags::O_DIRECTORY) .with_wrap(|| format!("cannot re-open {} with O_DIRECTORY", FrozenFd::from(handle)))?; // For the remaining let remaining_parts = remaining .iter() .flat_map(PathIterExt::raw_components) .map(|p| p.to_os_string()) // Skip over no-op entries. .filter(|part| !part.is_empty() && part.as_bytes() != b".") .collect::>(); // If the path contained ".." components after the end of the "real" // components, we simply error out. We could try to safely resolve ".." // here but that would add a bunch of extra logic for something that // it's not clear even needs to be supported. // // We also can't just do something like filepath.Clean(), because ".." // could erase dangling symlinks and produce a path that doesn't match // what the user asked for. if remaining_parts.iter().any(|part| part.as_bytes() == b"..") { Err(ErrorImpl::OsError { operation: "mkdir_all remaining components".into(), source: IOError::from_raw_os_error(libc::ENOENT), }) .with_wrap(|| { format!("yet-to-be-created path {remaining:?} contains '..' components") })? } // For the remaining components, create a each component one-by-one. for part in remaining_parts { if part.as_bytes().contains(&b'/') { Err(ErrorImpl::SafetyViolation { description: "remaining component for mkdir contains '/'".into(), })?; } // Try to create the component first, to reduce the risk of races // where the inode gets created between the openat() check and then // the fallback mkdirat(). An attacker could delete the inode, of // course, but we can just error out in that case. // // mkdirat(2) does not follow trailing symlinks (even if it is a // dangling symlink with only a trailing component missing), so we // can safely create the final component without worrying about // symlink-exchange attacks. if let Err(err) = syscalls::mkdirat(¤t, &part, perm.mode()) { // If we got EEXIST then either the directory existed before or // a racing Root::mkdir_all created the directory before us. We // can safely continue because the following openat() will only // succeed if it is a directory at open()-time (and not another // inode type an attacker might've swapped in). if err.errno() != Errno::EXIST { Err(ErrorImpl::RawOsError { operation: "create next directory component".into(), source: err, })?; } } // Get a handle to the directory we just created. Unfortunately we // can't do an atomic create+open (a-la O_CREAT) with mkdirat(), so // a separate O_DIRECTORY|O_NOFOLLOW is the best we can do. let next = syscalls::openat( ¤t, &part, OpenFlags::O_NOFOLLOW | OpenFlags::O_DIRECTORY, 0, ) .map_err(|err| ErrorImpl::RawOsError { operation: "open newly created directory".into(), source: err, })?; // Unfortunately, we cannot create a directory and open it // atomically (a-la O_CREAT). This means an attacker could swap our // newly created directory with one they have. Ideally we would // verify that the directory "looks right" by checking the owner, // mode, and whether it is empty. // // However, it turns out that trying to do this correctly is more // complicated than you might expect. Basic Unix DACs are mostly // trivial to emulate, but POSIX ACLs and filesystem-specific mount // options can affect ownership and modes in unexpected ways, // resulting in spurious errors. In addition, some pseudofilesystems // (like cgroupfs) create non-empty directories so requiring new // directories be empty would result in spurious errors. // // Ultimately, the semantics of Root::mkdir_all() permit reusing an // existing directory that an attacker created beforehand, so // verifying that directories we create weren't swapped really // doesn't seem to provide any practical benefit. // Keep walking. current = next.into(); } Ok(Handle::from_fd(current)) } /// Within the [`RootRef`]'s tree, remove the inode of type `inode_type` at /// `path`. /// /// Any existing [`Handle`]s to `path` will continue to work as before, /// since Linux does not invalidate file handles to unlinked files (though, /// directory handling is not as simple). /// /// # Errors /// /// If the path does not exist, was not actually `inode_type`, or was a /// non-empty directory an error will be returned. In order to remove a path /// regardless of whether it exists, its type, or if it it's a non-empty /// directory, you can use [`remove_all`]. /// /// [`remove_all`]: Self::remove_all fn remove_inode(&self, path: &Path, inode_type: RemoveInodeType) -> Result<(), Error> { // unlinkat(2) doesn't let us remove an inode using just a handle (for // obvious reasons -- on Unix hardlinks mean that "unlink this file" // doesn't make sense without referring to a specific directory entry). let (dir, name, trailing_slash) = self .resolve_parent(path.as_ref()) .wrap("resolve file removal path")?; let name = name.ok_or_else(|| ErrorImpl::InvalidArgument { name: "path".into(), description: "file removal path is /".into(), })?; let flags = match inode_type { RemoveInodeType::Regular => { if trailing_slash { Err(ErrorImpl::OsError { operation: "file removal path cannot have trailing slash".into(), source: IOError::from_raw_os_error(libc::ENOTDIR), })?; } AtFlags::empty() } RemoveInodeType::Directory => AtFlags::REMOVEDIR, }; syscalls::unlinkat(dir, name, flags).map_err(|err| { ErrorImpl::RawOsError { operation: "pathrs remove".into(), source: err, } .into() }) } /// Within the [`RootRef`]'s tree, remove the empty directory at `path`. /// /// Any existing [`Handle`]s to `path` will continue to work as before, /// since Linux does not invalidate file handles to unlinked files (though, /// directory handling is not as simple). /// /// # Errors /// /// If the path does not exist, was not actually a directory, or was a /// non-empty directory an error will be returned. In order to remove a /// directory and all of its children, you can use [`remove_all`]. /// /// [`remove_all`]: Self::remove_all #[doc(alias = "pathrs_inroot_rmdir")] #[inline] pub fn remove_dir(&self, path: impl AsRef) -> Result<(), Error> { self.remove_inode(path.as_ref(), RemoveInodeType::Directory) } /// Within the [`RootRef`]'s tree, remove the file (any non-directory inode) /// at `path`. /// /// Any existing [`Handle`]s to `path` will continue to work as before, /// since Linux does not invalidate file handles to unlinked files (though, /// directory handling is not as simple). /// /// # Errors /// /// If the path does not exist or was actually a directory an error will be /// returned. In order to remove a path regardless of its type (even if it /// is a non-empty directory), you can use [`remove_all`]. /// /// [`remove_all`]: Self::remove_all #[doc(alias = "pathrs_inroot_unlink")] #[inline] pub fn remove_file(&self, path: impl AsRef) -> Result<(), Error> { self.remove_inode(path.as_ref(), RemoveInodeType::Regular) } /// Within the [`RootRef`]'s tree, recursively delete the provided `path` /// and any children it contains if it is a directory. This is effectively /// equivalent to [`std::fs::remove_dir_all`], Go's [`os.RemoveAll`], or /// Unix's `rm -r`. /// /// Any existing [`Handle`]s to paths within `path` will continue to work as /// before, since Linux does not invalidate file handles to unlinked files /// (though, directory handling is not as simple). /// /// # Errors /// /// If the path does not exist or some other error occurred during the /// deletion process an error will be returned. /// /// [`os.RemoveAll`]: https://pkg.go.dev/os#RemoveAll #[doc(alias = "pathrs_inroot_remove_all")] pub fn remove_all(&self, path: impl AsRef) -> Result<(), Error> { // Ignore trailing slashes -- we want to support handling trailing // slashes for directories, but adding resolve_exists_parent-like // verification logic to remove_all() is a bit much. So we just ignore // trailing slashes entirely. // // For what it's worth, this matches the behaviour of Go's os.RemoveAll // and GNU's "rm -rf". let (dir, name, _) = self .resolve_parent(path.as_ref()) .wrap("resolve remove-all path")?; // TODO: We can probably support removing "/" or "." in the future by // simply removing all children and not removing the top-level // directory. let name = name.ok_or_else(|| ErrorImpl::InvalidArgument { name: "path".into(), description: "remove all path is /".into(), })?; utils::remove_all(&dir, name) } /// Within the [`RootRef`]'s tree, perform a rename with the given `source` /// and `directory`. The `flags` argument is passed directly to /// [`renameat2(2)`]. /// /// # Errors /// /// The error rules are identical to [`renameat2(2)`]. /// /// [`renameat2(2)`]: http://man7.org/linux/man-pages/man2/renameat2.2.html #[doc(alias = "pathrs_inroot_rename")] pub fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), Error> { // renameat2(2) doesn't let us rename paths using AT_EMPTY_PATH handles. // Note that the source must always exist, and we do want to verify the // trailing path is correct. let (src_dir, src_name) = self .resolve_exists_parent(source.as_ref()) .wrap("resolve rename source path")?; let src_name = src_name.ok_or_else(|| ErrorImpl::InvalidArgument { name: "source".into(), description: "rename source path is /".into(), })?; // However, target path handling is unfortunately a little more // complicated. Ideally we want to match the native trailing-slash // behaviour of renameat2(2) to avoid confusion. let (dst_dir, dst_name) = if rflags.contains(RenameFlags::RENAME_EXCHANGE) { // For RENAME_EXCHANGE, the target simply must exist and can be a // different type from the source. Trailing slashes are only allowed // if the target path is a directory. self.resolve_exists_parent(destination.as_ref()) } else { // For all other renames, trailing slashes on the *target* path are // only allowed if the *source* path is a directory. (Also, if the // target path exists, it must be a directory but this is implicitly // handled by renameat2(2) and so we don't need to check it here.) self.resolve_parent(destination.as_ref()) .and_then(|(dir, name, trailing_slash)| { if trailing_slash { let src_stat = syscalls::fstatat(&src_dir, src_name).map_err(|err| { ErrorImpl::RawOsError { operation: "check rename source path is a directory".into(), source: err, } })?; if !FileType::from_raw_mode(src_stat.st_mode).is_dir() { Err(ErrorImpl::OsError { operation: "destination path has trailing slash but source is not a directory".into(), source: IOError::from_raw_os_error(libc::ENOTDIR), })?; } } Ok((dir, name)) }) } .wrap("resolve rename destination path")?; let dst_name = dst_name.ok_or_else(|| ErrorImpl::InvalidArgument { name: "destination".into(), description: "rename destination path is /".into(), })?; syscalls::renameat2(src_dir, src_name, dst_dir, dst_name, rflags).map_err(|err| { ErrorImpl::RawOsError { operation: "pathrs rename".into(), source: err, } .into() }) } } impl<'fd> From> for RootRef<'fd> { /// Shorthand for [`RootRef::from_fd`]. fn from(fd: BorrowedFd<'fd>) -> Self { Self::from_fd(fd) } } impl AsFd for RootRef<'_> { /// Access the underlying file descriptor for a [`RootRef`]. /// /// **Note**: This method is primarily intended to allow for tests and other /// code to check the status of the underlying file descriptor. It is not /// safe to use this [`BorrowedFd`] directly to do filesystem operations. /// Please use the provided [`RootRef`] methods. #[inline] fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } #[cfg(test)] mod tests { use crate::{resolvers::ResolverBackend, Root, RootRef}; use std::os::unix::io::{AsFd, AsRawFd, OwnedFd}; use anyhow::Error; use pretty_assertions::assert_eq; impl Root { // TODO: Should we make this public? Is there any real benefit? #[inline] pub(crate) fn resolver_backend(&self) -> ResolverBackend { self.resolver.backend } // TODO: Should we make this public? Is there any real benefit? #[inline] pub(crate) fn set_resolver_backend(&mut self, backend: ResolverBackend) -> &mut Self { self.resolver.backend = backend; self } // TODO: Should we make this public? Is there any real benefit? #[inline] pub(crate) fn with_resolver_backend(mut self, backend: ResolverBackend) -> Self { self.set_resolver_backend(backend); self } } impl RootRef<'_> { // TODO: Should we make this public? Is there any real benefit? #[inline] pub(crate) fn resolver_backend(&self) -> ResolverBackend { self.resolver.backend } // TODO: Should we make this public? Is there any real benefit? #[inline] pub(crate) fn set_resolver_backend(&mut self, backend: ResolverBackend) -> &mut Self { self.resolver.backend = backend; self } // TODO: Should we make this public? Is there any real benefit? #[inline] pub(crate) fn with_resolver_backend(mut self, backend: ResolverBackend) -> Self { self.set_resolver_backend(backend); self } } #[test] fn from_fd() -> Result<(), Error> { let root = Root::open(".")?; let root_ref1 = root.as_ref(); let root_ref2 = RootRef::from_fd(root.as_fd()); assert_eq!( root.as_fd().as_raw_fd(), root_ref1.as_fd().as_raw_fd(), "Root::as_ref should have the same underlying fd" ); assert_eq!( root.as_fd().as_raw_fd(), root_ref2.as_fd().as_raw_fd(), "RootRef::from_fd should have the same underlying fd" ); Ok(()) } #[test] fn into_from_ownedfd() -> Result<(), Error> { let root = Root::open(".")?; let root_fd = root.as_fd().as_raw_fd(); let owned: OwnedFd = root.into(); let owned_fd = owned.as_fd().as_raw_fd(); let root2: Root = owned.into(); let root2_fd = root2.as_fd().as_raw_fd(); assert_eq!( root_fd, owned_fd, "OwnedFd::from(root) should have same underlying fd", ); assert_eq!( root_fd, root2_fd, "Root -> OwnedFd -> Root roundtrip should have same underlying fd", ); Ok(()) } } pathrs-0.2.1/src/syscalls.rs000064400000000000000000000672341046102023000141330ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ // We need to permit unsafe code because we are interacting with libc APIs. #![allow(unsafe_code)] use crate::{ flags::{OpenFlags, RenameFlags}, utils::{FdExt, ToCString}, }; use std::{ ffi::OsStr, fmt, io::Error as IOError, mem::MaybeUninit, os::unix::{ ffi::OsStrExt, io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd}, }, path::{Path, PathBuf}, }; use bitflags::bitflags; use once_cell::sync::Lazy; use rustix::{ fs::{ self as rustix_fs, Access, AtFlags, Dev, FileType, Mode, RawMode, Stat, StatFs, Statx, StatxFlags, }, io::Errno, mount::{self as rustix_mount, FsMountFlags, FsOpenFlags, MountAttrFlags, OpenTreeFlags}, process as rustix_process, thread as rustix_thread, }; // TODO: Figure out how we can put a backtrace here (it seems we can't use // thiserror's backtrace support without nightly Rust because thiserror // wants to be able to derive an Error for Backtrace?). We could add a // backtrace to error::Error but if we also add a backtrace to // syscalls::Error this might get a little complicated. // MSRV(1.65): Use std::backtrace::Backtrace. // SAFETY: AT_FDCWD is always a valid file descriptor. pub(crate) const AT_FDCWD: BorrowedFd<'static> = rustix_fs::CWD; // SAFETY: BADFD is not a valid file descriptor, but it's not -1. pub(crate) const BADFD: BorrowedFd<'static> = unsafe { BorrowedFd::borrow_raw(-libc::EBADF) }; /// Representation of a file descriptor and its associated path at a given point /// in time. /// /// This is primarily used to make pretty-printing syscall arguments much nicer, /// and users really shouldn't be interacting with this directly. /// /// # Caveats /// Note that the file descriptor value is very unlikely to reference a live /// file descriptor. Its value is only used for informational purposes. #[derive(Clone, Debug)] pub(crate) struct FrozenFd(RawFd, Option); // TODO: Should probably be a pub(crate) impl. impl From for FrozenFd { fn from(fd: Fd) -> Self { // SAFETY: as_unsafe_path is safe here since it is only used for // pretty-printing error messages and no real logic. Self(fd.as_fd().as_raw_fd(), fd.as_unsafe_path_unchecked().ok()) } } impl fmt::Display for FrozenFd { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { libc::AT_FDCWD => write!(f, "[AT_FDCWD]")?, fd => write!(f, "[{fd}]")?, }; match &self.1 { Some(path) => write!(f, "{path:?}")?, None => write!(f, "")?, }; Ok(()) } } /// Internal error returned by libpathrs's syscall wrappers. /// /// The primary thing of note is that these errors contain detailed debugging /// information about the arguments to each given syscall. Users would most /// often not interact with these error variants directly and instead would make /// use of the top-level [`Error`] type. /// /// [`Error`]: crate::error::Error #[derive(thiserror::Error, Debug)] pub(crate) enum Error { // NOTE: This is temporary until the issue is fixed in rustix. #[error("invalid file descriptor {fd} (see for more details)")] InvalidFd { fd: RawFd, source: Errno }, #[error("accessat({dirfd}, {path:?}, {access:?}, {flags:?})")] Accessat { dirfd: FrozenFd, path: PathBuf, access: Access, flags: AtFlags, source: Errno, }, #[error("openat({dirfd}, {path:?}, {flags:?}, 0o{mode:o})")] Openat { dirfd: FrozenFd, path: PathBuf, flags: OpenFlags, mode: u32, source: Errno, }, #[error("openat2({dirfd}, {path:?}, {how}, {size})")] Openat2 { dirfd: FrozenFd, path: PathBuf, how: OpenHow, size: usize, source: Errno, }, #[error("readlinkat({dirfd}, {path:?})")] Readlinkat { dirfd: FrozenFd, path: PathBuf, source: Errno, }, #[error("mkdirat({dirfd}, {path:?}, 0o{mode:o})")] Mkdirat { dirfd: FrozenFd, path: PathBuf, mode: u32, source: Errno, }, #[error("mknodat({dirfd}, {path:?}, 0o{mode:o}, {major}:{minor})")] Mknodat { dirfd: FrozenFd, path: PathBuf, mode: u32, major: u32, minor: u32, source: Errno, }, #[error("unlinkat({dirfd}, {path:?}, {flags:?})")] Unlinkat { dirfd: FrozenFd, path: PathBuf, flags: AtFlags, source: Errno, }, #[error("linkat({old_dirfd}, {old_path:?}, {new_dirfd}, {new_path:?}, {flags:?})")] Linkat { old_dirfd: FrozenFd, old_path: PathBuf, new_dirfd: FrozenFd, new_path: PathBuf, flags: AtFlags, source: Errno, }, #[error("symlinkat({dirfd}, {path:?}, {target:?})")] Symlinkat { dirfd: FrozenFd, path: PathBuf, target: PathBuf, source: Errno, }, #[error("renameat({old_dirfd}, {old_path:?}, {new_dirfd}, {new_path:?})")] Renameat { old_dirfd: FrozenFd, old_path: PathBuf, new_dirfd: FrozenFd, new_path: PathBuf, source: Errno, }, #[error("renameat2({old_dirfd}, {old_path:?}, {new_dirfd}, {new_path:?}, {flags:?})")] Renameat2 { old_dirfd: FrozenFd, old_path: PathBuf, new_dirfd: FrozenFd, new_path: PathBuf, flags: RenameFlags, source: Errno, }, #[error("fstatfs({fd})")] Fstatfs { fd: FrozenFd, source: Errno }, #[error("fstatat({dirfd}, {path:?}, {flags:?})")] Fstatat { dirfd: FrozenFd, path: PathBuf, flags: AtFlags, source: Errno, }, #[error("statx({dirfd}, {path:?}, flags={flags:?}, mask={mask:?})")] Statx { dirfd: FrozenFd, path: PathBuf, flags: AtFlags, mask: StatxFlags, source: Errno, }, #[error("fsopen({fstype:?}, {flags:?})")] Fsopen { fstype: String, flags: FsOpenFlags, source: Errno, }, #[error("fsconfig({sfd}, FSCONFIG_CMD_CREATE)")] FsconfigCreate { sfd: FrozenFd, source: Errno }, #[error("fsconfig({sfd}, FSCONFIG_SET_STRING, {key:?}, {value:?})")] FsconfigSetString { sfd: FrozenFd, key: String, value: String, source: Errno, }, #[error("fsmount({sfd}, {flags:?}, {mount_attrs:?})")] Fsmount { sfd: FrozenFd, flags: FsMountFlags, mount_attrs: MountAttrFlags, source: Errno, }, #[error("open_tree({dirfd}, {path:?}, {flags:?})")] OpenTree { dirfd: FrozenFd, path: PathBuf, flags: OpenTreeFlags, source: Errno, }, } impl Error { pub(crate) fn errno(&self) -> Errno { // XXX: This should probably be a macro... *match self { Error::InvalidFd { source, .. } => source, Error::Accessat { source, .. } => source, Error::Openat { source, .. } => source, Error::Openat2 { source, .. } => source, Error::Readlinkat { source, .. } => source, Error::Mkdirat { source, .. } => source, Error::Mknodat { source, .. } => source, Error::Unlinkat { source, .. } => source, Error::Linkat { source, .. } => source, Error::Symlinkat { source, .. } => source, Error::Renameat { source, .. } => source, Error::Renameat2 { source, .. } => source, Error::Fstatfs { source, .. } => source, Error::Fstatat { source, .. } => source, Error::Statx { source, .. } => source, Error::Fsopen { source, .. } => source, Error::FsconfigCreate { source, .. } => source, Error::FsconfigSetString { source, .. } => source, Error::Fsmount { source, .. } => source, Error::OpenTree { source, .. } => source, } } // TODO: Switch to returning &Errno. pub(crate) fn root_cause(&self) -> IOError { IOError::from_raw_os_error(self.errno().raw_os_error()) } } /// Rustix will trigger a panic if a [`BorrowedFd`] has a value it deems /// "unacceptable" (namely, most negative values). In rustix 1.0 they added /// support for the `-EBADF` pattern, but a user passing a different negative /// value should not trigger a crash, so we need to add this check to all fd /// operations using rustix. /// /// See for more /// information about the underlying issue. Note that while the issue is closed, /// the resolution was to only accept `-EBADF` -- any other negative values /// (other than `AT_FDCWD`) will still crash the program. trait CheckRustixFd: Sized { fn check_rustix_fd(self) -> Result; } impl CheckRustixFd for Fd { fn check_rustix_fd(self) -> Result { // We can't use BADFD.as_raw_fd() or (-libc::EBADF as _) in a match arm, // instead we need to define a constant that we can then reference. const BADFD: RawFd = -libc::EBADF as _; match self.as_fd().as_raw_fd() { libc::AT_FDCWD | BADFD | 0.. => Ok(self), fd => Err(Error::InvalidFd { fd, source: Errno::BADF, }), } } } /// Wrapper for `faccessat(2)`. pub(crate) fn accessat( dirfd: impl AsFd, path: impl AsRef, access: Access, mut flags: AtFlags, ) -> Result<(), Error> { let (dirfd, path) = (dirfd.as_fd().check_rustix_fd()?, path.as_ref()); flags |= AtFlags::SYMLINK_NOFOLLOW; rustix_fs::accessat(dirfd, path, access, flags).map_err(|errno| Error::Accessat { dirfd: dirfd.into(), path: path.into(), flags, access, source: errno, }) } /// Wrapper for `openat(2)` which auto-sets `O_CLOEXEC | O_NOCTTY`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `openat(2)`. We need the dirfd argument, so we need a wrapper. pub(crate) fn openat_follow( dirfd: impl AsFd, path: impl AsRef, mut flags: OpenFlags, mode: RawMode, // TODO: Should we take rustix::fs::Mode directly? ) -> Result { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); // O_CLOEXEC is needed for obvious reasons, and O_NOCTTY ensures that a // malicious file won't take control of our terminal. flags.insert(OpenFlags::O_CLOEXEC | OpenFlags::O_NOCTTY); rustix_fs::openat(dirfd, path, flags.into(), Mode::from_raw_mode(mode)).map_err(|errno| { Error::Openat { dirfd: dirfd.into(), path: path.into(), flags, mode, source: errno, } }) } /// Wrapper for `openat(2)` which auto-sets `O_CLOEXEC | O_NOCTTY | O_NOFOLLOW`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `openat(2)`. We need the dirfd argument, so we need a wrapper. pub(crate) fn openat( dirfd: impl AsFd, path: impl AsRef, mut flags: OpenFlags, mode: RawMode, // TODO: Should we take rustix::fs::Mode directly? ) -> Result { flags.insert(OpenFlags::O_NOFOLLOW); openat_follow(dirfd, path, flags, mode) } /// Wrapper for `readlinkat(2)`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `readlinkat(2)`. We need the dirfd argument, so we need a /// wrapper. pub(crate) fn readlinkat(dirfd: impl AsFd, path: impl AsRef) -> Result { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); // If the contents of the symlink are larger than this, we bail out avoid // DoS vectors (because there is no way to get the size of a symlink // beforehand, you just have to read it). // MSRV(1.79): Use const {}? let mut linkbuf: [MaybeUninit; 32 * 4096] = [MaybeUninit::uninit(); 32 * libc::PATH_MAX as usize]; let (target, trailing) = rustix_fs::readlinkat_raw(dirfd, path, &mut linkbuf[..]).map_err(|errno| { Error::Readlinkat { dirfd: dirfd.into(), path: path.into(), source: errno, } })?; if trailing.is_empty() { // The buffer was too small, return an error. Err(Error::Readlinkat { dirfd: dirfd.into(), path: path.into(), source: Errno::NAMETOOLONG, }) } else { Ok(PathBuf::from(OsStr::from_bytes(target))) } } /// Wrapper for `mkdirat(2)`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `mkdirat(2)`. We need the dirfd argument, so we need a wrapper. pub(crate) fn mkdirat( dirfd: impl AsFd, path: impl AsRef, mode: RawMode, // TODO: Should we take rustix::fs::Mode directly? ) -> Result<(), Error> { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); rustix_fs::mkdirat(dirfd, path, Mode::from_raw_mode(mode)).map_err(|errno| Error::Mkdirat { dirfd: dirfd.into(), path: path.into(), mode, source: errno, }) } pub(crate) fn devmajorminor(dev: Dev) -> (u32, u32) { (rustix_fs::major(dev), rustix_fs::minor(dev)) } /// Wrapper for `mknodat(2)`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `mknodat(2)`. We need the dirfd argument, so we need a wrapper. pub(crate) fn mknodat( dirfd: impl AsFd, path: impl AsRef, raw_mode: RawMode, // TODO: Should we take rustix::fs::{Mode,FileType} directly? dev: Dev, ) -> Result<(), Error> { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); let (file_type, mode) = ( FileType::from_raw_mode(raw_mode), Mode::from_raw_mode(raw_mode), ); rustix_fs::mknodat(dirfd, path, file_type, mode, dev).map_err(|errno| { let (major, minor) = devmajorminor(dev); Error::Mknodat { dirfd: dirfd.into(), path: path.into(), mode: raw_mode, major, minor, source: errno, } }) } /// Wrapper for `unlinkat(2)`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `unlinkat(2)`. We need the dirfd argument, so we need a wrapper. pub(crate) fn unlinkat( dirfd: impl AsFd, path: impl AsRef, flags: AtFlags, ) -> Result<(), Error> { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); rustix_fs::unlinkat(dirfd, path, flags).map_err(|errno| Error::Unlinkat { dirfd: dirfd.into(), path: path.into(), flags, source: errno, }) } /// Wrapper for `linkat(2)`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `linkat(2)`. We need the dirfd argument, so we need a wrapper. pub(crate) fn linkat( old_dirfd: impl AsFd, old_path: impl AsRef, new_dirfd: impl AsFd, new_path: impl AsRef, flags: AtFlags, ) -> Result<(), Error> { let (old_dirfd, old_path) = (old_dirfd.as_fd().check_rustix_fd()?, old_path.as_ref()); let (new_dirfd, new_path) = (new_dirfd.as_fd().check_rustix_fd()?, new_path.as_ref()); rustix_fs::linkat(old_dirfd, old_path, new_dirfd, new_path, flags).map_err(|errno| { Error::Linkat { old_dirfd: old_dirfd.into(), old_path: old_path.into(), new_dirfd: new_dirfd.into(), new_path: new_path.into(), flags, source: errno, } }) } /// Wrapper for `symlinkat(2)`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `symlinkat(2)`. We need the dirfd argument, so we need a /// wrapper. pub(crate) fn symlinkat( target: impl AsRef, dirfd: impl AsFd, path: impl AsRef, ) -> Result<(), Error> { let (dirfd, path) = (dirfd.as_fd().check_rustix_fd()?, path.as_ref()); let target = target.as_ref(); rustix_fs::symlinkat(target, dirfd, path).map_err(|errno| Error::Symlinkat { dirfd: dirfd.into(), path: path.into(), target: target.into(), source: errno, }) } /// Wrapper for `renameat(2)`. /// /// This is needed because Rust doesn't provide a way to access the dirfd /// argument of `renameat(2)`. We need the dirfd argument, so we need a wrapper. pub(crate) fn renameat( old_dirfd: impl AsFd, old_path: impl AsRef, new_dirfd: impl AsFd, new_path: impl AsRef, ) -> Result<(), Error> { let (old_dirfd, old_path) = (old_dirfd.as_fd().check_rustix_fd()?, old_path.as_ref()); let (new_dirfd, new_path) = (new_dirfd.as_fd().check_rustix_fd()?, new_path.as_ref()); rustix_fs::renameat(old_dirfd, old_path, new_dirfd, new_path).map_err(|errno| Error::Renameat { old_dirfd: old_dirfd.into(), old_path: old_path.into(), new_dirfd: new_dirfd.into(), new_path: new_path.into(), source: errno, }) } // MSRV(1.80): Use LazyLock. pub(crate) static RENAME_FLAGS_SUPPORTED: Lazy = Lazy::new(|| { match renameat2(AT_FDCWD, ".", AT_FDCWD, ".", RenameFlags::RENAME_EXCHANGE) { Ok(_) => true, // We expect EBUSY, but just to be safe we only check for ENOSYS. Err(err) => err.root_cause().raw_os_error() != Some(libc::ENOSYS), } }); /// Wrapper for `renameat2(2)`. /// /// This is needed because Rust doesn't provide any interface for `renameat2(2)` /// (especially not an interface for the dirfd). pub(crate) fn renameat2( old_dirfd: impl AsFd, old_path: impl AsRef, new_dirfd: impl AsFd, new_path: impl AsRef, flags: RenameFlags, ) -> Result<(), Error> { // Use renameat(2) if no flags are specified. if flags.is_empty() { return renameat(old_dirfd, old_path, new_dirfd, new_path); } let (old_dirfd, old_path) = (old_dirfd.as_fd().check_rustix_fd()?, old_path.as_ref()); let (new_dirfd, new_path) = (new_dirfd.as_fd().check_rustix_fd()?, new_path.as_ref()); rustix_fs::renameat_with(old_dirfd, old_path, new_dirfd, new_path, flags.into()).map_err( |errno| Error::Renameat2 { old_dirfd: old_dirfd.into(), old_path: old_path.into(), new_dirfd: new_dirfd.into(), new_path: new_path.into(), flags, source: errno, }, ) } /// Wrapper for `fstatfs(2)`. /// /// This is needed because Rust doesn't provide any interface for `fstatfs(2)`. pub(crate) fn fstatfs(fd: impl AsFd) -> Result { let fd = fd.as_fd().check_rustix_fd()?; rustix_fs::fstatfs(fd).map_err(|errno| Error::Fstatfs { fd: fd.into(), source: errno, }) } /// Wrapper for `fstatat(2)`, which auto-sets `AT_NO_AUTOMOUNT | /// AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH`. /// /// This is needed because Rust doesn't provide any interface for `fstatat(2)`. pub(crate) fn fstatat(dirfd: impl AsFd, path: impl AsRef) -> Result { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); let flags = AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW | AtFlags::EMPTY_PATH; rustix_fs::statat(dirfd, path, flags).map_err(|errno| Error::Fstatat { dirfd: dirfd.into(), path: path.into(), flags, source: errno, }) } pub(crate) fn statx( dirfd: impl AsFd, path: impl AsRef, mask: StatxFlags, ) -> Result { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); let flags = AtFlags::NO_AUTOMOUNT | AtFlags::SYMLINK_NOFOLLOW | AtFlags::EMPTY_PATH; rustix_fs::statx(dirfd, path, flags, mask).map_err(|errno| Error::Statx { dirfd: dirfd.into(), path: path.into(), flags, mask, source: errno, }) } mod openat2 { use super::*; // MSRV(1.80): Use LazyLock. pub(crate) static OPENAT2_IS_SUPPORTED: Lazy = Lazy::new(|| openat2(AT_FDCWD, ".", Default::default()).is_ok()); bitflags! { /// Wrapper for the underlying `libc`'s `RESOLVE_*` flags. /// /// The flag values and their meaning is identical to the description in the /// [`openat2(2)`] man page. /// /// [`openat2(2)`]: http://man7.org/linux/man-pages/man2/openat2.2.html #[derive(Default, PartialEq, Eq, Debug, Clone, Copy)] pub(crate) struct ResolveFlags: u64 { const RESOLVE_BENEATH = libc::RESOLVE_BENEATH; const RESOLVE_IN_ROOT = libc::RESOLVE_IN_ROOT; const RESOLVE_NO_MAGICLINKS = libc::RESOLVE_NO_MAGICLINKS; const RESOLVE_NO_SYMLINKS = libc::RESOLVE_NO_SYMLINKS; const RESOLVE_NO_XDEV = libc::RESOLVE_NO_XDEV; const RESOLVE_CACHED = libc::RESOLVE_CACHED; // Don't clobber unknown RESOLVE_* bits. const _ = !0; } } /// Arguments for how `openat2` should open the target path. #[repr(C)] #[derive(Copy, Clone, Debug, Default)] pub(crate) struct OpenHow { /// O_* flags (`-EINVAL` on unknown or incompatible flags). pub flags: u64, /// O_CREAT or O_TMPFILE file mode (must be zero otherwise). pub mode: u64, /// RESOLVE_* flags (`-EINVAL` on unknown flags). pub resolve: u64, } impl fmt::Display for OpenHow { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{{ ")?; // self.flags if let Ok(oflags) = i32::try_from(self.flags) { // If the flags can fit inside OpenFlags, pretty-print the flags. write!(f, "flags: {:?}, ", OpenFlags::from_bits_retain(oflags))?; } else { write!(f, "flags: 0x{:x}, ", self.flags)?; } if self.flags & (libc::O_CREAT | libc::O_TMPFILE) as u64 != 0 { write!(f, "mode: 0o{:o}, ", self.mode)?; } // self.resolve write!( f, "resolve: {:?}", ResolveFlags::from_bits_retain(self.resolve) )?; write!(f, " }}") } } /// Wrapper for `openat2(2)` which auto-sets `O_CLOEXEC | O_NOCTTY`. // NOTE: rustix's openat2 wrapper is not extensible-friendly so we use our own // for now. See . pub(crate) fn openat2_follow( dirfd: impl AsFd, path: impl AsRef, mut how: OpenHow, ) -> Result { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); // Add O_CLOEXEC and O_NOCTTY explicitly (as we do for openat). However, // O_NOCTTY cannot be set if O_PATH is set (openat2 verifies flag // arguments). how.flags |= libc::O_CLOEXEC as u64; if how.flags & libc::O_PATH as u64 == 0 { how.flags |= libc::O_NOCTTY as u64; } // SAFETY: Obviously safe-to-use Linux syscall. let fd = unsafe { libc::syscall( libc::SYS_openat2, dirfd.as_raw_fd(), path.to_c_string().as_ptr(), &how as *const OpenHow, std::mem::size_of::(), ) } as RawFd; let err = IOError::last_os_error(); if fd >= 0 { // SAFETY: We know it's a real file descriptor. Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } else { Err(Error::Openat2 { dirfd: dirfd.into(), path: path.into(), how, size: std::mem::size_of::(), source: err .raw_os_error() .map(Errno::from_raw_os_error) .expect("syscall failure must result in a real OS error"), }) } } /// Wrapper for `openat2(2)` which auto-sets `O_CLOEXEC | O_NOCTTY | /// O_NOFOLLOW`. pub(crate) fn openat2( dirfd: impl AsFd, path: impl AsRef, mut how: OpenHow, ) -> Result { how.flags |= libc::O_NOFOLLOW as u64; openat2_follow(dirfd, path, how) } } pub(crate) use openat2::*; #[cfg(test)] pub(crate) fn getpid() -> rustix_process::RawPid { rustix_process::Pid::as_raw(Some(rustix_process::getpid())) } pub(crate) fn gettid() -> rustix_process::RawPid { rustix_process::Pid::as_raw(Some(rustix_thread::gettid())) } pub(crate) fn geteuid() -> rustix_process::RawUid { rustix_process::geteuid().as_raw() } #[cfg(test)] pub(crate) fn getegid() -> rustix_process::RawGid { rustix_process::getegid().as_raw() } #[cfg(test)] pub(crate) fn getcwd() -> Result { let buffer = Vec::with_capacity(libc::PATH_MAX as usize); Ok(OsStr::from_bytes(rustix_process::getcwd(buffer)?.to_bytes()).into()) } pub(crate) fn fsopen(fstype: &str, flags: FsOpenFlags) -> Result { rustix_mount::fsopen(fstype, flags).map_err(|errno| Error::Fsopen { fstype: fstype.into(), flags, source: errno, }) } pub(crate) fn fsconfig_set_string(sfd: impl AsFd, key: &str, value: &str) -> Result<(), Error> { let sfd = sfd.as_fd().check_rustix_fd()?; rustix_mount::fsconfig_set_string(sfd, key, value).map_err(|errno| Error::FsconfigSetString { sfd: sfd.into(), key: key.into(), value: value.into(), source: errno, }) } pub(crate) fn fsconfig_create(sfd: impl AsFd) -> Result<(), Error> { let sfd = sfd.as_fd().check_rustix_fd()?; rustix_mount::fsconfig_create(sfd).map_err(|errno| Error::FsconfigCreate { sfd: sfd.into(), source: errno, }) } pub(crate) fn fsmount( sfd: impl AsFd, flags: FsMountFlags, mount_attrs: MountAttrFlags, ) -> Result { let sfd = sfd.as_fd().check_rustix_fd()?; rustix_mount::fsmount(sfd, flags, mount_attrs).map_err(|errno| Error::Fsmount { sfd: sfd.into(), flags, mount_attrs, source: errno, }) } pub(crate) fn open_tree( dirfd: impl AsFd, path: impl AsRef, flags: OpenTreeFlags, ) -> Result { let dirfd = dirfd.as_fd().check_rustix_fd()?; let path = path.as_ref(); rustix_mount::open_tree(dirfd, path, flags).map_err(|errno| Error::OpenTree { dirfd: dirfd.into(), path: path.into(), flags, source: errno, }) } pathrs-0.2.1/src/tests/capi/handle.rs000064400000000000000000000061051046102023000155750ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ capi, flags::OpenFlags, tests::{ capi::utils::{self as capi_utils, CapiError}, traits::HandleImpl, }, }; use std::{ fs::File, os::unix::io::{AsFd, BorrowedFd, OwnedFd}, }; #[derive(Debug)] pub struct CapiHandle { inner: OwnedFd, } impl CapiHandle { fn from_fd(fd: impl Into) -> Self { Self { inner: fd.into() } } fn try_clone(&self) -> Result { Ok(Self::from_fd(self.inner.try_clone()?)) } fn reopen(&self, flags: impl Into) -> Result { let fd = self.inner.as_fd(); let flags = flags.into(); capi_utils::call_capi_fd(|| capi::core::pathrs_reopen(fd.into(), flags.bits())) .map(File::from) } } impl AsFd for CapiHandle { fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } impl From for OwnedFd { fn from(handle: CapiHandle) -> Self { handle.inner } } impl HandleImpl for CapiHandle { type Cloned = CapiHandle; type Error = CapiError; fn from_fd(fd: impl Into) -> Self::Cloned { Self::Cloned::from_fd(fd) } fn try_clone(&self) -> Result { self.try_clone() } fn reopen(&self, flags: impl Into) -> Result { self.reopen(flags) } } impl HandleImpl for &CapiHandle { type Cloned = CapiHandle; type Error = CapiError; fn from_fd(fd: impl Into) -> Self::Cloned { Self::Cloned::from_fd(fd) } fn try_clone(&self) -> Result { CapiHandle::try_clone(self) } fn reopen(&self, flags: impl Into) -> Result { CapiHandle::reopen(self, flags) } } pathrs-0.2.1/src/tests/capi/procfs.rs000064400000000000000000000142371046102023000156430ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ capi::{ self, procfs::{CProcfsBase, ProcfsOpenFlags, ProcfsOpenHow}, }, flags::OpenFlags, procfs::ProcfsBase, tests::{ capi::utils::{self as capi_utils, CapiError}, traits::ProcfsHandleImpl, }, }; use std::{ fs::File, mem, os::unix::io::{AsFd, OwnedFd}, path::{Path, PathBuf}, }; #[derive(Debug)] pub struct CapiProcfsHandle; impl CapiProcfsHandle { fn open_follow( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { let base: CProcfsBase = base.into(); let subpath = capi_utils::path_to_cstring(subpath); let oflags = oflags.into(); capi_utils::call_capi_fd(|| unsafe { capi::procfs::pathrs_proc_open(base, subpath.as_ptr(), oflags.bits()) }) .map(File::from) } fn open( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { // The C API exposes ProcfsHandle::open using O_NOFOLLOW. self.open_follow(base, subpath, oflags.into() | OpenFlags::O_NOFOLLOW) } fn readlink(&self, base: ProcfsBase, subpath: impl AsRef) -> Result { let base: CProcfsBase = base.into(); let subpath = capi_utils::path_to_cstring(subpath); capi_utils::call_capi_readlink(|linkbuf, linkbuf_size| unsafe { capi::procfs::pathrs_proc_readlink(base, subpath.as_ptr(), linkbuf, linkbuf_size) }) } } impl ProcfsHandleImpl for CapiProcfsHandle { type Error = CapiError; fn open_follow( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { self.open_follow(base, subpath, oflags) } fn open( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { self.open(base, subpath, oflags) } fn readlink( &self, base: ProcfsBase, subpath: impl AsRef, ) -> Result { self.readlink(base, subpath) } } #[derive(Debug)] pub struct CapiProcfsHandleFd(pub OwnedFd); impl From for OwnedFd { fn from(procfs: CapiProcfsHandleFd) -> Self { procfs.0 } } impl CapiProcfsHandleFd { pub fn new_unmasked() -> Result { capi_utils::call_capi_fd(|| unsafe { let how = ProcfsOpenHow { flags: ProcfsOpenFlags::PATHRS_PROCFS_NEW_UNMASKED, }; capi::procfs::pathrs_procfs_open(&how as *const _, mem::size_of::()) }) .map(CapiProcfsHandleFd) } fn open_follow( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { let base: CProcfsBase = base.into(); let subpath = capi_utils::path_to_cstring(subpath); let oflags = oflags.into(); capi_utils::call_capi_fd(|| unsafe { capi::procfs::pathrs_proc_openat( self.0.as_fd().into(), base, subpath.as_ptr(), oflags.bits(), ) }) .map(File::from) } fn open( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { // The C API exposes ProcfsHandle::open using O_NOFOLLOW. self.open_follow(base, subpath, oflags.into() | OpenFlags::O_NOFOLLOW) } fn readlink(&self, base: ProcfsBase, subpath: impl AsRef) -> Result { let base: CProcfsBase = base.into(); let subpath = capi_utils::path_to_cstring(subpath); capi_utils::call_capi_readlink(|linkbuf, linkbuf_size| unsafe { capi::procfs::pathrs_proc_readlinkat( self.0.as_fd().into(), base, subpath.as_ptr(), linkbuf, linkbuf_size, ) }) } } impl ProcfsHandleImpl for CapiProcfsHandleFd { type Error = CapiError; fn open_follow( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { self.open_follow(base, subpath, oflags) } fn open( &self, base: ProcfsBase, subpath: impl AsRef, oflags: impl Into, ) -> Result { self.open(base, subpath, oflags) } fn readlink( &self, base: ProcfsBase, subpath: impl AsRef, ) -> Result { self.readlink(base, subpath) } } pathrs-0.2.1/src/tests/capi/root.rs000064400000000000000000000324461046102023000153340ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ capi, flags::{OpenFlags, RenameFlags}, resolvers::Resolver, tests::{ capi::{ utils::{self as capi_utils, CapiError}, CapiHandle, }, traits::{HandleImpl, RootImpl}, }, InodeType, }; use std::{ fs::{File, Permissions}, os::unix::{ fs::PermissionsExt, io::{AsFd, BorrowedFd, OwnedFd}, }, path::{Path, PathBuf}, }; #[derive(Debug)] pub(in crate::tests) struct CapiRoot { inner: OwnedFd, } impl CapiRoot { pub(in crate::tests) fn open(path: impl AsRef) -> Result { let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_open_root(path.as_ptr()) }) .map(Self::from_fd) } pub(in crate::tests) fn from_fd(fd: impl Into) -> Self { Self { inner: fd.into() } } fn try_clone(&self) -> Result { Ok(Self::from_fd(self.inner.try_clone()?)) } fn resolve(&self, path: impl AsRef) -> Result { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_inroot_resolve(root_fd.into(), path.as_ptr()) }) .map(CapiHandle::from_fd) } fn resolve_nofollow(&self, path: impl AsRef) -> Result { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_inroot_resolve_nofollow(root_fd.into(), path.as_ptr()) }) .map(CapiHandle::from_fd) } fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); let flags = flags.into(); capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_inroot_open(root_fd.into(), path.as_ptr(), flags.bits()) }) .map(From::from) } fn readlink(&self, path: impl AsRef) -> Result { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_readlink(|linkbuf, linkbuf_size| unsafe { capi::core::pathrs_inroot_readlink(root_fd.into(), path.as_ptr(), linkbuf, linkbuf_size) }) } fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), CapiError> { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_zst(|| match inode_type { InodeType::File(perm) => unsafe { capi::core::pathrs_inroot_mknod( root_fd.into(), path.as_ptr(), libc::S_IFREG | perm.mode(), 0, ) }, InodeType::Directory(perm) => unsafe { capi::core::pathrs_inroot_mkdir(root_fd.into(), path.as_ptr(), perm.mode()) }, InodeType::Symlink(target) => { let target = capi_utils::path_to_cstring(target); unsafe { capi::core::pathrs_inroot_symlink( root_fd.into(), path.as_ptr(), target.as_ptr(), ) } } InodeType::Hardlink(target) => { let target = capi_utils::path_to_cstring(target); unsafe { capi::core::pathrs_inroot_hardlink( root_fd.into(), path.as_ptr(), target.as_ptr(), ) } } InodeType::Fifo(perm) => unsafe { capi::core::pathrs_inroot_mknod( root_fd.into(), path.as_ptr(), libc::S_IFIFO | perm.mode(), 0, ) }, InodeType::CharacterDevice(perm, dev) => unsafe { capi::core::pathrs_inroot_mknod( root_fd.into(), path.as_ptr(), libc::S_IFCHR | perm.mode(), *dev, ) }, InodeType::BlockDevice(perm, dev) => unsafe { capi::core::pathrs_inroot_mknod( root_fd.into(), path.as_ptr(), libc::S_IFBLK | perm.mode(), *dev, ) }, }) } fn create_file( &self, path: impl AsRef, flags: impl Into, perm: &Permissions, ) -> Result { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); let flags = flags.into(); capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_inroot_creat( root_fd.into(), path.as_ptr(), flags.bits(), perm.mode(), ) }) .map(File::from) } fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_fd(|| unsafe { capi::core::pathrs_inroot_mkdir_all(root_fd.into(), path.as_ptr(), perm.mode()) }) .map(CapiHandle::from_fd) } fn remove_dir(&self, path: impl AsRef) -> Result<(), CapiError> { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_zst(|| unsafe { capi::core::pathrs_inroot_rmdir(root_fd.into(), path.as_ptr()) }) } fn remove_file(&self, path: impl AsRef) -> Result<(), CapiError> { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_zst(|| unsafe { capi::core::pathrs_inroot_unlink(root_fd.into(), path.as_ptr()) }) } fn remove_all(&self, path: impl AsRef) -> Result<(), CapiError> { let root_fd = self.inner.as_fd(); let path = capi_utils::path_to_cstring(path); capi_utils::call_capi_zst(|| unsafe { capi::core::pathrs_inroot_remove_all(root_fd.into(), path.as_ptr()) }) } fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), CapiError> { let root_fd = self.inner.as_fd(); let source = capi_utils::path_to_cstring(source); let destination = capi_utils::path_to_cstring(destination); capi_utils::call_capi_zst(|| unsafe { capi::core::pathrs_inroot_rename( root_fd.into(), source.as_ptr(), destination.as_ptr(), rflags.bits(), ) }) } } impl AsFd for CapiRoot { fn as_fd(&self) -> BorrowedFd<'_> { self.inner.as_fd() } } impl From for OwnedFd { fn from(root: CapiRoot) -> Self { root.inner } } impl RootImpl for CapiRoot { type Cloned = CapiRoot; type Handle = CapiHandle; // NOTE: We can't use anyhow::Error here. // type Error = CapiError; fn from_fd(fd: impl Into, resolver: Resolver) -> Self::Cloned { assert_eq!( resolver, Resolver::default(), "cannot use non-default Resolver with capi" ); Self::Cloned::from_fd(fd) } fn resolver(&self) -> Resolver { Resolver::default() } fn try_clone(&self) -> Result { self.try_clone() } fn resolve(&self, path: impl AsRef) -> Result { self.resolve(path) } fn resolve_nofollow(&self, path: impl AsRef) -> Result { self.resolve_nofollow(path) } fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { self.open_subpath(path, flags) } fn readlink(&self, path: impl AsRef) -> Result { self.readlink(path) } fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Self::Error> { self.create(path, inode_type) } fn create_file( &self, path: impl AsRef, flags: OpenFlags, perm: &Permissions, ) -> Result { self.create_file(path, flags, perm) } fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result { self.mkdir_all(path, perm) } fn remove_dir(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_dir(path) } fn remove_file(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_file(path) } fn remove_all(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_all(path) } fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), CapiError> { self.rename(source, destination, rflags) } } impl RootImpl for &CapiRoot { type Cloned = CapiRoot; type Handle = CapiHandle; // NOTE: We can't use anyhow::Error here. // type Error = CapiError; fn from_fd(fd: impl Into, resolver: Resolver) -> Self::Cloned { assert_eq!( resolver, Resolver::default(), "cannot use non-default Resolver with capi" ); Self::Cloned::from_fd(fd) } fn resolver(&self) -> Resolver { Resolver::default() } fn try_clone(&self) -> Result { CapiRoot::try_clone(self) } fn resolve(&self, path: impl AsRef) -> Result { CapiRoot::resolve(self, path) } fn resolve_nofollow(&self, path: impl AsRef) -> Result { CapiRoot::resolve_nofollow(self, path) } fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { CapiRoot::open_subpath(self, path, flags) } fn readlink(&self, path: impl AsRef) -> Result { CapiRoot::readlink(self, path) } fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Self::Error> { CapiRoot::create(self, path, inode_type) } fn create_file( &self, path: impl AsRef, flags: OpenFlags, perm: &Permissions, ) -> Result { CapiRoot::create_file(self, path, flags, perm) } fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result { CapiRoot::mkdir_all(self, path, perm) } fn remove_dir(&self, path: impl AsRef) -> Result<(), Self::Error> { CapiRoot::remove_dir(self, path) } fn remove_file(&self, path: impl AsRef) -> Result<(), Self::Error> { CapiRoot::remove_file(self, path) } fn remove_all(&self, path: impl AsRef) -> Result<(), Self::Error> { CapiRoot::remove_all(self, path) } fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), CapiError> { CapiRoot::rename(self, source, destination, rflags) } } pathrs-0.2.1/src/tests/capi/utils.rs000064400000000000000000000141331046102023000155020ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ capi::error as capi_error, error::{ErrorExt, ErrorKind}, tests::traits::ErrorImpl, }; use std::{ ffi::{CStr, CString, OsStr}, fmt, os::unix::{ ffi::OsStrExt, io::{FromRawFd, OwnedFd}, }, path::{Path, PathBuf}, ptr, }; use errno::Errno; #[derive(Debug, Clone, thiserror::Error)] pub struct CapiError { errno: Option, description: String, } impl fmt::Display for CapiError { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "{}", self.description)?; if let Some(errno) = self.errno { write!(fmt, " ({errno})")?; } Ok(()) } } impl ErrorExt for CapiError { fn with_wrap(self, context_fn: F) -> Self where F: FnOnce() -> String, { Self { errno: self.errno, description: context_fn() + ": " + &self.description, } } } impl ErrorImpl for CapiError { fn kind(&self) -> ErrorKind { if let Some(errno) = self.errno { ErrorKind::OsError(Some(errno.0)) } else { // TODO: We should probably have an actual "no-op" error here that // is unused except for these tests so we can properly detect // a bad ErrorKind. ErrorKind::InternalError } } } fn fetch_error(res: libc::c_int) -> Result { if res >= 0 { Ok(res) } else { // SAFETY: pathrs_errorinfo is safe to call in general. match unsafe { capi_error::pathrs_errorinfo(res) } { Some(err) => { let errno = match err.saved_errno as i32 { 0 => None, errno => Some(Errno(errno)), }; // SAFETY: pathrs_errorinfo returns a valid string pointer. We // can't take ownership because pathrs_errorinfo_free() will do // the freeing for us. let description = unsafe { CStr::from_ptr(err.description) } .to_string_lossy() .to_string(); // Free the error from the error map, now that we copied the // contents. // SAFETY: We are the only ones holding a reference to the err // pointer and don't touch it later, so we can free it freely. unsafe { capi_error::pathrs_errorinfo_free(err as *mut _) } Err(CapiError { errno, description }) } None => panic!("unknown error id {res}"), } } } pub(in crate::tests) fn path_to_cstring(path: impl AsRef) -> CString { CString::new(path.as_ref().as_os_str().as_bytes()) .expect("normal path conversion shouldn't result in spurious nul bytes") } pub(in crate::tests) fn call_capi(func: Func) -> Result where Func: Fn() -> libc::c_int, { fetch_error(func()) } pub(in crate::tests) fn call_capi_zst(func: Func) -> Result<(), CapiError> where Func: Fn() -> libc::c_int, { call_capi(func).map(|val| { assert_eq!( val, 0, "call_capi_zst must only be called on methods that return <= 0: got {val}" ); }) } pub(in crate::tests) fn call_capi_fd(func: Func) -> Result where Func: Fn() -> libc::c_int, { // SAFETY: The caller has guaranteed us that the closure will return an fd. call_capi(func).map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }) } pub(in crate::tests) fn call_capi_readlink(func: Func) -> Result where Func: Fn(*mut libc::c_char, libc::size_t) -> libc::c_int, { // Get the actual size by passing NULL. let mut actual_size = { // Try zero-size. let size1 = fetch_error(func(123 as *mut _, 0))? as usize; // Try NULL ptr. let size2 = fetch_error(func(ptr::null_mut(), 1000))? as usize; assert_eq!(size1, size2, "readlink size should be the same"); size1 }; // Start with a smaller buffer so we can exercise the trimming logic. // TODO: Use slice::assume_init_ref() once maybe_uninit_slice is stabilised. let mut linkbuf: Vec = Vec::with_capacity(0); actual_size /= 2; while actual_size > linkbuf.capacity() { linkbuf.reserve(actual_size); actual_size = fetch_error(func( linkbuf.as_mut_ptr() as *mut libc::c_char, linkbuf.capacity(), ))? as usize; } // SAFETY: The C code guarantees that actual_size is how many bytes are filled. unsafe { linkbuf.set_len(actual_size) }; Ok(PathBuf::from(OsStr::from_bytes( // readlink does *not* append a null terminator! CString::new(linkbuf) .expect("constructing a CString from the C API's copied CString should work") .to_bytes(), ))) } pathrs-0.2.1/src/tests/common/error.rs000064400000000000000000000044101046102023000160440ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{error::ErrorKind, tests::traits::ErrorImpl}; use std::fmt::Debug; use anyhow::Error; pub(in crate::tests) fn check_err( result: &Result, expected: &Result, ) -> Result<(), Error> where T1: Debug, Err1: ErrorImpl, T2: Debug, { let result = result.as_ref(); let expected = expected.as_ref(); match (result, expected) { (Err(error), Err(expected_kind)) => { let kind = error.kind(); let (result_errno, expected_errno) = (kind.errno(), expected_kind.errno()); if kind != *expected_kind && result_errno != expected_errno { anyhow::bail!( "expected error {expected_kind:?} (errno: {expected_errno:?}) but got {error:?} (kind: {kind:?}, errno: {result_errno:?})" ); } } (Ok(_), Ok(_)) => (), (result, expected) => anyhow::bail!("expected {expected:?} but got {result:?}"), } Ok(()) } pathrs-0.2.1/src/tests/common/handle.rs000064400000000000000000000153651046102023000161610ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::ErrorKind, flags::OpenFlags, resolvers::PartialLookup, tests::{common as tests_common, traits::HandleImpl}, utils::FdExt, }; use std::os::unix::io::AsFd; use anyhow::{Context, Error}; use pretty_assertions::assert_eq; use rustix::{ fs::{self as rustix_fs, OFlags}, io::{self as rustix_io, FdFlags}, }; pub type LookupResult<'a> = (&'a str, libc::mode_t); impl PartialLookup { pub(in crate::tests) fn as_inner_handle(&self) -> &H { match self { PartialLookup::Complete(handle) => handle, PartialLookup::Partial { handle, .. } => handle, } } } pub(in crate::tests) trait AsError { fn as_error(&self) -> Option<&E>; } impl AsError for Result, E> { fn as_error(&self) -> Option<&E> { match self { Ok(PartialLookup::Complete(_)) => None, Ok(PartialLookup::Partial { last_error, .. }) => Some(last_error), Err(err) => Some(err), } } } impl PartialEq for PartialLookup where H: PartialEq, E: PartialEq, { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Complete(left), Self::Complete(right)) => left == right, ( Self::Partial { handle: left_handle, remaining: left_remaining, last_error: left_last_error, }, Self::Partial { handle: right_handle, remaining: right_remaining, last_error: right_last_error, }, ) => { left_handle == right_handle && left_remaining == right_remaining && left_last_error == right_last_error } _ => false, } } } pub(in crate::tests) fn check_oflags(fd: impl AsFd, flags: OpenFlags) -> Result<(), Error> { let fd = fd.as_fd(); // Convert to OFlags so we can compare them. let mut wanted_flags = OFlags::from_bits_retain(flags.bits() as u32); // O_CLOEXEC is always automatically enabled by libpathrs. wanted_flags.insert(OFlags::CLOEXEC); // The kernel clears several flags from f_flags in do_dentry_open(), so we // need to drop them from the expected flag set. wanted_flags.remove(OFlags::CREATE | OFlags::EXCL | OFlags::NOCTTY | OFlags::TRUNC); // The O_PATH one-shot resolver (i.e., resolvers::procfs::opath_resolve) // will add O_NOFOLLOW silently to returned files and there isn't a way for // us to remove this (F_SETFL silently masks O_NOFOLLOW so we cannot clear // it). So, add O_NOFOLLOW to the wanted flags if it is set. For a returned // file, O_NOFOLLOW makes no practical difference anyway (and our tests // check for the actually opened file anyway). let got_file_flags = rustix_fs::fcntl_getfl(fd).context("failed to F_GETFL")?; if got_file_flags.contains(OFlags::NOFOLLOW) { wanted_flags.insert(OFlags::NOFOLLOW) } // Check regular file flags. assert_eq!( // Ignore O_LARGEFILE since it's basically a kernel internal. got_file_flags & !OFlags::LARGEFILE, // O_CLOEXEC is represented in the fd flags, not file flags. wanted_flags & !OFlags::CLOEXEC, "expected the reopened file's flags to match the requested flags" ); // Check fd flags (namely O_CLOEXEC). let got_fd_flags = rustix_io::fcntl_getfd(fd).context("failed to F_GETFD")?; assert_eq!( got_fd_flags.contains(FdFlags::CLOEXEC), wanted_flags.contains(OFlags::CLOEXEC), "expected the reopened file's O_CLOEXEC to be correct (oflags: {flags:?})", ); assert!( got_fd_flags.difference(FdFlags::CLOEXEC).is_empty(), "expected fd flags to not contain anything other than FD_CLOEXEC (got flags: 0x{:x})", got_fd_flags.bits() ); Ok(()) } pub(in crate::tests) fn check_reopen( handle: H, flags: OpenFlags, expected_error: Option, ) -> Result<(), Error> { let expected_error = match expected_error { None => Ok(()), Some(errno) => Err(ErrorKind::OsError(Some(errno))), }; let file = match (handle.reopen(flags), expected_error) { (Ok(f), Ok(_)) => f, (result, expected) => { let result = match result { Ok(file) => Ok(file.as_unsafe_path_unchecked()?), Err(err) => Err(err), }; tests_common::check_err(&result, &expected) .with_context(|| format!("reopen handle {flags:?}"))?; assert!( result.is_err(), "we should never see an Ok(file) after check_err if we expected {expected:?}" ); return Ok(()); } }; let real_handle_path = handle.as_unsafe_path_unchecked()?; let real_reopen_path = file.as_unsafe_path_unchecked()?; 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()?; assert_eq!( real_handle_path, clone_handle_path, "cloned handle should be equivalent to old handle", ); check_oflags( &file, // NOTE: Handle::reopen() drops O_NOFOLLOW, so we shouldn't see it. flags.difference(OpenFlags::O_NOFOLLOW), )?; Ok(()) } pathrs-0.2.1/src/tests/common/mntns.rs000064400000000000000000000151211046102023000160530ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{flags::OpenFlags, syscalls, utils::FdExt}; use std::{ fs::File, os::unix::io::{AsFd, AsRawFd}, path::{Path, PathBuf}, }; use anyhow::{bail, Context, Error}; use rustix::{ mount::{self as rustix_mount, MountFlags, MountPropagationFlags}, thread::{self as rustix_thread, LinkNameSpaceType, UnshareFlags}, }; #[derive(Debug, Clone)] pub(crate) enum MountType { Tmpfs, Bind { src: PathBuf }, RebindWithFlags { flags: MountFlags }, } // TODO: NOSYMFOLLOW is not exported for the libc backend of rustix. Until this // is fixed by we need to // hardcode the value here. Thanfully, it has the same value for all // architectures. pub(in crate::tests) const NOSYMFOLLOW: MountFlags = MountFlags::from_bits_retain(0x100); // From . fn are_vfs_flags(flags: MountFlags) -> bool { flags .difference( // MS_RDONLY can be both a vfsmount and sb flag, but if we're operating // using MS_BIND then it acts like a vfs flag. MountFlags::RDONLY | // These NO* flags are all per-vfsmount flags. MountFlags::NOSUID | MountFlags::NODEV | MountFlags::NOEXEC | NOSYMFOLLOW | // Except LAZYATIME, these are all per-vfsmount flags. MountFlags::NOATIME | MountFlags::NODIRATIME | MountFlags::RELATIME, ) .is_empty() } pub(in crate::tests) fn mount(dst: impl AsRef, ty: MountType) -> Result<(), Error> { let dst = dst.as_ref(); let dst_file = syscalls::openat( syscalls::AT_FDCWD, dst, OpenFlags::O_NOFOLLOW | OpenFlags::O_PATH, 0, )?; let dst_path = format!("/proc/self/fd/{}", dst_file.as_raw_fd()); match ty { MountType::Tmpfs => rustix_mount::mount("", &dst_path, "tmpfs", MountFlags::empty(), None) .with_context(|| { format!( "mount tmpfs on {:?}", dst_file .as_unsafe_path_unchecked() .unwrap_or(dst_path.into()) ) }), MountType::Bind { src } => { let src_file = syscalls::openat( syscalls::AT_FDCWD, src, OpenFlags::O_NOFOLLOW | OpenFlags::O_PATH, 0, )?; let src_path = format!("/proc/self/fd/{}", src_file.as_raw_fd()); rustix_mount::mount_bind(&src_path, &dst_path).with_context(|| { format!( "bind-mount {:?} -> {:?}", src_file .as_unsafe_path_unchecked() .unwrap_or(src_path.into()), dst_file .as_unsafe_path_unchecked() .unwrap_or(dst_path.into()) ) }) } MountType::RebindWithFlags { flags } => { if !are_vfs_flags(flags) { bail!("rebind-with-flags mount options {flags:?} contains non-vfsmount flags"); } // Create a bind-mount first for us to apply our mount flags to. rustix_mount::mount_bind_recursive(&dst_path, &dst_path).with_context(|| { format!( "bind-mount {:?} to self", dst_file .as_unsafe_path_unchecked() .unwrap_or(dst_path.clone().into()) ) })?; // We need to re-open the path because the handle references the // dentry below the mount, and so MS_REMOUNT will return -EINVAL if // we don't get a new handle. // TODO: Would be nice to be able to do reopen(O_PATH|O_NOFOLLOW). let dst_file = syscalls::openat( syscalls::AT_FDCWD, dst, OpenFlags::O_NOFOLLOW | OpenFlags::O_PATH, 0, )?; let dst_path = format!("/proc/self/fd/{}", dst_file.as_raw_fd()); // Then apply our mount flags. rustix_mount::mount_remount(&dst_path, MountFlags::BIND | flags, "").with_context( || { format!( "vfs-remount {:?} with {flags:?}", dst_file .as_unsafe_path_unchecked() .unwrap_or(dst_path.into()) ) }, ) } } } pub(in crate::tests) fn in_mnt_ns(func: F) -> Result where F: FnOnce() -> Result, { let old_ns = File::open("/proc/self/ns/mnt")?; // TODO: Run this in a subprocess. // SAFETY: CLONE_FS | CLONE_NEWNS do not impact the IO safety of file // descriptors, and we do not send file descriptors from the test to other // threads anyway. unsafe { rustix_thread::unshare_unsafe(UnshareFlags::FS | UnshareFlags::NEWNS) } .expect("unable to create a mount namespace"); // Mark / as MS_SLAVE ("DOWNSTREAM" in rustix) to avoid DoSing the host. rustix_mount::mount_change( "/", MountPropagationFlags::DOWNSTREAM | MountPropagationFlags::REC, )?; let ret = func(); rustix_thread::move_into_link_name_space(old_ns.as_fd(), Some(LinkNameSpaceType::Mount)) .expect("unable to rejoin old namespace"); ret } pathrs-0.2.1/src/tests/common/root.rs000064400000000000000000000424711046102023000157070ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use std::{ fs, os::unix::fs as unixfs, path::{Path, PathBuf}, }; use crate::{ syscalls, tests::common::{self as tests_common, MountType}, }; use anyhow::{Context, Error}; use rustix::{ fs::{self as rustix_fs, AtFlags, OFlags, CWD}, mount::MountFlags, }; use tempfile::TempDir; // TODO: Make these macros usable from outside this crate... macro_rules! create_inode { // "/foo/bar" @ chmod 0o755 (@do $path:expr, chmod $mode:expr) => { // rustix returns -EOPNOTSUPP if you use AT_SYMLINK_NOFOLLOW. rustix_fs::chmodat(CWD, $path, $mode.into(), AtFlags::empty()) .with_context(|| format!("chmod 0o{:o} {}", $mode, $path.display()))?; }; // "/foo/bar" @ chown 0:0 (@do $path:expr, chown $uid:literal : $gid:literal) => { rustix_fs::chownat( CWD, $path, Some(::rustix::process::Uid::from_raw($uid)), Some(::rustix::process::Gid::from_raw($gid)), AtFlags::SYMLINK_NOFOLLOW, ) .with_context(|| format!("chown {}:{} {}", $uid, $gid, $path.display()))?; }; // "/foo/bar" @ chown 0: (@do $path:expr, chown $uid:literal :) => { rustix_fs::chownat( CWD, $path, Some(::rustix::process::Uid::from_raw($uid)), None, AtFlags::SYMLINK_NOFOLLOW, ) .with_context(|| format!("chown {}: {}", $uid, $path.display()))?; }; // "/foo/bar" @ chown :0 (@do $path:expr, chown : $gid:literal) => { rustix_fs::chownat( CWD, $path, None, Some(::rustix::process::Gid::from_raw($gid)), AtFlags::SYMLINK_NOFOLLOW, ) .with_context(|| format!("chown :{} {}", $gid, $path.display()))?; }; // "/foo/bar" => dir ($path:expr => dir $(,{$($extra:tt)*})*) => { rustix_fs::mkdir($path, 0o755.into()) .with_context(|| format!("mkdir {}", $path.display()))?; $( create_inode!(@do $path, $($extra)*); )* }; // "/foo/bar" => file ($path:expr => file $(,{$($extra:tt)*})*) => { rustix_fs::open($path, OFlags::CREATE, 0o644.into()) .with_context(|| format!("mkfile {}", $path.display()))?; $( create_inode!(@do $path, $($extra)*); )* }; // "/foo/bar" => fifo ($path:expr => fifo $(, {$($extra:tt)*})*) => { syscalls::mknodat(rustix_fs::CWD, $path, libc::S_IFIFO | 0o644, 0) .with_context(|| format!("mkfifo {}", $path.display()))?; $( create_inode!(@do $path, $($extra)*); )* }; // "/foo/bar" => sock ($path:expr => sock $(,{$($extra:tt)*})*) => { syscalls::mknodat(rustix_fs::CWD, $path, libc::S_IFSOCK | 0o644, 0) .with_context(|| format!("mksock {}", $path.display()))?; $( create_inode!(@do $path, $($extra)*); )* }; // "/foo/bar" => symlink -> "target" ($path:expr => symlink -> $target:expr $(,{$($extra:tt)*})*) => { unixfs::symlink($target, $path) .with_context(|| format!("symlink {} -> {}", $path.display(), $target))?; $( create_inode!(@do $path, $($extra)*); )* }; // "/foo/bar" => hardlink -> "target" ($path:expr => hardlink -> $target:expr) => { fs::hard_link($target, $path) .with_context(|| format!("hardlink {} -> {}", $path.display(), $target))?; }; } macro_rules! create_tree { // create_tree! { // "a" => (dir); // "a/b/c" => (file); // "b-link" => (symlink -> "a/b"); // } ($($subpath:expr => $(#[$meta:meta])* ($($inner:tt)*));+ $(;)*) => { { let root = TempDir::new()?; $( $(#[$meta])* { let root_dir: &Path = root.as_ref(); let subpath = $subpath; let path = root_dir.join(subpath.trim_start_matches('/')); if let Some(parent) = path.parent() { fs::create_dir_all(parent).with_context(|| format!("mkdirall {}", path.display()))?; } create_inode!(&path => $($inner)*); } )* root } } } pub(crate) fn create_basic_tree() -> Result { Ok(create_tree! { // Basic inodes. "a" => (dir); "b/c/d/e/f" => (dir); "b/c/file" => (file); "e" => (symlink -> "/b/c/d/e"); "b-file" => (symlink -> "b/c/file"); "root-link1" => (symlink -> "/"); "root-link2" => (symlink -> "/.."); "root-link3" => (symlink -> "/../../../../.."); "escape-link1" => (symlink -> "../../../../../../../../../../target"); "escape-link2" => (symlink -> "/../../../../../../../../../../target"); // Some "bad" inodes that non-privileged users can create. "b/fifo" => (fifo); "b/sock" => (sock); // Dangling symlinks. "a-fake1" => (symlink -> "a/fake"); "a-fake2" => (symlink -> "a/fake/foo/bar/.."); "a-fake3" => (symlink -> "a/fake/../../b"); "c/a-fake1" => (symlink -> "/a/fake"); "c/a-fake2" => (symlink -> "/a/fake/foo/bar/.."); "c/a-fake3" => (symlink -> "/a/fake/../../b"); // Non-lexical symlinks. "target" => (dir); "link1/target_abs" => (symlink -> "/target"); "link1/target_rel" => (symlink -> "../target"); "link2/link1_abs" => (symlink -> "/link1"); "link2/link1_rel" => (symlink -> "../link1"); "link3/target_abs" => (symlink -> "/link2/link1_rel/target_rel"); "link3/target_rel" => (symlink -> "../link2/link1_rel/target_rel"); "link3/deep_dangling1" => (symlink -> "../link2/link1_rel/target_rel/nonexist"); "link3/deep_dangling2" => (symlink -> "../link2/link1_abs/target_abs/nonexist"); // Deep dangling symlinks (with single components). "dangling/a" => (symlink -> "b/c"); "dangling/b/c" => (symlink -> "../c"); "dangling/c" => (symlink -> "d/e"); "dangling/d/e" => (symlink -> "../e"); "dangling/e" => (symlink -> "f/../g"); "dangling/f" => (dir); "dangling/g" => (symlink -> "h/i/j/nonexistent"); "dangling/h/i/j" => (dir); // Deep dangling symlinks using a non-dir component. "dangling-file/a" => (symlink -> "b/c"); "dangling-file/b/c" => (symlink -> "../c"); "dangling-file/c" => (symlink -> "d/e"); "dangling-file/d/e" => (symlink -> "../e"); "dangling-file/e" => (symlink -> "f/../g"); "dangling-file/f" => (dir); "dangling-file/g" => (symlink -> "h/i/j/file/foo"); "dangling-file/h/i/j/file" => (file); // Symlink loops. "loop/basic-loop1" => (symlink -> "basic-loop1"); "loop/basic-loop2" => (symlink -> "/loop/basic-loop2"); "loop/basic-loop3" => (symlink -> "../loop/basic-loop3"); "loop/a/link" => (symlink -> "../b/link"); "loop/b/link" => (symlink -> "/loop/c/link"); "loop/c/link" => (symlink -> "/loop/d/link"); "loop/d" => (symlink -> "e"); "loop/e/link" => (symlink -> "../a/link"); "loop/link" => (symlink -> "a/link"); // Symlinks for use with MS_NOSYMFOLLOW testing. "nosymfollow/goodlink" => (symlink -> "/"); "nosymfollow/badlink" => (symlink -> "/"); // MS_NOSYMFOLLOW "nosymfollow/nosymdir/dir/badlink" => (symlink -> "/"); // MS_NOSYMFOLLOW "nosymfollow/nosymdir/dir/goodlink" => (symlink -> "/"); "nosymfollow/nosymdir/dir/foo/yessymdir/bar/goodlink" => (symlink -> "/"); // Symlinks in a world-writable directory (fs.protected_symlinks). // ... owned by us. "tmpfs-self" => (dir, {chmod 0o1777}); "tmpfs-self/file" => (file); "tmpfs-self/link-self" => (symlink -> "file"); "tmpfs-self/link-otheruid" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown 12345:}); "tmpfs-self/link-othergid" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown :12345}); "tmpfs-self/link-other" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown 12345:12345}); // ... owned by another user. "tmpfs-other" => #[cfg(feature = "_test_as_root")] (dir, {chown 12345:12345}, {chmod 0o1777}); "tmpfs-other/file" => #[cfg(feature = "_test_as_root")] (file); "tmpfs-other/link-self" => #[cfg(feature = "_test_as_root")] (symlink -> "file"); "tmpfs-other/link-selfuid" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown :11111}); "tmpfs-other/link-owner" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown 12345:12345}); "tmpfs-other/link-otheruid" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown 11111:12345}); "tmpfs-other/link-othergid" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown 12345:11111}); "tmpfs-other/link-other" => #[cfg(feature = "_test_as_root")] (symlink -> "file", {chown 11111:11111}); // setgid has unique behaviour when interacting with mkdir_all. "setgid-self" => (dir, {chmod 0o7777}); "setgid-other" => #[cfg(feature = "_test_as_root")] (dir, {chown 12345:12345}, {chmod 0o7777}); // Deep directory tree to be used for testing remove-all. "deep-rmdir" => (dir); "deep-rmdir/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" => (file); "deep-rmdir/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" => (dir); "deep-rmdir/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" => (file); "deep-rmdir/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z" => (symlink -> "/"); "deep-rmdir/aa/bb/foo/bar/baz" => (file); "deep-rmdir/aa/cc/foo/bar/baz" => (file); "deep-rmdir/aa/dd/foo/bar/baz" => (file); "deep-rmdir/aa/ee/foo/bar/baz" => (file); "deep-rmdir/aa/ff/foo/bar/baz" => (file); "deep-rmdir/aa/gg/foo/bar/baz" => (file); "deep-rmdir/aa/hh/foo/bar/baz" => (file); "deep-rmdir/aa/ii/foo/bar/baz" => (file); "deep-rmdir/aa/jj/foo/bar/baz" => (file); "deep-rmdir/aa/kk/foo/bar/baz" => (file); "deep-rmdir/aa/ll/foo/bar/baz" => (file); "deep-rmdir/aa/mm/foo/bar/baz" => (file); "deep-rmdir/aa/nn/foo/bar/baz" => (file); "deep-rmdir/aa/oo/foo/bar/baz" => (file); "deep-rmdir/aa/pp/foo/bar/baz" => (file); "deep-rmdir/aa/qq/foo/bar/baz" => (file); "deep-rmdir/aa/rr/foo/bar/baz" => (file); "deep-rmdir/aa/ss/foo/bar/baz" => (file); "deep-rmdir/aa/tt/foo/bar/baz" => (file); "deep-rmdir/aa/uu/foo/bar/baz" => (file); "deep-rmdir/aa/vv/foo/bar/baz" => (file); "deep-rmdir/aa/ww/foo/bar/baz" => (file); "deep-rmdir/aa/xx/foo/bar/baz" => (file); "deep-rmdir/aa/yy/foo/bar/baz" => (file); "deep-rmdir/aa/zz/foo/bar/baz" => (file); "deep-rmdir/bb/bb/foo/bar/baz" => (file); "deep-rmdir/bb/cc/foo/bar/baz" => (file); "deep-rmdir/bb/dd/foo/bar/baz" => (file); "deep-rmdir/bb/ee/foo/bar/baz" => (file); "deep-rmdir/bb/ff/foo/bar/baz" => (file); "deep-rmdir/bb/gg/foo/bar/baz" => (file); "deep-rmdir/bb/hh/foo/bar/baz" => (file); "deep-rmdir/bb/ii/foo/bar/baz" => (file); "deep-rmdir/bb/jj/foo/bar/baz" => (file); "deep-rmdir/bb/kk/foo/bar/baz" => (file); "deep-rmdir/bb/ll/foo/bar/baz" => (file); "deep-rmdir/bb/mm/foo/bar/baz" => (file); "deep-rmdir/bb/nn/foo/bar/baz" => (file); "deep-rmdir/bb/oo/foo/bar/baz" => (file); "deep-rmdir/bb/pp/foo/bar/baz" => (file); "deep-rmdir/bb/qq/foo/bar/baz" => (file); "deep-rmdir/bb/rr/foo/bar/baz" => (file); "deep-rmdir/bb/ss/foo/bar/baz" => (file); "deep-rmdir/bb/tt/foo/bar/baz" => (file); "deep-rmdir/bb/uu/foo/bar/baz" => (file); "deep-rmdir/bb/vv/foo/bar/baz" => (file); "deep-rmdir/bb/ww/foo/bar/baz" => (file); "deep-rmdir/bb/xx/foo/bar/baz" => (file); "deep-rmdir/bb/yy/foo/bar/baz" => (file); "deep-rmdir/bb/zz/foo/bar/baz" => (file); "deep-rmdir/cc/bb/foo/bar/baz" => (file); "deep-rmdir/cc/cc/foo/bar/baz" => (file); "deep-rmdir/cc/dd/foo/bar/baz" => (file); "deep-rmdir/cc/ee/foo/bar/baz" => (file); "deep-rmdir/cc/ff/foo/bar/baz" => (file); "deep-rmdir/cc/gg/foo/bar/baz" => (file); "deep-rmdir/cc/hh/foo/bar/baz" => (file); "deep-rmdir/cc/ii/foo/bar/baz" => (file); "deep-rmdir/cc/jj/foo/bar/baz" => (file); "deep-rmdir/cc/kk/foo/bar/baz" => (file); "deep-rmdir/cc/ll/foo/bar/baz" => (file); "deep-rmdir/cc/mm/foo/bar/baz" => (file); "deep-rmdir/cc/nn/foo/bar/baz" => (file); "deep-rmdir/cc/oo/foo/bar/baz" => (file); "deep-rmdir/cc/pp/foo/bar/baz" => (file); "deep-rmdir/cc/qq/foo/bar/baz" => (file); "deep-rmdir/cc/rr/foo/bar/baz" => (file); "deep-rmdir/cc/ss/foo/bar/baz" => (file); "deep-rmdir/cc/tt/foo/bar/baz" => (file); "deep-rmdir/cc/uu/foo/bar/baz" => (file); "deep-rmdir/cc/vv/foo/bar/baz" => (file); "deep-rmdir/cc/ww/foo/bar/baz" => (file); "deep-rmdir/cc/xx/foo/bar/baz" => (file); "deep-rmdir/cc/yy/foo/bar/baz" => (file); "deep-rmdir/cc/zz/foo/bar/baz" => (file); }) } pub(crate) fn mask_nosymfollow(root: &Path) -> Result<(), Error> { // Apply NOSYMFOLLOW for some subpaths. let root_prefix = root.to_path_buf(); // NOSYMFOLLOW applied to a single symlink itself. tests_common::mount( root_prefix.join("nosymfollow/badlink"), MountType::RebindWithFlags { flags: tests_common::NOSYMFOLLOW, }, )?; // NOSYMFOLLOW applied to a directory. tests_common::mount( root_prefix.join("nosymfollow/nosymdir/dir"), MountType::RebindWithFlags { flags: tests_common::NOSYMFOLLOW, }, )?; // NOSYMFOLLOW cleared to paths under a directory. (To clear // NOSYMFOLLOW we just remount with no flags.) tests_common::mount( root_prefix.join("nosymfollow/nosymdir/dir/goodlink"), MountType::RebindWithFlags { flags: MountFlags::empty(), }, )?; tests_common::mount( root_prefix.join("nosymfollow/nosymdir/dir/foo/yessymdir"), MountType::RebindWithFlags { flags: MountFlags::empty(), }, )?; Ok(()) } pub(crate) fn create_race_tree() -> Result<(TempDir, PathBuf), Error> { let tmpdir = create_tree! { // Our root. "root" => (dir); // The path that the race tests will try to operate on. "root/a/b/c/d" => (dir); // Symlinks to swap that are semantically equivalent but should also // trigger breakout errors. "root/b-link" => (symlink -> "../b/../b/../b/../b/../b/../b/../b/../b/../b/../b/../b/../b/../b"); "root/c-link" => (symlink -> "../../b/c/../../b/c/../../b/c/../../b/c/../../b/c/../../b/c/../../b/c/../../b/c/../../b/c"); // Bad paths that should result in an error. "root/bad-link1" => (symlink -> "/non/exist"); "root/bad-link2" => (symlink -> "/file/non/exist"); // Try to attack the root to get access to /etc/passwd. "root/etc/passwd" => (file); "root/etc-target/passwd" => (file); "root/etc-attack-rel-link" => (symlink -> "../../../../../../../../../../../../../../../../../../etc"); "root/etc-attack-abs-link" => (symlink -> "/../../../../../../../../../../../../../../../../../../etc"); "root/passwd-attack-rel-link" => (symlink -> "../../../../../../../../../../../../../../../../../../etc/passwd"); "root/passwd-attack-abs-link" => (symlink -> "/../../../../../../../../../../../../../../../../../../etc/passwd"); // File to swap a directory with. "root/file" => (file); // Directory outside the root we can swap with. "outsideroot" => (dir); }; let root: PathBuf = [tmpdir.as_ref(), Path::new("root")].iter().collect(); Ok((tmpdir, root)) } pathrs-0.2.1/src/tests/test_procfs.rs000064400000000000000000000662251046102023000157720ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #[cfg(feature = "capi")] use crate::tests::capi::{CapiProcfsHandle, CapiProcfsHandleFd}; use crate::{ error::ErrorKind, flags::OpenFlags, procfs::{ProcfsBase, ProcfsHandle, ProcfsHandleBuilder}, resolvers::procfs::ProcfsResolver, syscalls, }; use utils::ExpectedResult; use anyhow::Error; use rustix::mount::OpenTreeFlags; macro_rules! procfs_tests { // Create the actual test functions. ($(#[$meta:meta])* @rust-fn [<$func_prefix:ident $test_name:ident>] $procfs_inst:block . $procfs_op:ident ($($args:expr),*) => (over_mounts: $over_mounts:expr, error: $expect_error:expr) ;) => { paste::paste! { #[test] $(#[$meta])* fn []() -> Result<(), Error> { utils::[]( || $procfs_inst, $($args,)* $over_mounts, ExpectedResult::$expect_error, ) } #[test] $(#[$meta])* fn []() -> Result<(), Error> { if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } utils::[]( || { let mut proc = $procfs_inst ?; // Force openat2 resolver. proc.resolver = ProcfsResolver::Openat2; Ok(proc) }, $($args,)* $over_mounts, ExpectedResult::$expect_error, ) } #[test] $(#[$meta])* fn []() -> Result<(), Error> { // This test only makes sense if openat2 is supported (i.e., the // default resolver is openat2 -- otherwise the default test // already tested this case). if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } utils::[]( || { let mut proc = $procfs_inst ?; // Force opath resolver. proc.resolver = ProcfsResolver::RestrictedOpath; Ok(proc) }, $($args,)* $over_mounts, ExpectedResult::$expect_error, ) } } }; // Create the actual test function for the C API. ($(#[$meta:meta])* @capi-fn [<$func_prefix:ident $test_name:ident>] $procfs_inst:block . $procfs_op:ident ($($args:expr),*) => (over_mounts: $over_mounts:expr, error: $expect_error:expr) ;) => { paste::paste! { #[test] #[cfg(feature = "capi")] $(#[$meta])* fn []() -> Result<(), Error> { utils::[]( || $procfs_inst, $($args,)* $over_mounts, ExpectedResult::$expect_error, ) } } }; // Create a test for each ProcfsHandle::new_* method. ($(#[$meta:meta])* @impl $test_name:ident $procfs_var:ident . $procfs_op:ident ($($args:tt)*) => ($($tt:tt)*) ;) => { procfs_tests! { $(#[$meta])* @rust-fn [] { ProcfsHandle::new() }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { $(#[$meta])* @rust-fn [] { ProcfsHandleBuilder::new() .unmasked() .build() }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { $(#[$meta])* #[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))] @rust-fn [] { ProcfsHandle::new_fsopen(false) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { $(#[$meta])* #[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))] @rust-fn [] { ProcfsHandle::new_fsopen(true) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { $(#[$meta])* #[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))] @rust-fn [] { ProcfsHandle::new_open_tree(OpenTreeFlags::OPEN_TREE_CLONE) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { $(#[$meta])* #[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))] @rust-fn [] { ProcfsHandle::new_open_tree(OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::AT_RECURSIVE) }.$procfs_op($($args)*) => (over_mounts: true, $($tt)*); } procfs_tests! { $(#[$meta])* @rust-fn [] { ProcfsHandle::new_unsafe_open() }.$procfs_op($($args)*) => (over_mounts: true, $($tt)*); } // Assume that ProcfsHandle::new() is fsopen(2)-based. // // TODO: Figure out the fd type of ProcfsHandle::new(). In principle we // would expect to be able to do fsopen(2) (otherwise the fsopen(2) // tests will fail) but it would be nice to avoid possible spurious // errors. procfs_tests! { $(#[$meta])* @capi-fn [] { Ok(CapiProcfsHandle) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } procfs_tests! { $(#[$meta])* @capi-fn [] { CapiProcfsHandleFd::new_unmasked() }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*); } }; // procfs_tests! { abc: readlink(ProcfsBase::ProcRoot, "foo") => (error: ExpectedResult::Some(ErrorKind::OsError(Some(libc::ENOENT)))) } ($(#[cfg($ignore_meta:meta)])* $test_name:ident : readlink (ProcfsBase::$base:ident $(($pid:literal))?, $path:expr ) => ($($tt:tt)*)) => { paste::paste! { procfs_tests! { $(#[cfg_attr(not($ignore_meta), ignore, allow(unused_attributes))])* @impl [<$base:lower $($pid)* _readlink_ $test_name>] procfs.readlink(ProcfsBase::$base $(($pid))*, $path) => ($($tt)*); } } }; // procfs_tests! { xyz: open(ProcfsBase::ProcSelf, "fd", O_DIRECTORY) => (error: None) } ($(#[cfg($ignore_meta:meta)])* $test_name:ident : open (ProcfsBase::$base:ident $(($pid:literal))?, $path:expr, $($flag:ident)|* ) => ($($tt:tt)*)) => { paste::paste! { procfs_tests! { $(#[cfg_attr(not($ignore_meta), ignore, allow(unused_attributes))])* @impl [<$base:lower $($pid)* _open_ $test_name>] procfs.open(ProcfsBase::$base $(($pid))*, $path, $(OpenFlags::$flag)|*) => ($($tt)*); } } }; // procfs_tests! { def: open_follow(ProcfsBase::ProcSelf, "exe", O_DIRECTORY | O_PATH) => (error: ErrorKind::OsError(Some(libc::ENOTDIR) } ($(#[cfg($ignore_meta:meta)])* $test_name:ident : open_follow (ProcfsBase::$base:ident $(($pid:literal))?, $path:expr, $($flag:ident)|* ) => ($($tt:tt)*)) => { paste::paste! { procfs_tests! { $(#[cfg_attr(not($ignore_meta), ignore, allow(unused_attributes))])* @impl [<$base:lower $($pid)* _open_follow_ $test_name>] procfs.open_follow(ProcfsBase::$base $(($pid))*, $path, $(OpenFlags::$flag)|*) => ($($tt)*); } } }; // procfs_tests! { xyz: open(self, "fd", O_DIRECTORY) => (error: None) } // procfs_tests! { abc: open(ProcfsBase::ProcPid(1), "stat", O_RDONLY) => (error: None) } // procfs_tests! { def: open_follow(self, "exe", O_DIRECTORY | O_PATH) => (error: ErrorKind::OsError(Some(libc::ENOTDIR) } // procfs_tests! { abc: readlink(ProcfsBase::ProcRoot, "foo") => (error: ExpectedResult::Some(ErrorKind::OsError(Some(libc::ENOENT)))) } ($(#[$meta:meta])* $test_name:ident : $func:ident (self, $($args:tt)*) => ($($tt:tt)*)) => { paste::paste! { procfs_tests! { $(#[$meta])* $test_name : $func (ProcfsBase::ProcSelf, $($args)*) => ($($tt)*); } procfs_tests! { $(#[$meta])* $test_name : $func (ProcfsBase::ProcThreadSelf, $($args)*) => ($($tt)*); } } }; ($($(#[$meta:meta])* $test_name:ident : $func:ident ($($args:tt)*) => ($($res:tt)*) );* $(;)?) => { paste::paste! { $( $(#[$meta])* procfs_tests!{$test_name : $func ( $($args)* ) => ($($res)*) } )* } } } procfs_tests! { // Non-procfs overmount. tmpfs_dir: open(self, "net", O_DIRECTORY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV)))); tmpfs_dir: open_follow(self, "net", O_DIRECTORY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV)))); // No overmounts. nomount: open(self, "attr/current", O_RDONLY) => (error: Ok); nomount: open_follow(self, "attr/current", O_RDONLY) => (error: Ok); nomount_dir: open(self, "attr", O_RDONLY) => (error: Ok); nomount_dir: open_follow(self, "attr", O_RDONLY) => (error: Ok); nomount_dir_odir: open(self, "attr", O_DIRECTORY|O_RDONLY) => (error: Ok); nomount_dir_odir: open_follow(self, "attr", O_DIRECTORY|O_RDONLY) => (error: Ok); nomount_dir_trailing_slash: open(self, "attr/", O_RDONLY) => (error: Ok); nomount_dir_trailing_slash: open_follow(self, "attr/", O_RDONLY) => (error: Ok); global_nomount: open(ProcfsBase::ProcRoot, "filesystems", O_RDONLY) => (error: Ok); global_nomount: readlink(ProcfsBase::ProcRoot, "mounts") => (error: Ok); pid1_nomount: open(ProcfsBase::ProcPid(1), "stat", O_RDONLY) => (error: Ok); pid1_nomount: open_follow(ProcfsBase::ProcPid(1), "stat", O_RDONLY) => (error: Ok); #[cfg(feature = "_test_as_root")] pid1_nomount: readlink(ProcfsBase::ProcPid(1), "cwd") => (error: Ok); #[cfg(not(feature = "_test_as_root"))] pid1_nomount: readlink(ProcfsBase::ProcPid(1), "cwd") => (error: Err(ErrorKind::OsError(Some(libc::EACCES)))); // Procfs regular file overmount. proc_file_wr: open(self, "attr/exec", O_WRONLY) => (error: ErrOvermount("/proc/self/attr/exec", ErrorKind::OsError(Some(libc::EXDEV)))); proc_file_wr: open_follow(self, "attr/exec", O_WRONLY) => (error: ErrOvermount("/proc/self/attr/exec", ErrorKind::OsError(Some(libc::EXDEV)))); proc_file_rd: open(self, "mountinfo", O_RDONLY) => (error: ErrOvermount("/proc/self/mountinfo", ErrorKind::OsError(Some(libc::EXDEV)))); proc_file_rd: open_follow(self, "mountinfo", O_RDONLY) => (error: ErrOvermount("/proc/self/mountinfo", ErrorKind::OsError(Some(libc::EXDEV)))); global_cpuinfo_rd: open(ProcfsBase::ProcRoot, "cpuinfo", O_RDONLY) => (error: ErrOvermount("/proc/cpuinfo", ErrorKind::OsError(Some(libc::EXDEV)))); global_meminfo_rd: open(ProcfsBase::ProcRoot, "meminfo", O_RDONLY) => (error: ErrOvermount("/proc/meminfo", ErrorKind::OsError(Some(libc::EXDEV)))); global_fs_dir: open(ProcfsBase::ProcRoot, "fs", O_RDONLY|O_DIRECTORY) => (error: ErrOvermount("/proc/fs", ErrorKind::OsError(Some(libc::EXDEV)))); // Regular symlinks. symlink: open(ProcfsBase::ProcRoot, "mounts", O_PATH) => (error: Ok); symlink: open_follow(ProcfsBase::ProcRoot, "mounts", O_RDONLY) => (error: Ok); symlink: readlink(ProcfsBase::ProcRoot, "mounts") => (error: Ok); symlink_parentdir: open(ProcfsBase::ProcRoot, "self/mounts", O_PATH) => (error: Ok); symlink_parentdir: open_follow(ProcfsBase::ProcRoot, "self/mounts", O_RDONLY) => (error: Ok); symlink_parentdir: readlink(ProcfsBase::ProcRoot, "self/cwd") => (error: Ok); symlink_overmount: open(ProcfsBase::ProcRoot, "net", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP)))); symlink_overmount: open_follow(ProcfsBase::ProcRoot, "net", O_PATH|O_DIRECTORY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV)))); symlink_overmount: readlink(ProcfsBase::ProcRoot, "net") => (error: Ok); symlink_parentdir_overmount: open(ProcfsBase::ProcRoot, "net/unix", O_RDONLY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV)))); symlink_parentdir_overmount: open_follow(ProcfsBase::ProcRoot, "net/unix", O_RDONLY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV)))); // Magic-links with no overmount. magiclink_nomount: open(self, "cwd", O_PATH) => (error: Ok); magiclink_nomount: open_follow(self, "cwd", O_RDONLY) => (error: Ok); magiclink_nomount: readlink(self, "cwd") => (error: Ok); magiclink_nomount_fd1: readlink(self, "fd/1") => (error: Ok); magiclink_nomount_fd2: readlink(self, "fd/2") => (error: Ok); // Magic-links with overmount. magiclink_exe: open(self, "exe", O_PATH) => (error: ErrOvermount("/proc/self/exe", ErrorKind::OsError(Some(libc::EXDEV)))); magiclink_exe: open_follow(self, "exe", O_RDONLY) => (error: ErrOvermount("/proc/self/exe", ErrorKind::OsError(Some(libc::EXDEV)))); magiclink_exe: readlink(self, "exe") => (error: ErrOvermount("/proc/self/exe", ErrorKind::OsError(Some(libc::EXDEV)))); magiclink_fd0: open(self, "fd/0", O_PATH) => (error: ErrOvermount("/proc/self/fd/0", ErrorKind::OsError(Some(libc::EXDEV)))); magiclink_fd0: open_follow(self, "fd/0", O_RDONLY) => (error: ErrOvermount("/proc/self/fd/0", ErrorKind::OsError(Some(libc::EXDEV)))); magiclink_fd0: readlink(self, "fd/0") => (error: ErrOvermount("/proc/self/fd/0", ErrorKind::OsError(Some(libc::EXDEV)))); // Behaviour-related testing. nondir_odir: open_follow(self, "environ", O_DIRECTORY|O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR)))); nondir_trailing_slash: open_follow(self, "environ/", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR)))); proc_cwd_odir: open_follow(self, "cwd", O_DIRECTORY|O_RDONLY) => (error: Ok); proc_cwd_trailing_slash: open_follow(self, "cwd/", O_RDONLY) => (error: Ok); proc_fdlink_odir: open_follow(self, "fd//1", O_DIRECTORY|O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR)))); proc_fdlink_trailing_slash: open_follow(self, "fd//1/", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR)))); // TODO: root can always open procfs files with O_RDWR even if writes fail. // proc_nowrite: open(self, "status", O_RDWR) => (error: Err(ErrorKind::OsError(Some(libc::EACCES)))); proc_dotdot_escape: open_follow(self, "../..", O_PATH) => (error: Err(ErrorKind::OsError(Some(libc::EXDEV)))); // TODO: openat2(self, RESOLVE_BENEATH) does not block all ".." components unlike // our custom resolver, so "fd/.." has different results based on the // resolver. // proc_dotdot_escape: open_follow(self, "fd/../..", O_PATH) => (error: Err(ErrorKind::OsError(Some(libc::EXDEV)))); // proc_dotdot: open_follow(self, "fd/..", O_PATH) => (error: Err(ErrorKind::OsError(Some(libc::EXDEV)))); proc_magic_component: open(self, "root/etc/passwd", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP)))); proc_magic_component: open_follow(self, "root/etc/passwd", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP)))); proc_magic_component: readlink(self, "root/etc/passwd") => (error: Err(ErrorKind::OsError(Some(libc::ELOOP)))); proc_sym_onofollow: open(self, "fd/1", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP)))); proc_sym_opath_onofollow: open(self, "fd/1", O_PATH) => (error: Ok); proc_sym_odir_opath_onofollow: open(self, "fd/1", O_DIRECTORY|O_PATH) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR)))); proc_dir_odir_opath_onofollow: open(self, "fd", O_DIRECTORY|O_PATH) => (error: Ok); } mod utils { use std::{ collections::HashSet, fmt::Debug, path::{Path, PathBuf}, }; use crate::{ error::ErrorKind, flags::OpenFlags, procfs::ProcfsBase, syscalls, tests::{ common::{self as tests_common, MountType}, traits::{ErrorImpl, ProcfsHandleImpl}, }, utils, }; use anyhow::{Context, Error}; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(super) enum ExpectedResult { Ok, Err(ErrorKind), ErrOvermount(&'static str, ErrorKind), } fn check_proc_error( res: Result, over_mounts: &HashSet, expected: ExpectedResult, ) -> Result<(), Error> { let want_error = match expected { ExpectedResult::Ok => Ok(()), ExpectedResult::Err(kind) => Err(kind), ExpectedResult::ErrOvermount(path, kind) => { if over_mounts.contains(Path::new(path)) { Err(kind) } else { Ok(()) } } }; tests_common::check_err(&res, &want_error) .with_context(|| format!("unexpected result for overmounts={over_mounts:?}"))?; Ok(()) } fn in_host_mnt_ns(expected: ExpectedResult, func: F) -> Result<(), Error> where T: Debug, E: ErrorImpl, F: FnOnce() -> Result, { // Non-mnt-ns tests don't have overmounts configured. let over_mounts = HashSet::new(); let res = func(); check_proc_error(res, &over_mounts, expected)?; Ok(()) } // Since Linux 6.12, the kernel no longer allows us to mount on top of // certain procfs paths. This is a net good for us, because it makes certain // attacks libpathrs needs to defend against no longer possible, but we // still want to test for these attacks in CI. // // For more information, see d80b065bb172 ('Merge patch series "proc: // restrict overmounting of ephemeral entities"'). In future kernel versions // these restrictions will be even more restrictive (hopefully one day // including all of /proc//*). const PROCFS_MAYBE_UNMOUNTABLE: &[&str] = &[ // 3836b31c3e71 ("proc: block mounting on top of /proc//map_files/*") "/proc/self/map_files/", "/proc/thread-self/map_files/", // 74ce208089f4 ("proc: block mounting on top of /proc//fd/*") "/proc/self/fd/", "/proc/thread-self/fd/", // cf71eaa1ad18 ("proc: block mounting on top of /proc//fdinfo/*") "/proc/self/fdinfo/", "/proc/thread-self/fdinfo/", ]; fn try_mount( over_mounts: &mut HashSet, dst: impl AsRef, ty: MountType, ) -> Result<(), Error> { let dst = dst.as_ref(); let might_fail = { let dst = dst.to_str().expect("our path strings are valid utf8"); PROCFS_MAYBE_UNMOUNTABLE .iter() .any(|prefix| dst.starts_with(prefix)) }; match (tests_common::mount(dst, ty), might_fail) { (Ok(_), _) => { over_mounts.insert(dst.to_path_buf()); } (Err(_), true) => (), (Err(err), false) => Err(err)?, }; Ok(()) } fn in_mnt_ns_with_overmounts( are_over_mounts_visible: bool, expected: ExpectedResult, func: F, ) -> Result<(), Error> where T: Debug, E: ErrorImpl, F: FnOnce() -> Result, { tests_common::in_mnt_ns(|| { let mut over_mounts = HashSet::new(); // Add some overmounts to /proc. try_mount( &mut over_mounts, "/proc/fs", // Non-procfs file. MountType::Tmpfs, )?; try_mount( &mut over_mounts, "/proc/meminfo", // Non-procfs file. MountType::Bind { src: "/dev/null".into(), }, )?; try_mount( &mut over_mounts, "/proc/cpuinfo", // A bind-mount of a real procfs file than can have custom data. MountType::Bind { src: "/proc/1/environ".into(), }, )?; // Add some overmounts to /proc/self and /proc/thread-self. for prefix in ["/proc/self", "/proc/thread-self"] { let prefix = PathBuf::from(prefix); try_mount( &mut over_mounts, prefix.join("net"), // Non-procfs mount. MountType::Tmpfs, )?; try_mount( &mut over_mounts, prefix.join("attr/exec"), // A bind-mount of a real procfs file that ignores all // writes. MountType::Bind { src: "/proc/1/sched".into(), }, )?; try_mount( &mut over_mounts, prefix.join("mountinfo"), // A bind-mount of a real procfs file that can have custom // data. MountType::Bind { src: "/proc/1/environ".into(), }, )?; // Magic-link overmounts. try_mount( &mut over_mounts, prefix.join("exe"), MountType::Bind { src: "/proc/1/fd/0".into(), }, )?; try_mount( &mut over_mounts, prefix.join("fd/0"), MountType::Bind { src: "/proc/1/exe".into(), }, )?; // TODO: Add some tests for mounts on top of /proc/self. } // If overmounts are not visible, clear the hashset. if !are_over_mounts_visible { over_mounts.clear(); } let res = func(); check_proc_error(res, &over_mounts, expected)?; Ok(()) }) } fn check_func( are_over_mounts_visible: bool, expected: ExpectedResult, func: F, ) -> Result<(), Error> where T: Debug, E: ErrorImpl, F: FnOnce() -> Result, { if syscalls::geteuid() == 0 { in_mnt_ns_with_overmounts(are_over_mounts_visible, expected, func) } else { in_host_mnt_ns(expected, func) } } pub(super) fn check_proc_open( proc_fn: ProcFn, base: ProcfsBase, path: impl AsRef, oflags: impl Into, are_over_mounts_visible: bool, expected: ExpectedResult, ) -> Result<(), Error> where Proc: ProcfsHandleImpl, ProcFn: FnOnce() -> Result, { check_func( are_over_mounts_visible, expected, || -> Result<_, Proc::Error> { let oflags = oflags.into(); let proc = proc_fn()?; let f = proc.open(base, path, oflags)?; // Check that the flags are what a user would expect. let mut want_oflags = oflags; // O_NOFOLLOW is always set by open. want_oflags.insert(OpenFlags::O_NOFOLLOW); // O_DIRECTORY is *not* set automatically! tests_common::check_oflags(&f, want_oflags).expect("check oflags"); Ok(f) }, ) } pub(super) fn check_proc_open_follow( proc_fn: ProcFn, base: ProcfsBase, path: impl AsRef, oflags: impl Into, are_over_mounts_visible: bool, expected: ExpectedResult, ) -> Result<(), Error> where Proc: ProcfsHandleImpl, ProcFn: FnOnce() -> Result, { check_func( are_over_mounts_visible, expected, || -> Result<_, Proc::Error> { let path = path.as_ref(); let oflags = oflags.into(); let proc = proc_fn()?; let f = proc.open_follow(base, path, oflags)?; // Check that the flags are what a user would expect. let mut want_oflags = oflags; let (_, trailing_slash) = utils::path_strip_trailing_slash(path); // If the path has a trailing slash then open(_follow) will // insert O_DIRECTORY automatically. if trailing_slash { want_oflags.insert(OpenFlags::O_DIRECTORY); } tests_common::check_oflags(&f, want_oflags).expect("check oflags"); Ok(f) }, ) } pub(super) fn check_proc_readlink( proc_fn: ProcFn, base: ProcfsBase, path: impl AsRef, are_over_mounts_visible: bool, expected: ExpectedResult, ) -> Result<(), Error> where Proc: ProcfsHandleImpl, ProcFn: FnOnce() -> Result, { check_func(are_over_mounts_visible, expected, || { proc_fn()?.readlink(base, path) }) } } pathrs-0.2.1/src/tests/test_race_resolve_partial.rs000064400000000000000000001232651046102023000206610ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::ErrorKind, flags::ResolverFlags, resolvers::{PartialLookup, ResolverBackend}, syscalls, tests::common as tests_common, Root, }; use std::{os::unix::io::AsFd, sync::mpsc, thread}; use anyhow::Error; macro_rules! resolve_race_tests { // resolve_race_tests! { // test_ok: resolve_partial(...) => Ok(("path", Some("remaining", ErrorKind::...)), libc::S_IF...)); // test_err: resolve_partial(...) => Err(ErrorKind::...); // } ([$root_dir:expr] fn $test_name:ident (mut $root_var:ident : Root) $body:block) => { paste::paste! { #[test] fn []() -> Result<(), Error> { let (tmpdir, root_dir) = $root_dir; let mut $root_var = Root::open(&root_dir)?; assert_eq!( $root_var.resolver_backend(), ResolverBackend::default(), "ResolverBackend not the default despite not being configured" ); { $body } // Make sure tmpdir is not dropped earlier. let _tmpdir = tmpdir; // Make sure the mut $root_var doesn't give us a warning. $root_var.set_resolver_flags($root_var.resolver_flags()); Ok(()) } #[test] fn []() -> Result<(), Error> { let (tmpdir, root_dir) = $root_dir; let mut $root_var = Root::open(&root_dir)?; $root_var.set_resolver_backend(ResolverBackend::KernelOpenat2); assert_eq!( $root_var.resolver_backend(), ResolverBackend::KernelOpenat2, "incorrect ResolverBackend despite using set_resolver_backend" ); if !$root_var.resolver_backend().supported() { // Skip if not supported. return Ok(()); } { $body } // Make sure tmpdir is not dropped earlier. let _tmpdir = tmpdir; Ok(()) } #[test] fn []() -> Result<(), Error> { // This test only makes sense if openat2 is supported (i.e., the // default resolver is openat2 -- otherwise the default test // already tested this case). if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } let (tmpdir, root_dir) = $root_dir; let mut $root_var = Root::open(&root_dir)?; $root_var.set_resolver_backend(ResolverBackend::EmulatedOpath); assert_eq!( $root_var.resolver_backend(), ResolverBackend::EmulatedOpath, "incorrect ResolverBackend despite using set_resolver_backend" ); // EmulatedOpath is always supported. assert!( $root_var.resolver_backend().supported(), "emulated opath is always supported", ); { $body } // Make sure tmpdir is not dropped earlier. let _tmpdir = tmpdir; Ok(()) } } }; (@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()?] fn [<$op_name _ $test_name>](mut root: Root) { root.set_resolver_flags($rflags); thread::scope(|s| -> Result<_, Error> { use utils::RenameStateMsg; let root_fd = root.as_fd(); let (ctl_tx, ctl_rx) = mpsc::sync_channel(0); s.spawn(move || { utils::rename_exchange( ctl_rx, root_fd, $rename_a, $rename_b, ) }); // If openat2 is disabled, the race tests get really // slow (especially in CI), so just reduce the number of // rounds to something far more sane. // TODO: Maybe we should do more runs locally than in // CI, since GHA boxes are quite slow compared to my // laptop? let test_retries = if *syscalls::OPENAT2_IS_SUPPORTED { 20000 } else { 1000 }; let expected = vec![ $($expected)* ]; for _ in 0..test_retries { // Make sure the rename thread isn't paused. ctl_tx .send(RenameStateMsg::Run) .expect("should be able to send run signal to rename thread"); utils::[]( &root, &ctl_tx, $path, $no_follow_trailing, &expected, )?; } // Make sure a kill signal gets sent. When the tx handle // is dropped, the rename loop should die anyway but // this just makes sure. We have to ignore any errors // because if the rename loop is already dead (and so rx // has been dropped) then send will return an error. let _ = ctl_tx.send(RenameStateMsg::Quit); Ok(()) })?; } } } }; (@impl [$($race_task:tt)*] $test_name:ident $op_name:ident ($path:expr, rflags = $($rflag:ident)|+) => { $($expected:tt)* } ) => { resolve_race_tests! { @impl [$($race_task)*] $test_name $op_name($path, $(ResolverFlags::$rflag)|*, false) => { $($expected)* } } }; (@impl [$($race_task:tt)*] $test_name:ident $op_name:ident ($path:expr, no_follow_trailing = $no_follow_trailing:expr) => { $($expected:tt)* } ) => { resolve_race_tests! { @impl [$($race_task)*] $test_name $op_name($path, ResolverFlags::empty(), $no_follow_trailing) => { $($expected)* } } }; (@impl [$($race_task:tt)*] $test_name:ident $op_name:ident ($path:expr) => { $($expected:tt)* } ) => { resolve_race_tests! { @impl [$($race_task)*] $test_name $op_name($path, ResolverFlags::empty(), false) => { $($expected)* } } }; // NOTE: Because of the way that the repetition is nested, we need to make // the $race_task metavariable a basic tt ($($race_task:tt)* fails when we // try to substitute it). Luckily we can just use :tt for blocks of the form // [] and then re-parse it in the individual rule. ($($test_prefix:ident $race_task:tt { $($test_name:ident : $op_name:ident ($($args:tt)*) => { $($expected:tt)* } );* $(;)? });* $(;)?) => { $( $( paste::paste! { resolve_race_tests! { @impl $race_task [<$test_prefix _ $test_name>] $op_name ($($args)*) => { $($expected)* } } } )* )* }; } resolve_race_tests! { // Swap a directory component with a symlink during lookup. swap_dir_link1 ["a/b" <=> "b-link"] { basic: resolve_partial("/a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; dotdot1: resolve_partial("/a/b/../b/../b/../b/../b/../b/../b/c/d/../d/../d/../d/../d/../d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "../d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "../b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; dotdot2: resolve_partial("/a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "../d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "../c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; }; swap_dir_link2 ["a/b/c" <=> "c-link"] { basic: resolve_partial("/a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; dotdot: resolve_partial("/a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "../d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "../c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; }; // Swap a directory with a non-directory. swap_dir_file ["a/b" <=> "file"] { basic: resolve_partial("/a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), // Hit the file during lookup. Ok(PartialLookup::Partial { handle: ("/file", libc::S_IFREG), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), }; dotdot: resolve_partial("a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "../d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "../c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), // Hit the file during lookup. Ok(PartialLookup::Partial { handle: ("/file", libc::S_IFREG), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), }; }; // Swap a directory with a dangling symlink. swap_dir_badlink_enoent ["a/b" <=> "bad-link1"] { basic: resolve_partial("/a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // Hit the dangling symlink (this makes us stop above it at "/a"). Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; dotdot: resolve_partial("a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "../d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "../c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // Hit the dangling symlink (this makes us stop above it at "/a"). Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; }; swap_dir_badlink_enotdir ["a/b" <=> "bad-link2"] { basic: resolve_partial("/a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), // Hit the dangling symlink (this makes us stop above it at "/a"). Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), }; dotdot: resolve_partial("a/b/c/../c/../c/../c/../c/../c/../c/d/../d/../d/../d/../d/../d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "../d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "../c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), // Hit the dangling symlink (this makes us stop above it at "/a"). Ok(PartialLookup::Partial { handle: ("/a", libc::S_IFDIR), remaining: "b/c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }), }; }; // Swap a directory with a symlink that would cause a naive resolver to // escape the root. This is effectively CVE-2018-15664. swap_dir_attack_link ["etc-target" <=> "etc-attack-rel-link"] { basic: resolve_partial("/etc-target/passwd") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Complete(("/etc-target/passwd", libc::S_IFREG))), // We successfully resolved the swapped symlink inside the root. Ok(PartialLookup::Complete(("/etc/passwd", libc::S_IFREG))), }; dotdot: resolve_partial("/etc-target/../etc-target/../etc-target/../etc-target/../etc-target/../etc-target/passwd") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Complete(("/etc-target/passwd", libc::S_IFREG))), // We successfully resolved the swapped symlink inside the root. Ok(PartialLookup::Complete(("/etc/passwd", libc::S_IFREG))), }; }; swap_dir_attack_abs_link ["etc-target" <=> "etc-attack-abs-link"] { basic: resolve_partial("/etc-target/passwd") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Complete(("/etc-target/passwd", libc::S_IFREG))), // We successfully resolved the swapped symlink inside the root. Ok(PartialLookup::Complete(("/etc/passwd", libc::S_IFREG))), }; dotdot: resolve_partial("/etc-target/../etc-target/../etc-target/../etc-target/../etc-target/../etc-target/passwd") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Complete(("/etc-target/passwd", libc::S_IFREG))), // We successfully resolved the swapped symlink inside the root. Ok(PartialLookup::Complete(("/etc/passwd", libc::S_IFREG))), }; }; // Move the root to a different location. This should not affect lookups move_root ["." <=> "../outsideroot"] { basic: resolve_partial("a/b/c/d/e") => { // Breakout detected. //Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; dotdot: resolve_partial("a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; // TODO: dotdot_extra with 10 copies of "b/c/d/../../../". }; // Try to move a directory we are walking inside to be outside the root. // A naive "is .. the root" implementation would be tripped up by this. swap_dir_outside_root ["a/b" <=> "../outsideroot"] { basic: resolve_partial("a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // We could also land in the "outsideroot" path. This is okay // because there was a moment when the directory was inside the // root, and the attacker just moved it outside the root. We know // that neither resolver will allow us to walk into ".." in this // scenario, so we should be okay. Ok(PartialLookup::Partial { handle: ("../outsideroot/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("../outsideroot/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("../outsideroot", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; dotdot: resolve_partial("a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/../../a/b/c/d/e") => { // Breakout detected. Err(ErrorKind::SafetyViolation), // We successfully resolved the path as if there wasn't a race. Ok(PartialLookup::Partial { handle: ("/a/b/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // We could also land in the "outsideroot" path. This is okay // because there was a moment when the directory was inside the // root, and the attacker just moved it outside the root. We know // that neither resolver will allow us to walk into ".." in this // scenario, so we should be okay. Ok(PartialLookup::Partial { handle: ("../outsideroot/c/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), // There was a race during the walk-back logic, which resulted in an // error but then the path was replaced back when walking back to // find the "last good" path. Ok(PartialLookup::Partial { handle: ("../outsideroot/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b/c", libc::S_IFDIR), remaining: "d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("../outsideroot", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), Ok(PartialLookup::Partial { handle: ("/a/b", libc::S_IFDIR), remaining: "c/d/e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }), }; }; } mod utils { use crate::{ error::ErrorKind, flags::RenameFlags, resolvers::PartialLookup, syscalls, tests::{ common::{self as tests_common, AsError, LookupResult}, traits::RootImpl, }, utils::FdExt, Root, }; use std::{ os::unix::{fs::MetadataExt, io::AsFd}, path::{Path, PathBuf}, sync::mpsc::{Receiver, RecvError, SyncSender, TryRecvError}, }; use anyhow::Error; use path_clean::PathClean; pub(super) enum RenameStateMsg { Run, Pause, Quit, } pub(super) fn rename_exchange( ctl_rx: Receiver, root: impl AsFd, path1: impl AsRef, path2: impl AsRef, ) { let root = root.as_fd(); let (path1, path2) = (path1.as_ref(), path2.as_ref()); // One of the paths might be ".", which will give us -EBUSY from // renameat. We can just use full paths here (we are the only thing // doing renames here, and the root path is known to be safe to us). let root_path = root .as_unsafe_path_unchecked() .expect("should be able to get real path"); let (root, path1, path2) = ( syscalls::AT_FDCWD, [&root_path, path1].iter().collect::().clean(), [&root_path, path2].iter().collect::().clean(), ); 'rename: loop { match ctl_rx.try_recv() { Ok(RenameStateMsg::Quit) | Err(TryRecvError::Disconnected) => break 'rename, Ok(RenameStateMsg::Run) | Err(TryRecvError::Empty) => (), Ok(RenameStateMsg::Pause) => { // Wait for a "Run" message. loop { match ctl_rx.recv() { Ok(RenameStateMsg::Quit) | Err(RecvError) => break 'rename, Ok(RenameStateMsg::Pause) => continue, Ok(RenameStateMsg::Run) => break, } } } } syscalls::renameat2(root, &path1, root, &path2, RenameFlags::RENAME_EXCHANGE) .expect("swap A <-> B should work"); syscalls::renameat2(root, &path1, root, &path2, RenameFlags::RENAME_EXCHANGE) .expect("swap B <-> A back should work"); } } pub(super) fn check_root_race_resolve_partial( root: &Root, ctl_tx: &SyncSender, unsafe_path: impl AsRef, no_follow_trailing: bool, allowed_results: &[Result, ErrorKind>], ) -> Result<(), Error> { let unsafe_path = unsafe_path.as_ref(); // Resolve the path. let result = loop { let res = root .resolver() .resolve_partial(root, unsafe_path, no_follow_trailing); if !res .as_error() .map(|err| err.can_retry()) .unwrap_or_default() { break res; } }; // Pause the rename attack so that we can get the "unswapped" path with // as_unsafe_path_unchecked(). ctl_tx .send(RenameStateMsg::Pause) .expect("should be able to pause rename attack"); let root_dir = root.as_unsafe_path_unchecked()?; // Convert the handle to something useful for our tests. let result = result.map(|lookup_result| { let (path, file_type) = { let file = lookup_result.as_inner_handle(); ( file.as_unsafe_path_unchecked() .expect("should be able to get real path of handle"), file.metadata() .expect("should be able to fstat handle") .mode() & libc::S_IFMT, ) }; match lookup_result { PartialLookup::Complete(_) => PartialLookup::Complete((path, file_type)), PartialLookup::Partial { handle: _, remaining, last_error, } => PartialLookup::Partial { handle: (path, file_type), remaining: remaining.clean(), last_error: last_error.kind(), }, } }); // TODO: Check that we hit every error condition at least once, maybe // even output some statistics like filepath-securejoin does? assert!( allowed_results.iter().any(|expected| { let expected = expected .as_ref() .map(|lookup_result| { let (path, file_type) = { let (path, file_type) = lookup_result.as_inner_handle(); ( root_dir.join(path.trim_start_matches('/')).clean(), *file_type, ) }; match lookup_result { PartialLookup::Complete(_) => { PartialLookup::Complete((path, file_type)) } PartialLookup::Partial { handle: _, remaining, last_error, } => PartialLookup::Partial { handle: (path, file_type), remaining: remaining.clone(), last_error: *last_error, }, } }) .map_err(|err| *err); eprintln!("trying to match got {result:?} against allowed {expected:?}"); match (&result, &expected) { (Ok(lookup_result), Ok(expected_lookup_result)) => { lookup_result == expected_lookup_result } (result, expected) => tests_common::check_err(result, expected).is_ok(), } }), "resolve({unsafe_path:?}) result {result:?} not in allowed result set" ); Ok(()) } } pathrs-0.2.1/src/tests/test_resolve.rs000064400000000000000000001205371046102023000161520ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #[cfg(feature = "capi")] use crate::tests::capi::CapiRoot; use crate::{error::ErrorKind, flags::ResolverFlags, resolvers::ResolverBackend, syscalls, Root}; use std::path::Path; use anyhow::Error; macro_rules! resolve_tests { // resolve_tests! { // [create_root_path] { // test_ok: resolve(...) => Ok(("path", libc::S_IF...)) // test_err: resolve(...) => Err(ErrorKind::...) // } // } ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* rust-fn $test_name:ident (mut $root_var:ident : Root) $body:block => $expected:expr) => { paste::paste! { #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { let mut $root_var = Root::open(root_dir)?; assert_eq!( $root_var.resolver_backend(), ResolverBackend::default(), "ResolverBackend not the default despite not being configured" ); { $body } // Make sure the mut $root_var doesn't give us a warning. $root_var.set_resolver_flags($root_var.resolver_flags()); Ok(()) }) } #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { let root = Root::open(root_dir)?; let mut $root_var = root.as_ref(); assert_eq!( $root_var.resolver_backend(), ResolverBackend::default(), "ResolverBackend not the default despite not being configured" ); { $body } // Make sure the mut $root_var doesn't give us a warning. $root_var.set_resolver_flags($root_var.resolver_flags()); Ok(()) }) } #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { let mut $root_var = Root::open(root_dir)?; $root_var.set_resolver_backend(ResolverBackend::KernelOpenat2); assert_eq!( $root_var.resolver_backend(), ResolverBackend::KernelOpenat2, "incorrect ResolverBackend despite using set_resolver_backend" ); if !$root_var.resolver_backend().supported() { // Skip if not supported. return Ok(()); } { $body } Ok(()) }) } #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { let root = Root::open(root_dir)?; let mut $root_var = root.as_ref(); $root_var.set_resolver_backend(ResolverBackend::KernelOpenat2); assert_eq!( $root_var.resolver_backend(), ResolverBackend::KernelOpenat2, "incorrect ResolverBackend despite using set_resolver_backend" ); if !$root_var.resolver_backend().supported() { // Skip if not supported. return Ok(()); } { $body } Ok(()) }) } #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { // This test only makes sense if openat2 is supported (i.e., the // default resolver is openat2 -- otherwise the default test // already tested this case). if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } utils::$with_root_fn(|root_dir: &Path| { let mut $root_var = Root::open(root_dir)?; $root_var.set_resolver_backend(ResolverBackend::EmulatedOpath); assert_eq!( $root_var.resolver_backend(), ResolverBackend::EmulatedOpath, "incorrect ResolverBackend despite using set_resolver_backend" ); // EmulatedOpath is always supported. assert!( $root_var.resolver_backend().supported(), "emulated opath is always supported", ); { $body } Ok(()) }) } #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { // This test only makes sense if openat2 is supported (i.e., the // default resolver is openat2 -- otherwise the default test // already tested this case). if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } utils::$with_root_fn(|root_dir: &Path| { let root = Root::open(root_dir)?; let mut $root_var = root .as_ref(); $root_var.set_resolver_backend(ResolverBackend::EmulatedOpath); assert_eq!( $root_var.resolver_backend(), ResolverBackend::EmulatedOpath, "incorrect ResolverBackend despite using set_resolver_backend" ); // EmulatedOpath is always supported. assert!( $root_var.resolver_backend().supported(), "emulated opath is always supported", ); { $body } // Make sure root_dir is not dropped earlier. let _root_dir = root_dir; Ok(()) }) } } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* capi-fn $test_name:ident ($root_var:ident : CapiRoot) $body:block => $expected:expr) => { paste::paste! { #[test] #[cfg(feature = "capi")] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|root_dir: &Path| { let $root_var = CapiRoot::open(root_dir)?; { $body } Ok(()) }) } } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @rust-impl $test_name:ident $op_name:ident ($path:expr, $rflags:expr, $no_follow_trailing:expr) => $expected:expr) => { paste::paste! { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* rust-fn [<$op_name _ $test_name>](mut root: Root) { root.set_resolver_flags($rflags); let expected = $expected; utils::[]( &root, $path, $no_follow_trailing, expected, )?; } => $expected } } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @capi-impl $test_name:ident $op_name:ident ($path:expr, $no_follow_trailing:expr) => $expected:expr) => { paste::paste! { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* capi-fn [<$op_name _ $test_name>](root: CapiRoot) { let expected = $expected; utils::[]( &root, $path, $no_follow_trailing, expected, )?; } => $expected } } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @impl $test_name:ident $op_name:ident ($path:expr, rflags = $($rflag:ident)|+) => $expected:expr ) => { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @rust-impl $test_name $op_name($path, $(ResolverFlags::$rflag)|*, false) => $expected } // The C API doesn't support custom ResolverFlags. }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @impl $test_name:ident $op_name:ident ($path:expr, no_follow_trailing = $no_follow_trailing:expr) => $expected:expr ) => { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @rust-impl $test_name $op_name($path, ResolverFlags::empty(), $no_follow_trailing) => $expected } resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @capi-impl $test_name $op_name($path, $no_follow_trailing) => $expected } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @impl $test_name:ident $op_name:ident ($path:expr) => $expected:expr ) => { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @rust-impl $test_name $op_name($path, ResolverFlags::empty(), false) => $expected } resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @capi-impl $test_name $op_name($path, false) => $expected } }; ($([$with_root_fn:ident] { $($(#[cfg($ignore_meta:meta)])* $test_name:ident : $op_name:ident ($($args:tt)*) => $expected:expr);* $(;)? });* $(;)?) => { $( $( resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @impl $test_name $op_name ($($args)*) => $expected } )* )* } } resolve_tests! { // Test the magic-link-related handling. [with_proc] { proc_pseudo_magiclink: resolve("self/sched") => Ok(("{{/proc/self}}/sched", libc::S_IFREG)); proc_pseudo_magiclink_nosym1: resolve("self", rflags = NO_SYMLINKS) => Err(ErrorKind::OsError(Some(libc::ELOOP))); proc_pseudo_magiclink_nosym2: resolve("self/sched", rflags = NO_SYMLINKS) => Err(ErrorKind::OsError(Some(libc::ELOOP))); proc_pseudo_magiclink_nofollow1: resolve("self", no_follow_trailing = true) => Ok(("self", libc::S_IFLNK)); proc_pseudo_magiclink_nofollow2: resolve("self/sched", no_follow_trailing = true) => Ok(("{{/proc/self}}/sched", libc::S_IFREG)); // Verify forced RESOLVE_NO_MAGICLINKS behaviour. proc_magiclink: resolve("self/exe") => Err(ErrorKind::OsError(Some(libc::ELOOP))); proc_magiclink_nofollow: resolve("self/exe", no_follow_trailing = true) => Ok(("{{/proc/self}}/exe", libc::S_IFLNK)); proc_magiclink_component_nofollow: resolve("self/root/etc/passwd", no_follow_trailing = true) => Err(ErrorKind::OsError(Some(libc::ELOOP))); }; // Complete lookups. [with_basic_tree] { complete_root1: resolve("/") => Ok(("/", libc::S_IFDIR)); complete_root2: resolve("/../../../../../..") => Ok(("/", libc::S_IFDIR)); complete_root_link1: resolve("root-link1") => Ok(("/", libc::S_IFDIR)); complete_root_link2: resolve("root-link2") => Ok(("/", libc::S_IFDIR)); complete_root_link3: resolve("root-link3") => Ok(("/", libc::S_IFDIR)); complete_dir1: resolve("a") => Ok(("/a", libc::S_IFDIR)); complete_dir2: resolve("b/c/d/e/f") => Ok(("/b/c/d/e/f", libc::S_IFDIR)); complete_dir3: resolve("b///././c////.//d/./././///e////.//./f//././././") => Ok(("/b/c/d/e/f", libc::S_IFDIR)); complete_file: resolve("b/c/file") => Ok(("/b/c/file", libc::S_IFREG)); complete_file_link: resolve("b-file") => Ok(("/b/c/file", libc::S_IFREG)); complete_fifo: resolve("b/fifo") => Ok(("/b/fifo", libc::S_IFIFO)); complete_sock: resolve("b/sock") => Ok(("/b/sock", libc::S_IFSOCK)); // Partial lookups. partial_dir_basic: resolve("a/b/c/d/e/f/g/h") => Err(ErrorKind::OsError(Some(libc::ENOENT))); partial_dir_dotdot: resolve("a/foo/../bar/baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); // Non-lexical symlinks. nonlexical_basic_complete: resolve("target") => Ok(("/target", libc::S_IFDIR)); nonlexical_basic_complete1: resolve("target/") => Ok(("/target", libc::S_IFDIR)); nonlexical_basic_complete2: resolve("target//") => Ok(("/target", libc::S_IFDIR)); nonlexical_basic_partial: resolve("target/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_basic_partial_dotdot: resolve("target/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level1_abs_complete1: resolve("link1/target_abs") => Ok(("/target", libc::S_IFDIR)); nonlexical_level1_abs_complete2: resolve("link1/target_abs/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level1_abs_complete3: resolve("link1/target_abs//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level1_abs_partial: resolve("link1/target_abs/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level1_abs_partial_dotdot: resolve("link1/target_abs/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level1_rel_complete1: resolve("link1/target_rel") => Ok(("/target", libc::S_IFDIR)); nonlexical_level1_rel_complete2: resolve("link1/target_rel/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level1_rel_complete3: resolve("link1/target_rel//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level1_rel_partial: resolve("link1/target_rel/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level1_rel_partial_dotdot: resolve("link1/target_rel/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_abs_abs_complete1: resolve("link2/link1_abs/target_abs") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_abs_complete2: resolve("link2/link1_abs/target_abs/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_abs_complete3: resolve("link2/link1_abs/target_abs//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_abs_partial: resolve("link2/link1_abs/target_abs/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_abs_abs_partial_dotdot: resolve("link2/link1_abs/target_abs/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_abs_rel_complete1: resolve("link2/link1_abs/target_rel") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_rel_complete2: resolve("link2/link1_abs/target_rel/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_rel_complete3: resolve("link2/link1_abs/target_rel//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_rel_partial: resolve("link2/link1_abs/target_rel/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_abs_rel_partial_dotdot: resolve("link2/link1_abs/target_rel/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_abs_open_complete1: resolve("link2/link1_abs/../target") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_open_complete2: resolve("link2/link1_abs/../target/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_open_complete3: resolve("link2/link1_abs/../target//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_abs_open_partial: resolve("link2/link1_abs/../target/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_abs_open_partial_dotdot: resolve("link2/link1_abs/../target/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_rel_abs_complete1: resolve("link2/link1_rel/target_abs") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_abs_complete2: resolve("link2/link1_rel/target_abs/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_abs_complete3: resolve("link2/link1_rel/target_abs//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_abs_partial: resolve("link2/link1_rel/target_abs/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_rel_abs_partial_dotdot: resolve("link2/link1_rel/target_abs/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_rel_rel_complete1: resolve("link2/link1_rel/target_rel") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_rel_complete2: resolve("link2/link1_rel/target_rel/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_rel_complete3: resolve("link2/link1_rel/target_rel//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_rel_partial: resolve("link2/link1_rel/target_rel/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_rel_rel_partial_dotdot: resolve("link2/link1_rel/target_rel/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_rel_open_complete1: resolve("link2/link1_rel/../target") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_open_complete2: resolve("link2/link1_rel/../target/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_open_complete3: resolve("link2/link1_rel/../target//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level2_rel_open_partial: resolve("link2/link1_rel/../target/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level2_rel_open_partial_dotdot: resolve("link2/link1_rel/../target/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level3_abs_complete1: resolve("link3/target_abs") => Ok(("/target", libc::S_IFDIR)); nonlexical_level3_abs_complete2: resolve("link3/target_abs/") => Ok(("/target", libc::S_IFDIR)); nonlexical_level3_abs_complete3: resolve("link3/target_abs//") => Ok(("/target", libc::S_IFDIR)); nonlexical_level3_abs_partial: resolve("link3/target_abs/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level3_abs_partial_dotdot: resolve("link3/target_abs/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level3_rel_complete: resolve("link3/target_rel") => Ok(("/target", libc::S_IFDIR)); nonlexical_level3_rel_partial: resolve("link3/target_rel/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); nonlexical_level3_rel_partial_dotdot: resolve("link3/target_rel/../target/foo/bar/../baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); // Partial lookups due to hitting a non_directory. partial_nondir_slash1: resolve("b/c/file/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_slash2: resolve("b/c/file//") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_dot: resolve("b/c/file/.") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_dotdot1: resolve("b/c/file/..") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_dotdot2: resolve("b/c/file/../foo/bar") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_symlink_slash1: resolve("b-file/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_symlink_slash2: resolve("b-file//") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_symlink_dot: resolve("b-file/.") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_symlink_dotdot1: resolve("b-file/..") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_nondir_symlink_dotdot2: resolve("b-file/../foo/bar") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_fifo_slash1: resolve("b/fifo/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_fifo_slash2: resolve("b/fifo//") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_fifo_dot: resolve("b/fifo/.") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_fifo_dotdot1: resolve("b/fifo/..") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_fifo_dotdot2: resolve("b/fifo/../foo/bar") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_sock_slash1: resolve("b/sock/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_sock_slash2: resolve("b/sock//") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_sock_dot: resolve("b/sock/.") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_sock_dotdot1: resolve("b/sock/..") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); partial_sock_dotdot2: resolve("b/sock/../foo/bar") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); // O_NOFOLLOW doesn't matter for trailing-slash paths. partial_symlink_nofollow_slash1: resolve("link3/target_abs/", no_follow_trailing = true) => Ok(("/target", libc::S_IFDIR)); partial_symlink_nofollow_slash2: resolve("link3/target_abs//", no_follow_trailing = true) => Ok(("/target", libc::S_IFDIR)); partial_symlink_nofollow_dot: resolve("link3/target_abs/.", no_follow_trailing = true) => Ok(("/target", libc::S_IFDIR)); // Dangling symlinks are treated as though they are non_existent. dangling1_inroot_trailing: resolve("a-fake1") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling1_inroot_partial: resolve("a-fake1/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling1_inroot_partial_dotdot: resolve("a-fake1/../bar/baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling1_sub_trailing: resolve("c/a-fake1") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling1_sub_partial: resolve("c/a-fake1/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling1_sub_partial_dotdot: resolve("c/a-fake1/../bar/baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling2_inroot_trailing: resolve("a-fake2") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling2_inroot_partial: resolve("a-fake2/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling2_inroot_partial_dotdot: resolve("a-fake2/../bar/baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling2_sub_trailing: resolve("c/a-fake2") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling2_sub_partial: resolve("c/a-fake2/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling2_sub_partial_dotdot: resolve("c/a-fake2/../bar/baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling3_inroot_trailing: resolve("a-fake3") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling3_inroot_partial: resolve("a-fake3/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling3_inroot_partial_dotdot: resolve("a-fake3/../bar/baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling3_sub_trailing: resolve("c/a-fake3") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling3_sub_partial: resolve("c/a-fake3/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling3_sub_partial_dotdot: resolve("c/a-fake3/../bar/baz") => Err(ErrorKind::OsError(Some(libc::ENOENT))); // Tricky dangling symlinks. dangling_tricky1_trailing: resolve("link3/deep_dangling1") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling_tricky1_partial: resolve("link3/deep_dangling1/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling_tricky1_partial_dotdot: resolve("link3/deep_dangling1/..") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling_tricky2_trailing: resolve("link3/deep_dangling2") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling_tricky2_partial: resolve("link3/deep_dangling2/foo") => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling_tricky2_partial_dotdot: resolve("link3/deep_dangling2/..") => Err(ErrorKind::OsError(Some(libc::ENOENT))); // Really deep dangling links. deep_dangling1: resolve("dangling/a") => Err(ErrorKind::OsError(Some(libc::ENOENT))); deep_dangling2: resolve("dangling/b/c") => Err(ErrorKind::OsError(Some(libc::ENOENT))); deep_dangling3: resolve("dangling/c") => Err(ErrorKind::OsError(Some(libc::ENOENT))); deep_dangling4: resolve("dangling/d/e") => Err(ErrorKind::OsError(Some(libc::ENOENT))); deep_dangling5: resolve("dangling/e") => Err(ErrorKind::OsError(Some(libc::ENOENT))); deep_dangling6: resolve("dangling/g") => Err(ErrorKind::OsError(Some(libc::ENOENT))); deep_dangling_fileasdir1: resolve("dangling-file/a") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); deep_dangling_fileasdir2: resolve("dangling-file/b/c") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); deep_dangling_fileasdir3: resolve("dangling-file/c") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); deep_dangling_fileasdir4: resolve("dangling-file/d/e") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); deep_dangling_fileasdir5: resolve("dangling-file/e") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); deep_dangling_fileasdir6: resolve("dangling-file/g") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); // Symlink loops. loop1: resolve("loop/link") => Err(ErrorKind::OsError(Some(libc::ELOOP))); loop_basic1: resolve("loop/basic-loop1") => Err(ErrorKind::OsError(Some(libc::ELOOP))); loop_basic2: resolve("loop/basic-loop2") => Err(ErrorKind::OsError(Some(libc::ELOOP))); loop_basic3: resolve("loop/basic-loop3") => Err(ErrorKind::OsError(Some(libc::ELOOP))); // NO_FOLLOW. symlink_nofollow: resolve("link3/target_abs", no_follow_trailing = true) => Ok(("link3/target_abs", libc::S_IFLNK)); symlink_component_nofollow1: resolve("e/f", no_follow_trailing = true) => Ok(("b/c/d/e/f", libc::S_IFDIR)); symlink_component_nofollow2: resolve("link2/link1_abs/target_rel", no_follow_trailing = true) => Ok(("link1/target_rel", libc::S_IFLNK)); loop_nofollow: resolve("loop/link", no_follow_trailing = true) => Ok(("loop/link", libc::S_IFLNK)); // RESOLVE_NO_SYMLINKS. dir_nosym: resolve("b/c/d/e", rflags = NO_SYMLINKS) => Ok(("b/c/d/e", libc::S_IFDIR)); symlink_nosym: resolve("link3/target_abs", rflags = NO_SYMLINKS) => Err(ErrorKind::OsError(Some(libc::ELOOP))); symlink_component_nosym1: resolve("e/f", rflags = NO_SYMLINKS) => Err(ErrorKind::OsError(Some(libc::ELOOP))); symlink_component_nosym2: resolve("link2/link1_abs/target_rel", rflags = NO_SYMLINKS) => Err(ErrorKind::OsError(Some(libc::ELOOP))); loop_nosym: resolve("loop/link", rflags = NO_SYMLINKS) => Err(ErrorKind::OsError(Some(libc::ELOOP))); // fs.protected_symlinks for a directory owned by us. protected_symlinks_selfdir_selfsym: resolve("tmpfs-self/link-self") => Ok(("tmpfs-self/file", libc::S_IFREG)); protected_symlinks_selfdir_selfsym_nofollow: resolve("tmpfs-self/link-self", no_follow_trailing = true) => Ok(("tmpfs-self/link-self", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_selfdir_otheruidsym: resolve("tmpfs-self/link-otheruid") => Err(ErrorKind::OsError(Some(libc::EACCES))); #[cfg(feature = "_test_as_root")] protected_symlinks_selfdir_otheruidsym_nofollow: resolve("tmpfs-self/link-otheruid", no_follow_trailing = true) => Ok(("tmpfs-self/link-otheruid", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_selfdir_othergidsym: resolve("tmpfs-self/link-othergid") => Ok(("tmpfs-self/file", libc::S_IFREG)); #[cfg(feature = "_test_as_root")] protected_symlinks_selfdir_othergidsym_nofollow: resolve("tmpfs-self/link-othergid", no_follow_trailing = true) => Ok(("tmpfs-self/link-othergid", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_selfdir_othersym: resolve("tmpfs-self/link-other") => Err(ErrorKind::OsError(Some(libc::EACCES))); #[cfg(feature = "_test_as_root")] protected_symlinks_selfdir_othersym_nofollow: resolve("tmpfs-self/link-other", no_follow_trailing = true) => Ok(("tmpfs-self/link-other", libc::S_IFLNK)); // fs.protected_symlinks for a directory owned by someone else. #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_selfsym: resolve("tmpfs-other/link-self") => Ok(("tmpfs-other/file", libc::S_IFREG)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_selfsym_nofollow: resolve("tmpfs-other/link-self", no_follow_trailing = true) => Ok(("tmpfs-other/link-self", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_selfuidsym: resolve("tmpfs-other/link-selfuid") => Ok(("tmpfs-other/file", libc::S_IFREG)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_selfuidsym_nofollow: resolve("tmpfs-other/link-selfuid", no_follow_trailing = true) => Ok(("tmpfs-other/link-selfuid", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_ownersym: resolve("tmpfs-other/link-owner") => Ok(("tmpfs-other/file", libc::S_IFREG)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_ownersym_nofollow: resolve("tmpfs-other/link-owner", no_follow_trailing = true) => Ok(("tmpfs-other/link-owner", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_otheruidsym: resolve("tmpfs-other/link-otheruid") => Err(ErrorKind::OsError(Some(libc::EACCES))); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_otheruidsym_nofollow: resolve("tmpfs-other/link-otheruid", no_follow_trailing = true) => Ok(("tmpfs-other/link-otheruid", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_othergidsym: resolve("tmpfs-other/link-othergid") => Ok(("tmpfs-other/file", libc::S_IFREG)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_othergidsym_nofollow: resolve("tmpfs-other/link-othergid", no_follow_trailing = true) => Ok(("tmpfs-other/link-othergid", libc::S_IFLNK)); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_othersym: resolve("tmpfs-other/link-other") => Err(ErrorKind::OsError(Some(libc::EACCES))); #[cfg(feature = "_test_as_root")] protected_symlinks_otherdir_othersym_nofollow: resolve("tmpfs-other/link-other", no_follow_trailing = true) => Ok(("tmpfs-other/link-other", libc::S_IFLNK)); } } resolve_tests! { [with_nosymfollow_root] { #[cfg(feature = "_test_as_root")] symlink_regular_symfollow: resolve("nosymfollow/goodlink") => Ok((".", libc::S_IFDIR)); #[cfg(feature = "_test_as_root")] symlink_itself_nosymfollow: resolve("nosymfollow/badlink") => Err(ErrorKind::OsError(Some(libc::ELOOP))); #[cfg(feature = "_test_as_root")] symlink_inside_nosymfollow_dir: resolve("nosymfollow/nosymdir/dir/badlink") => Err(ErrorKind::OsError(Some(libc::ELOOP))); #[cfg(feature = "_test_as_root")] symlink_itself_nested_clear_symfollow: resolve("nosymfollow/nosymdir/dir/goodlink") => Ok((".", libc::S_IFDIR)); #[cfg(feature = "_test_as_root")] symlink_inside_nested_clear_symfollow: resolve("nosymfollow/nosymdir/dir/foo/yessymdir/bar/goodlink") => Ok((".", libc::S_IFDIR)); } } mod utils { use crate::{ error::ErrorKind, flags::OpenFlags, syscalls, tests::{ common::{self as tests_common, LookupResult}, traits::{HandleImpl, RootImpl}, }, utils::FdExt, }; use std::{os::unix::fs::MetadataExt, path::Path}; use anyhow::{Context, Error}; use pretty_assertions::assert_eq; pub(super) fn with_basic_tree(func: F) -> Result<(), Error> where F: FnOnce(&Path) -> Result<(), Error>, { let root_dir = tests_common::create_basic_tree()?; let res = func(root_dir.path()); let _root_dir = root_dir; // make sure tmpdir is kept alive res } pub(super) fn with_proc(func: F) -> Result<(), Error> where F: FnOnce(&Path) -> Result<(), Error>, { func(Path::new("/proc")) } pub(super) fn with_nosymfollow_root(func: F) -> Result<(), Error> where F: FnOnce(&Path) -> Result<(), Error>, { tests_common::in_mnt_ns(|| { let root_dir = tests_common::create_basic_tree()?; tests_common::mask_nosymfollow(root_dir.path())?; let res = func(root_dir.path()); let _root_dir = root_dir; // make sure tmpdir is kept alive res }) } pub(super) fn check_root_resolve( root: R, unsafe_path: impl AsRef, no_follow_trailing: bool, expected: Result, ) -> Result<(), Error> where H: HandleImpl, R: RootImpl, for<'a> &'a R::Handle: HandleImpl, { let root_dir = root.as_unsafe_path_unchecked()?; let unsafe_path = unsafe_path.as_ref(); let result = if no_follow_trailing { root.resolve_nofollow(unsafe_path) } else { root.resolve(unsafe_path) }; let (handle, expected_path, expected_file_type) = match (result, expected) { (Ok(handle), Ok((expected_path, file_type))) => ( handle, expected_path.replace("{{/proc/self}}", &syscalls::getpid().to_string()), file_type, ), (result, expected) => { let result = match result { Ok(handle) => Ok(handle.as_unsafe_path_unchecked()?), Err(err) => Err(err), }; tests_common::check_err(&result, &expected) .with_context(|| format!("root resolve {unsafe_path:?}"))?; assert!( result.is_err(), "we should never see an Ok(file) after check_err if we expected {expected:?}" ); return Ok(()); } }; let expected_path = expected_path.trim_start_matches('/'); let real_handle_path = handle.as_unsafe_path_unchecked()?; assert_eq!( real_handle_path, root_dir.join(expected_path), "resolve({unsafe_path:?}, {no_follow_trailing}) path mismatch", ); let meta = handle.metadata()?; let real_file_type = meta.mode() & libc::S_IFMT; assert_eq!(real_file_type, expected_file_type, "file type mismatch",); match real_file_type { libc::S_IFDIR => { tests_common::check_reopen(&handle, OpenFlags::O_RDONLY, None)?; tests_common::check_reopen(&handle, OpenFlags::O_DIRECTORY, None)?; // Make sure O_NOFOLLOW acts the way you would expect. tests_common::check_reopen( &handle, OpenFlags::O_RDONLY | OpenFlags::O_NOFOLLOW, None, )?; tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW, None, )?; tests_common::check_reopen(&handle, OpenFlags::O_PATH, None)?; // Forcefully set O_CLOEXEC. tests_common::check_reopen( &handle, OpenFlags::O_RDONLY | OpenFlags::O_CLOEXEC, None, )?; tests_common::check_reopen( &handle, OpenFlags::O_DIRECTORY | OpenFlags::O_CLOEXEC, None, )?; } libc::S_IFREG => { tests_common::check_reopen(&handle, OpenFlags::O_RDWR, None)?; tests_common::check_reopen(&handle, OpenFlags::O_DIRECTORY, Some(libc::ENOTDIR))?; // Make sure O_NOFOLLOW acts the way you would expect. tests_common::check_reopen( &handle, OpenFlags::O_RDONLY | OpenFlags::O_NOFOLLOW, None, )?; tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW, None, )?; tests_common::check_reopen(&handle, OpenFlags::O_PATH, None)?; // Forcefully set O_CLOEXEC. tests_common::check_reopen( &handle, OpenFlags::O_RDWR | OpenFlags::O_CLOEXEC, None, )?; tests_common::check_reopen( &handle, OpenFlags::O_DIRECTORY | OpenFlags::O_CLOEXEC, Some(libc::ENOTDIR), )?; } libc::S_IFLNK => { assert!( no_follow_trailing, "we must only get a symlink handle if no_follow_trailing is set" ); tests_common::check_reopen(&handle, OpenFlags::O_RDONLY, Some(libc::ELOOP)) .context("reopen(O_RDONLY) of a symlink handle should fail with ELOOP")?; tests_common::check_reopen(&handle, OpenFlags::O_PATH, Some(libc::ELOOP)) .context("reopen(O_PATH) of a symlink handle should fail with ELOOP")?; tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW, Some(libc::ELOOP), ) .context("reopen(O_PATH|O_NOFOLLOW) of a symlink handle should fail with ELOOP")?; } _ => { tests_common::check_reopen(&handle, OpenFlags::O_PATH, None)?; tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, Some(libc::ENOTDIR), )?; // Make sure O_NOFOLLOW acts the way you would expect. tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW, None, )?; // Forcefully set O_CLOEXEC. tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_CLOEXEC, None, )?; tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_DIRECTORY | OpenFlags::O_CLOEXEC, Some(libc::ENOTDIR), )?; } } Ok(()) } } pathrs-0.2.1/src/tests/test_resolve_partial.rs000064400000000000000000001324111046102023000176600ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::ErrorKind, flags::ResolverFlags, resolvers::{PartialLookup, ResolverBackend}, syscalls, Root, }; use anyhow::Error; macro_rules! resolve_tests { // resolve_tests! { // [create_root_path] { // test_ok: resolve_partial(...) => Ok(("path", Some("remaining", ErrorKind::...)), libc::S_IF...)); // test_err: resolve_partial(...) => Err(ErrorKind::...); // } // } ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* fn $test_name:ident (mut $root_var:ident : Root) $body:block) => { paste::paste! { #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|mut $root_var: Root| { assert_eq!( $root_var.resolver_backend(), ResolverBackend::default(), "ResolverBackend not the default despite not being configured" ); { $body } // Make sure the mut $root_var doesn't give us a warning. $root_var.set_resolver_flags($root_var.resolver_flags()); Ok(()) }) } #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { utils::$with_root_fn(|mut $root_var: Root| { $root_var.set_resolver_backend(ResolverBackend::KernelOpenat2); assert_eq!( $root_var.resolver_backend(), ResolverBackend::KernelOpenat2, "incorrect ResolverBackend despite using set_resolver_backend" ); if !$root_var.resolver_backend().supported() { // Skip if not supported. return Ok(()); } { $body } Ok(()) }) } #[test] $(#[cfg_attr(not($ignore_meta), ignore)])* fn []() -> Result<(), Error> { // This test only makes sense if openat2 is supported (i.e., the // default resolver is openat2 -- otherwise the default test // already tested this case). if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } utils::$with_root_fn(|mut $root_var: Root| { $root_var.set_resolver_backend(ResolverBackend::EmulatedOpath); assert_eq!( $root_var.resolver_backend(), ResolverBackend::EmulatedOpath, "incorrect ResolverBackend despite using set_resolver_backend" ); // EmulatedOpath is always supported. assert!( $root_var.resolver_backend().supported(), "emulated opath is always supported", ); { $body } Ok(()) }) } } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @impl $test_name:ident $op_name:ident ($path:expr, $rflags:expr, $no_follow_trailing:expr) => $expected:expr) => { paste::paste! { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* fn [<$op_name _ $test_name>](mut root: Root) { root.set_resolver_flags($rflags); let expected = $expected; utils::[]( &root, $path, $no_follow_trailing, expected, )?; } } } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @impl $test_name:ident $op_name:ident ($path:expr, rflags = $($rflag:ident)|+) => $expected:expr ) => { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @impl $test_name $op_name($path, $(ResolverFlags::$rflag)|*, false) => $expected } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @impl $test_name:ident $op_name:ident ($path:expr, no_follow_trailing = $no_follow_trailing:expr) => $expected:expr ) => { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @impl $test_name $op_name($path, ResolverFlags::empty(), $no_follow_trailing) => $expected } }; ([$with_root_fn:ident] $(#[cfg($ignore_meta:meta)])* @impl $test_name:ident $op_name:ident ($path:expr) => $expected:expr ) => { resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @impl $test_name $op_name($path, ResolverFlags::empty(), false) => $expected } }; ($([$with_root_fn:ident] { $($(#[cfg($ignore_meta:meta)])* $test_name:ident : $op_name:ident ($($args:tt)*) => $expected:expr);* $(;)? });* $(;)?) => { $( $( resolve_tests! { [$with_root_fn] $(#[cfg($ignore_meta)])* @impl $test_name $op_name ($($args)*) => $expected } )* )* } } resolve_tests! { [with_basic_tree] { // Complete lookups. complete_root1: resolve_partial("/") => Ok(PartialLookup::Complete(("/", libc::S_IFDIR))); complete_root2: resolve_partial("/../../../../../..") => Ok(PartialLookup::Complete(("/", libc::S_IFDIR))); complete_root_link1: resolve_partial("root-link1") => Ok(PartialLookup::Complete(("/", libc::S_IFDIR))); complete_root_link2: resolve_partial("root-link2") => Ok(PartialLookup::Complete(("/", libc::S_IFDIR))); complete_root_link3: resolve_partial("root-link3") => Ok(PartialLookup::Complete(("/", libc::S_IFDIR))); complete_dir1: resolve_partial("a") => Ok(PartialLookup::Complete(("/a", libc::S_IFDIR))); complete_dir2: resolve_partial("b/c/d/e/f") => Ok(PartialLookup::Complete(("/b/c/d/e/f", libc::S_IFDIR))); complete_dir3: resolve_partial("b///././c////.//d/./././///e////.//./f//././././") => Ok(PartialLookup::Complete(("/b/c/d/e/f", libc::S_IFDIR))); complete_file: resolve_partial("b/c/file") => Ok(PartialLookup::Complete(("/b/c/file", libc::S_IFREG))); complete_file_link: resolve_partial("b-file") => Ok(PartialLookup::Complete(("/b/c/file", libc::S_IFREG))); complete_fifo: resolve_partial("b/fifo") => Ok(PartialLookup::Complete(("/b/fifo", libc::S_IFIFO))); complete_sock: resolve_partial("b/sock") => Ok(PartialLookup::Complete(("/b/sock", libc::S_IFSOCK))); // Partial lookups. partial_dir_basic: resolve_partial("a/b/c/d/e/f/g/h") => Ok(PartialLookup::Partial { handle: ("a", libc::S_IFDIR), remaining: "b/c/d/e/f/g/h".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); partial_dir_dotdot: resolve_partial("a/foo/../bar/baz") => Ok(PartialLookup::Partial { handle: ("a", libc::S_IFDIR), remaining: "foo/../bar/baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); // Non-lexical symlinks. nonlexical_basic_complete: resolve_partial("target") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_basic_complete1: resolve_partial("target/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_basic_complete2: resolve_partial("target//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_basic_partial: resolve_partial("target/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_basic_partial_dotdot: resolve_partial("target/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level1_abs_complete1: resolve_partial("link1/target_abs") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level1_abs_complete2: resolve_partial("link1/target_abs/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level1_abs_complete3: resolve_partial("link1/target_abs//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level1_abs_partial: resolve_partial("link1/target_abs/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level1_abs_partial_dotdot: resolve_partial("link1/target_abs/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level1_rel_complete1: resolve_partial("link1/target_rel") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level1_rel_complete2: resolve_partial("link1/target_rel/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level1_rel_complete3: resolve_partial("link1/target_rel//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level1_rel_partial: resolve_partial("link1/target_rel/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level1_rel_partial_dotdot: resolve_partial("link1/target_rel/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_abs_abs_complete1: resolve_partial("link2/link1_abs/target_abs") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_abs_complete2: resolve_partial("link2/link1_abs/target_abs/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_abs_complete3: resolve_partial("link2/link1_abs/target_abs//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_abs_partial: resolve_partial("link2/link1_abs/target_abs/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_abs_abs_partial_dotdot: resolve_partial("link2/link1_abs/target_abs/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_abs_rel_complete1: resolve_partial("link2/link1_abs/target_rel") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_rel_complete2: resolve_partial("link2/link1_abs/target_rel/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_rel_complete3: resolve_partial("link2/link1_abs/target_rel//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_rel_partial: resolve_partial("link2/link1_abs/target_rel/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_abs_rel_partial_dotdot: resolve_partial("link2/link1_abs/target_rel/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_abs_open_complete1: resolve_partial("link2/link1_abs/../target") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_open_complete2: resolve_partial("link2/link1_abs/../target/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_open_complete3: resolve_partial("link2/link1_abs/../target//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_abs_open_partial: resolve_partial("link2/link1_abs/../target/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_abs_open_partial_dotdot: resolve_partial("link2/link1_abs/../target/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_rel_abs_complete1: resolve_partial("link2/link1_rel/target_abs") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_abs_complete2: resolve_partial("link2/link1_rel/target_abs/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_abs_complete3: resolve_partial("link2/link1_rel/target_abs//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_abs_partial: resolve_partial("link2/link1_rel/target_abs/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_rel_abs_partial_dotdot: resolve_partial("link2/link1_rel/target_abs/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_rel_rel_complete1: resolve_partial("link2/link1_rel/target_rel") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_rel_complete2: resolve_partial("link2/link1_rel/target_rel/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_rel_complete3: resolve_partial("link2/link1_rel/target_rel//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_rel_partial: resolve_partial("link2/link1_rel/target_rel/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_rel_rel_partial_dotdot: resolve_partial("link2/link1_rel/target_rel/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_rel_open_complete1: resolve_partial("link2/link1_rel/../target") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_open_complete2: resolve_partial("link2/link1_rel/../target/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_open_complete3: resolve_partial("link2/link1_rel/../target//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level2_rel_open_partial: resolve_partial("link2/link1_rel/../target/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level2_rel_open_partial_dotdot: resolve_partial("link2/link1_rel/../target/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level3_abs_complete1: resolve_partial("link3/target_abs") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level3_abs_complete2: resolve_partial("link3/target_abs/") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level3_abs_complete3: resolve_partial("link3/target_abs//") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level3_abs_partial: resolve_partial("link3/target_abs/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level3_abs_partial_dotdot: resolve_partial("link3/target_abs/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level3_rel_complete: resolve_partial("link3/target_rel") => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); nonlexical_level3_rel_partial: resolve_partial("link3/target_rel/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); nonlexical_level3_rel_partial_dotdot: resolve_partial("link3/target_rel/../target/foo/bar/../baz") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo/bar/../baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); // Partial lookups due to hitting a non_directory. partial_nondir_slash1: resolve_partial("b/c/file/") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_slash2: resolve_partial("b/c/file//") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "/".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_dot: resolve_partial("b/c/file/.") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: ".".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_dotdot1: resolve_partial("b/c/file/..") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "..".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_dotdot2: resolve_partial("b/c/file/../foo/bar") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "../foo/bar".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_symlink_slash1: resolve_partial("b-file/") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_symlink_slash2: resolve_partial("b-file//") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "/".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_symlink_dot: resolve_partial("b-file/.") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: ".".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_symlink_dotdot1: resolve_partial("b-file/..") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "..".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_nondir_symlink_dotdot2: resolve_partial("b-file/../foo/bar") => Ok(PartialLookup::Partial { handle: ("b/c/file", libc::S_IFREG), remaining: "../foo/bar".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_fifo_slash1: resolve_partial("b/fifo/") => Ok(PartialLookup::Partial { handle: ("b/fifo", libc::S_IFIFO), remaining: "".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_fifo_slash2: resolve_partial("b/fifo//") => Ok(PartialLookup::Partial { handle: ("b/fifo", libc::S_IFIFO), remaining: "/".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_fifo_dot: resolve_partial("b/fifo/.") => Ok(PartialLookup::Partial { handle: ("b/fifo", libc::S_IFIFO), remaining: ".".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_fifo_dotdot1: resolve_partial("b/fifo/..") => Ok(PartialLookup::Partial { handle: ("b/fifo", libc::S_IFIFO), remaining: "..".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_fifo_dotdot2: resolve_partial("b/fifo/../foo/bar") => Ok(PartialLookup::Partial { handle: ("b/fifo", libc::S_IFIFO), remaining: "../foo/bar".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_sock_slash1: resolve_partial("b/sock/") => Ok(PartialLookup::Partial { handle: ("b/sock", libc::S_IFSOCK), remaining: "".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_sock_slash2: resolve_partial("b/sock//") => Ok(PartialLookup::Partial { handle: ("b/sock", libc::S_IFSOCK), remaining: "/".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_sock_dot: resolve_partial("b/sock/.") => Ok(PartialLookup::Partial { handle: ("b/sock", libc::S_IFSOCK), remaining: ".".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_sock_dotdot1: resolve_partial("b/sock/..") => Ok(PartialLookup::Partial { handle: ("b/sock", libc::S_IFSOCK), remaining: "..".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); partial_sock_dotdot2: resolve_partial("b/sock/../foo/bar") => Ok(PartialLookup::Partial { handle: ("b/sock", libc::S_IFSOCK), remaining: "../foo/bar".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); // O_NOFOLLOW doesn't matter for trailing-slash paths. partial_symlink_nofollow_slash1: resolve_partial("link3/target_abs/", no_follow_trailing = true) => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); partial_symlink_nofollow_slash2: resolve_partial("link3/target_abs//", no_follow_trailing = true) => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); partial_symlink_nofollow_dot: resolve_partial("link3/target_abs/.", no_follow_trailing = true) => Ok(PartialLookup::Complete(("/target", libc::S_IFDIR))); // Dangling symlinks are treated as though they are non_existent. dangling1_inroot_trailing: resolve_partial("a-fake1") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake1".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling1_inroot_partial: resolve_partial("a-fake1/foo") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake1/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling1_inroot_partial_dotdot: resolve_partial("a-fake1/../bar/baz") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake1/../bar/baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling1_sub_trailing: resolve_partial("c/a-fake1") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake1".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling1_sub_partial: resolve_partial("c/a-fake1/foo") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake1/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling1_sub_partial_dotdot: resolve_partial("c/a-fake1/../bar/baz") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake1/../bar/baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling2_inroot_trailing: resolve_partial("a-fake2") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake2".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling2_inroot_partial: resolve_partial("a-fake2/foo") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake2/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling2_inroot_partial_dotdot: resolve_partial("a-fake2/../bar/baz") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake2/../bar/baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling2_sub_trailing: resolve_partial("c/a-fake2") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake2".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling2_sub_partial: resolve_partial("c/a-fake2/foo") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake2/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling2_sub_partial_dotdot: resolve_partial("c/a-fake2/../bar/baz") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake2/../bar/baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling3_inroot_trailing: resolve_partial("a-fake3") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake3".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling3_inroot_partial: resolve_partial("a-fake3/foo") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake3/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling3_inroot_partial_dotdot: resolve_partial("a-fake3/../bar/baz") => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "a-fake3/../bar/baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling3_sub_trailing: resolve_partial("c/a-fake3") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake3".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling3_sub_partial: resolve_partial("c/a-fake3/foo") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake3/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling3_sub_partial_dotdot: resolve_partial("c/a-fake3/../bar/baz") => Ok(PartialLookup::Partial { handle: ("c", libc::S_IFDIR), remaining: "a-fake3/../bar/baz".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); // Tricky dangling symlinks. dangling_tricky1_trailing: resolve_partial("link3/deep_dangling1") => Ok(PartialLookup::Partial { handle: ("link3", libc::S_IFDIR), remaining: "deep_dangling1".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling_tricky1_partial: resolve_partial("link3/deep_dangling1/foo") => Ok(PartialLookup::Partial { handle: ("link3", libc::S_IFDIR), remaining: "deep_dangling1/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling_tricky1_partial_dotdot: resolve_partial("link3/deep_dangling1/..") => Ok(PartialLookup::Partial { handle: ("link3", libc::S_IFDIR), remaining: "deep_dangling1/..".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling_tricky2_trailing: resolve_partial("link3/deep_dangling2") => Ok(PartialLookup::Partial { handle: ("link3", libc::S_IFDIR), remaining: "deep_dangling2".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling_tricky2_partial: resolve_partial("link3/deep_dangling2/foo") => Ok(PartialLookup::Partial { handle: ("link3", libc::S_IFDIR), remaining: "deep_dangling2/foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); dangling_tricky2_partial_dotdot: resolve_partial("link3/deep_dangling2/..") => Ok(PartialLookup::Partial { handle: ("link3", libc::S_IFDIR), remaining: "deep_dangling2/..".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); // Really deep dangling links. deep_dangling1: resolve_partial("dangling/a") => Ok(PartialLookup::Partial { handle: ("dangling", libc::S_IFDIR), remaining: "a".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); deep_dangling2: resolve_partial("dangling/b/c") => Ok(PartialLookup::Partial { handle: ("dangling/b", libc::S_IFDIR), remaining: "c".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); deep_dangling3: resolve_partial("dangling/c") => Ok(PartialLookup::Partial { handle: ("dangling", libc::S_IFDIR), remaining: "c".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); deep_dangling4: resolve_partial("dangling/d/e") => Ok(PartialLookup::Partial { handle: ("dangling/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); deep_dangling5: resolve_partial("dangling/e") => Ok(PartialLookup::Partial { handle: ("dangling", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); deep_dangling6: resolve_partial("dangling/g") => Ok(PartialLookup::Partial { handle: ("dangling", libc::S_IFDIR), remaining: "g".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); deep_dangling_fileasdir1: resolve_partial("dangling-file/a") => Ok(PartialLookup::Partial { handle: ("dangling-file", libc::S_IFDIR), remaining: "a".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); deep_dangling_fileasdir2: resolve_partial("dangling-file/b/c") => Ok(PartialLookup::Partial { handle: ("dangling-file/b", libc::S_IFDIR), remaining: "c".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); deep_dangling_fileasdir3: resolve_partial("dangling-file/c") => Ok(PartialLookup::Partial { handle: ("dangling-file", libc::S_IFDIR), remaining: "c".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); deep_dangling_fileasdir4: resolve_partial("dangling-file/d/e") => Ok(PartialLookup::Partial { handle: ("dangling-file/d", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); deep_dangling_fileasdir5: resolve_partial("dangling-file/e") => Ok(PartialLookup::Partial { handle: ("dangling-file", libc::S_IFDIR), remaining: "e".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); deep_dangling_fileasdir6: resolve_partial("dangling-file/g") => Ok(PartialLookup::Partial { handle: ("dangling-file", libc::S_IFDIR), remaining: "g".into(), last_error: ErrorKind::OsError(Some(libc::ENOTDIR)), }); // Symlink loops. loop1: resolve_partial("loop/link") => Ok(PartialLookup::Partial { handle: ("loop", libc::S_IFDIR), remaining: "link".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); loop_basic1: resolve_partial("loop/basic-loop1") => Ok(PartialLookup::Partial { handle: ("loop", libc::S_IFDIR), remaining: "basic-loop1".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); loop_basic2: resolve_partial("loop/basic-loop2") => Ok(PartialLookup::Partial { handle: ("loop", libc::S_IFDIR), remaining: "basic-loop2".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); loop_basic3: resolve_partial("loop/basic-loop3") => Ok(PartialLookup::Partial { handle: ("loop", libc::S_IFDIR), remaining: "basic-loop3".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); // NO_FOLLOW. symlink_nofollow: resolve_partial("link3/target_abs", no_follow_trailing = true) => Ok(PartialLookup::Complete(("link3/target_abs", libc::S_IFLNK))); symlink_component_nofollow1: resolve_partial("e/f", no_follow_trailing = true) => Ok(PartialLookup::Complete(("b/c/d/e/f", libc::S_IFDIR))); symlink_component_nofollow2: resolve_partial("link2/link1_abs/target_rel", no_follow_trailing = true) => Ok(PartialLookup::Complete(("link1/target_rel", libc::S_IFLNK))); loop_nofollow: resolve_partial("loop/link", no_follow_trailing = true) => Ok(PartialLookup::Complete(("loop/link", libc::S_IFLNK))); // RESOLVE_NO_SYMLINKS. dir_nosym: resolve_partial("b/c/d/e", rflags = NO_SYMLINKS) => Ok(PartialLookup::Complete(("b/c/d/e", libc::S_IFDIR))); symlink_nosym: resolve_partial("link3/target_abs", rflags = NO_SYMLINKS) => Ok(PartialLookup::Partial { handle: ("link3", libc::S_IFDIR), remaining: "target_abs".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); symlink_component_nosym1: resolve_partial("e/f", rflags = NO_SYMLINKS) => Ok(PartialLookup::Partial { handle: ("", libc::S_IFDIR), remaining: "e/f".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); symlink_component_nosym2: resolve_partial("link2/link1_abs/target_rel", rflags = NO_SYMLINKS) => Ok(PartialLookup::Partial { handle: ("link2", libc::S_IFDIR), remaining: "link1_abs/target_rel".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); loop_nosym: resolve_partial("loop/link", rflags = NO_SYMLINKS) => Ok(PartialLookup::Partial { handle: ("loop", libc::S_IFDIR), remaining: "link".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); // Make sure that the symlink stack doesn't get corrupted by no-op '..' components. noop_dotdot_link1: resolve_partial("escape-link1/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); noop_dotdot_link2: resolve_partial("escape-link2/foo") => Ok(PartialLookup::Partial { handle: ("target", libc::S_IFDIR), remaining: "foo".into(), last_error: ErrorKind::OsError(Some(libc::ENOENT)), }); } } resolve_tests! { [with_nosymfollow_root] { #[cfg(feature = "_test_as_root")] symlink_regular_symfollow: resolve_partial("nosymfollow/goodlink") => Ok(PartialLookup::Complete((".", libc::S_IFDIR))); #[cfg(feature = "_test_as_root")] symlink_itself_nosymfollow: resolve_partial("nosymfollow/badlink") => Ok(PartialLookup::Partial{ handle: ("nosymfollow", libc::S_IFDIR), remaining: "badlink".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); #[cfg(feature = "_test_as_root")] symlink_inside_nosymfollow_dir: resolve_partial("nosymfollow/nosymdir/dir/badlink") => Ok(PartialLookup::Partial{ handle: ("nosymfollow/nosymdir/dir", libc::S_IFDIR), remaining: "badlink".into(), last_error: ErrorKind::OsError(Some(libc::ELOOP)), }); #[cfg(feature = "_test_as_root")] symlink_itself_nested_clear_symfollow: resolve_partial("nosymfollow/nosymdir/dir/goodlink") => Ok(PartialLookup::Complete((".", libc::S_IFDIR))); #[cfg(feature = "_test_as_root")] symlink_inside_nested_clear_symfollow: resolve_partial("nosymfollow/nosymdir/dir/foo/yessymdir/bar/goodlink") => Ok(PartialLookup::Complete((".", libc::S_IFDIR))); } } mod utils { use crate::{ error::ErrorKind, flags::OpenFlags, resolvers::PartialLookup, tests::{ common::{self as tests_common, LookupResult}, traits::RootImpl, }, utils::FdExt, Root, }; use std::{os::unix::fs::MetadataExt, path::Path}; use anyhow::{Context, Error}; use pretty_assertions::assert_eq; pub(super) fn with_basic_tree(func: F) -> Result<(), Error> where F: FnOnce(Root) -> Result<(), Error>, { let root_dir = tests_common::create_basic_tree()?; let root = Root::open(&root_dir)?; let res = func(root); let _root_dir = root_dir; // make sure tmpdir is kept alive res } pub(super) fn with_nosymfollow_root(func: F) -> Result<(), Error> where F: FnOnce(Root) -> Result<(), Error>, { tests_common::in_mnt_ns(|| { let root_dir = tests_common::create_basic_tree()?; let root = Root::open(&root_dir)?; tests_common::mask_nosymfollow(root_dir.path())?; let res = func(root); let _root_dir = root_dir; // make sure tmpdir is kept alive res }) } pub(super) fn check_root_resolve_partial( root: &Root, unsafe_path: impl AsRef, no_follow_trailing: bool, expected: Result, ErrorKind>, ) -> Result<(), Error> { let root_dir = root.as_unsafe_path_unchecked()?; let unsafe_path = unsafe_path.as_ref(); let result = root .resolver() .resolve_partial(root, unsafe_path, no_follow_trailing) .map(|lookup_result| { let (path, file_type) = { let file = lookup_result.as_inner_handle(); ( file.as_unsafe_path_unchecked() .expect("should be able to get real path of handle"), file.metadata() .expect("should be able to fstat handle") .mode() & libc::S_IFMT, ) }; match lookup_result { PartialLookup::Complete(handle) => { (handle, PartialLookup::Complete((path, file_type))) } PartialLookup::Partial { handle, remaining, last_error, } => ( handle, PartialLookup::Partial { handle: (path, file_type), remaining, last_error: last_error.kind(), }, ), } }); let ((handle, lookup_result), expected_lookup_result) = match (result, expected) { (Ok((handle, lookup_result)), Ok(expected_lookup_result)) => { let (path, file_type) = { let (path, file_type) = expected_lookup_result.as_inner_handle(); (root_dir.join(path.trim_start_matches('/')), *file_type) }; let expected_lookup_result = match expected_lookup_result { PartialLookup::Complete(_) => PartialLookup::Complete((path, file_type)), PartialLookup::Partial { handle: _, remaining, last_error, } => PartialLookup::Partial { handle: (path, file_type), remaining, last_error, }, }; ((handle, lookup_result), expected_lookup_result) } (result, expected) => { tests_common::check_err(&result, &expected) .with_context(|| format!("resolve partial {unsafe_path:?}"))?; assert!( result.is_err(), "we should never see an Ok(handle) after check_err if we expected {expected:?}" ); return Ok(()); } }; assert_eq!( lookup_result, expected_lookup_result, "partial_lookup({unsafe_path:?}, {no_follow_trailing}) result mismatch", ); let (_, real_file_type) = lookup_result.as_inner_handle(); match *real_file_type { libc::S_IFDIR => { tests_common::check_reopen(&handle, OpenFlags::O_RDONLY, None)?; tests_common::check_reopen(&handle, OpenFlags::O_DIRECTORY, None)?; } libc::S_IFREG => { tests_common::check_reopen(&handle, OpenFlags::O_RDWR, None)?; tests_common::check_reopen(&handle, OpenFlags::O_DIRECTORY, Some(libc::ENOTDIR))?; } libc::S_IFLNK => { assert!( no_follow_trailing, "we must only get a symlink handle if no_follow_trailing is set" ); tests_common::check_reopen(&handle, OpenFlags::O_RDONLY, Some(libc::ELOOP)) .context("reopen(O_RDONLY) of a symlink handle should fail with ELOOP")?; tests_common::check_reopen(&handle, OpenFlags::O_PATH, Some(libc::ELOOP)) .context("reopen(O_PATH) of a symlink handle should fail with ELOOP")?; tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW, Some(libc::ELOOP), ) .context("reopen(O_PATH|O_NOFOLLOW) of a symlink handle should fail with ELOOP")?; } _ => { tests_common::check_reopen(&handle, OpenFlags::O_PATH, None)?; tests_common::check_reopen( &handle, OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, Some(libc::ENOTDIR), )?; } } Ok(()) } } pathrs-0.2.1/src/tests/test_root_ops.rs000064400000000000000000002163361046102023000163420ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #[cfg(feature = "capi")] use crate::tests::capi; use crate::{ error::ErrorKind, flags::{OpenFlags, RenameFlags}, resolvers::ResolverBackend, syscalls, tests::common as tests_common, InodeType, Root, }; use std::{fs::Permissions, os::unix::fs::PermissionsExt}; use anyhow::Error; macro_rules! root_op_tests { ($(#[$meta:meta])* fn $test_name:ident ($root_var:ident) $body:block) => { paste::paste! { #[test] $(#[$meta])* fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; let $root_var = Root::open(&root_dir)?; $body } #[test] $(#[$meta])* fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; let root = Root::open(&root_dir)?; let $root_var = root.as_ref(); $body } #[test] $(#[$meta])* fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; let $root_var = Root::open(&root_dir)? .with_resolver_backend(ResolverBackend::KernelOpenat2); if !$root_var.resolver_backend().supported() { // Skip if not supported. return Ok(()); } $body } #[test] $(#[$meta])* fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; let root = Root::open(&root_dir)?; let $root_var = root .as_ref() .with_resolver_backend(ResolverBackend::KernelOpenat2); if !$root_var.resolver_backend().supported() { // Skip if not supported. return Ok(()); } $body } #[test] $(#[$meta])* fn []() -> Result<(), Error> { // This test only makes sense if openat2 is supported (i.e., the // default resolver is openat2 -- otherwise the default test // already tested this case). if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } let root_dir = tests_common::create_basic_tree()?; let $root_var = Root::open(&root_dir)? .with_resolver_backend(ResolverBackend::EmulatedOpath); // EmulatedOpath is always supported. assert!( $root_var.resolver_backend().supported(), "emulated opath is always supported", ); $body } #[test] $(#[$meta])* fn []() -> Result<(), Error> { // This test only makes sense if openat2 is supported (i.e., the // default resolver is openat2 -- otherwise the default test // already tested this case). if !*syscalls::OPENAT2_IS_SUPPORTED { // skip this test return Ok(()); } let root_dir = tests_common::create_basic_tree()?; let root = Root::open(&root_dir)?; let $root_var = root .as_ref() .with_resolver_backend(ResolverBackend::EmulatedOpath); // EmulatedOpath is always supported. assert!( $root_var.resolver_backend().supported(), "emulated opath is always supported", ); $body } #[test] #[cfg(feature = "capi")] $(#[$meta])* fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; let $root_var = capi::CapiRoot::open(&root_dir)?; $body } } }; ($(#[$meta:meta])* @mknod fn $test_name:ident ($path:expr) $make_inode_type:block => $expected_result:expr) => { root_op_tests!{ $(#[$meta])* fn $test_name(root) { let inode_type = $make_inode_type; utils::check_root_create(&root, $path, inode_type, $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl mkfile $test_name:ident ($path:expr, $mode:literal) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* @mknod fn $test_name ($path) { InodeType::File(Permissions::from_mode($mode)) } => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl mkdir $test_name:ident ($path:expr, $mode:literal) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* @mknod fn $test_name ($path) { InodeType::Directory(Permissions::from_mode($mode)) } => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl symlink $test_name:ident ($path:expr, $target:expr) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* @mknod fn $test_name ($path) { InodeType::Symlink($target.into()) } => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl hardlink $test_name:ident ($path:expr, $target:expr) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* @mknod fn $test_name ($path) { InodeType::Hardlink($target.into()) } => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl mkfifo $test_name:ident ($path:expr, $mode:literal) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* @mknod fn $test_name ($path) { InodeType::Fifo(Permissions::from_mode($mode)) } => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl mkblk $test_name:ident ($path:expr, $mode:literal, $major:literal, $minor:literal) => $expected_result:expr) => { root_op_tests!{ #[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))] $(#[cfg_attr(not($ignore_meta), ignore)])* @mknod fn $test_name ($path) { InodeType::BlockDevice(Permissions::from_mode($mode), libc::makedev($major, $minor)) } => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl mkchar $test_name:ident ($path:expr, $mode:literal, $major:literal, $minor:literal) => $expected_result:expr) => { root_op_tests!{ #[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))] $(#[cfg_attr(not($ignore_meta), ignore)])* @mknod fn $test_name ($path) { InodeType::CharacterDevice(Permissions::from_mode($mode), libc::makedev($major, $minor)) } => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl create_file $test_name:ident ($path:expr, $($oflag:ident)|+, $mode:literal) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* fn $test_name(root) { utils::check_root_create_file(&root, $path, $(OpenFlags::$oflag)|*, &Permissions::from_mode($mode), $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl open_subpath $test_name:ident ($path:expr, $($oflag:ident)|+) => $expected_result:expr) => { root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* fn $test_name(root) { utils::check_root_open_subpath(&root, $path, $(OpenFlags::$oflag)|*, $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl remove_dir $test_name:ident ($path:expr) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* fn $test_name(root) { utils::check_root_remove_dir(&root, $path, $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl remove_file $test_name:ident ($path:expr) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* fn $test_name(root) { utils::check_root_remove_file(&root, $path, $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl remove_all $test_name:ident ($path:expr) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* fn $test_name(root) { utils::check_root_remove_all(&root, $path, $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl rename $test_name:ident ($src_path:expr, $dst_path:expr, $rflags:expr) => $expected_result:expr) => { root_op_tests!{ $(#[cfg_attr(not($ignore_meta), ignore)])* fn $test_name(root) { utils::check_root_rename(&root, $src_path, $dst_path, $rflags, $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl mkdir_all $test_name:ident ($path:expr, $mode:expr) => $expected_result:expr) => { root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* fn $test_name(root) { utils::check_root_mkdir_all(&root, $path, Permissions::from_mode($mode), $expected_result) } } }; ($(#[cfg($ignore_meta:meta)])* @impl-race mkdir_all_racing [#$num_threads:expr] $test_name:ident ($path:expr, $mode:expr) => $expected_result:expr) => { paste::paste! { root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* fn [<$test_name _ $num_threads threads>](root) { utils::check_root_mkdir_all_racing($num_threads, &root, $path, Permissions::from_mode($mode), $expected_result) } } } }; ($(#[cfg($ignore_meta:meta)])* @impl-race remove_all_racing [#$num_threads:expr] $test_name:ident ($path:expr) => $expected_result:expr) => { paste::paste! { root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* fn [<$test_name _ $num_threads threads>](root) { utils::check_root_remove_all_racing($num_threads, &root, $path, $expected_result) } } } }; ($(#[cfg($ignore_meta:meta)])* @impl-race $op_name:ident $test_name:ident ( $($args:tt)* ) => $expected_result:expr) => { root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race $op_name [#2] $test_name( $($args)* ) => $expected_result } root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race $op_name [#4] $test_name( $($args)* ) => $expected_result } root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race $op_name [#8] $test_name( $($args)* ) => $expected_result } root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race $op_name [#16] $test_name( $($args)* ) => $expected_result } root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race $op_name [#32] $test_name( $($args)* ) => $expected_result } root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race $op_name [#64] $test_name( $($args)* ) => $expected_result } // This test fails fairly frequently in our GHA CI because the open file // limit is quite small. In principle the 64-parallel test should // already be more than enough. /* root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race $op_name [#128] $test_name( $($args)* ) => $expected_result } */ }; ($(#[cfg($ignore_meta:meta)])* @impl mkdir_all_racing $test_name:ident ( $($args:tt)* ) => $expected_result:expr) => { root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race mkdir_all_racing $test_name( $($args)* ) => $expected_result } }; ($(#[cfg($ignore_meta:meta)])* @impl remove_all_racing $test_name:ident ( $($args:tt)* ) => $expected_result:expr) => { root_op_tests! { $(#[cfg_attr(not($ignore_meta), ignore)])* @impl-race remove_all_racing $test_name( $($args)* ) => $expected_result } }; // root_tests!{ // ... // } ($($(#[cfg($ignore_meta:meta)])* $test_name:ident: $op_name:ident ( $($args:tt)* ) => $expected_result:expr );+ $(;)?) => { paste::paste! { $( root_op_tests!{ $(#[cfg($ignore_meta)])* @impl $op_name [<$op_name _ $test_name>]( $($args)* ) => $expected_result } )* } }; } root_op_tests! { plain: mkfile("abc", 0o444) => Ok(("abc", libc::S_IFREG | 0o444)); exist_file: mkfile("b/c/file", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: mkfile("a", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_symlink: mkfile("b-file", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dangling_symlink: mkfile("a-fake1", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); parentdir_trailing_slash: mkfile("b/c//foobar", 0o755) => Ok(("b/c/foobar", libc::S_IFREG | 0o755)); parentdir_trailing_dot: mkfile("b/c/./foobar", 0o755) => Ok(("b/c/foobar", libc::S_IFREG | 0o755)); parentdir_trailing_dotdot: mkfile("b/c/../foobar", 0o755) => Ok(("b/foobar", libc::S_IFREG | 0o755)); trailing_slash: mkfile("foobar/", 0o755) => Err(ErrorKind::InvalidArgument); trailing_dot: mkfile("foobar/.", 0o755) => Err(ErrorKind::InvalidArgument); trailing_dotdot: mkfile("foobar/..", 0o755) => Err(ErrorKind::InvalidArgument); root_slash: mkfile("/", 0o755) => Err(ErrorKind::InvalidArgument); root_slash2: mkfile("//", 0o755) => Err(ErrorKind::InvalidArgument); root_dot: mkfile(".", 0o755) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: mkfile("./", 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot: mkfile("..", 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: mkfile("../", 0o755) => Err(ErrorKind::InvalidArgument); plain: mkdir("abc", 0o311) => Ok(("abc", libc::S_IFDIR | 0o311)); exist_file: mkdir("b/c/file", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: mkdir("a", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_symlink: mkdir("b-file", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dangling_symlink: mkdir("a-fake1", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); parentdir_trailing_slash: mkdir("b/c//foobar", 0o755) => Ok(("b/c/foobar", libc::S_IFDIR | 0o755)); parentdir_trailing_dot: mkdir("b/c/./foobar", 0o755) => Ok(("b/c/foobar", libc::S_IFDIR | 0o755)); parentdir_trailing_dotdot: mkdir("b/c/../foobar", 0o755) => Ok(("b/foobar", libc::S_IFDIR | 0o755)); trailing_slash1: mkdir("b/c/abc/", 0o755) => Ok(("b/c/abc", libc::S_IFDIR | 0o755)); trailing_slash2: mkdir("b/c/abc///", 0o755) => Ok(("b/c/abc", libc::S_IFDIR | 0o755)); trailing_dot: mkdir("b/c/abc/.", 0o755) => Err(ErrorKind::InvalidArgument); trailing_dotdot: mkdir("b/c/abc/..", 0o755) => Err(ErrorKind::InvalidArgument); root_slash: mkdir("/", 0o755) => Err(ErrorKind::InvalidArgument); root_slash2: mkdir("//", 0o755) => Err(ErrorKind::InvalidArgument); root_dot: mkdir(".", 0o755) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: mkdir("./", 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot: mkdir("..", 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: mkdir("../", 0o755) => Err(ErrorKind::InvalidArgument); plain: symlink("abc", "/NEWLINK") => Ok(("abc", libc::S_IFLNK | 0o777)); exist_file: symlink("b/c/file", "/NEWLINK") => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: symlink("a", "/NEWLINK") => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_symlink: symlink("b-file", "/NEWLINK") => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dangling_symlink: symlink("a-fake1", "/NEWLINK") => Err(ErrorKind::OsError(Some(libc::EEXIST))); parentdir_trailing_slash: symlink("b/c//foobar", "/SOMELINK") => Ok(("b/c/foobar", libc::S_IFLNK | 0o777)); parentdir_trailing_dot: symlink("b/c/./foobar", "/SOMELINK") => Ok(("b/c/foobar", libc::S_IFLNK | 0o777)); parentdir_trailing_dotdot: symlink("b/c/../foobar", "/SOMELINK") => Ok(("b/foobar", libc::S_IFLNK | 0o777)); trailing_slash1: symlink("foobar/", "/SOMELINK") => Err(ErrorKind::InvalidArgument); trailing_slash2: symlink("foobar///", "/SOMELINK") => Err(ErrorKind::InvalidArgument); trailing_dot: symlink("foobar/.", "/SOMELINK") => Err(ErrorKind::InvalidArgument); trailing_dotdot: symlink("foobar/..", "/foobar") => Err(ErrorKind::InvalidArgument); root_slash: symlink("/", "/") => Err(ErrorKind::InvalidArgument); root_slash2: symlink("/", "//") => Err(ErrorKind::InvalidArgument); root_dot: symlink("/", ".") => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: symlink("/", "./") => Err(ErrorKind::InvalidArgument); root_dotdot: symlink("/", "..") => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: symlink("/", "../") => Err(ErrorKind::InvalidArgument); plain: hardlink("abc", "b/c/file") => Ok(("abc", libc::S_IFREG | 0o644)); exist_file: hardlink("b/c/file", "/b/c/file") => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: hardlink("a", "/b/c/file") => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_symlink: hardlink("b-file", "/b/c/file") => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dangling_symlink: hardlink("a-fake1", "/b/c/file") => Err(ErrorKind::OsError(Some(libc::EEXIST))); to_symlink: hardlink("link", "b-file") => Ok(("link", libc::S_IFLNK | 0o777)); to_dangling_symlink: hardlink("link", "a-fake1") => Ok(("link", libc::S_IFLNK | 0o777)); to_dir: hardlink("abc", "/b/c") => Err(ErrorKind::OsError(Some(libc::EPERM))); parentdir_trailing_slash_dst: hardlink("b/c//foobar", "b/c/file") => Ok(("b/c/foobar", libc::S_IFREG | 0o644)); parentdir_trailing_slash_src: hardlink("link", "b/c//file") => Ok(("link", libc::S_IFREG | 0o644)); parentdir_trailing_dot_dst: hardlink("b/c/./foobar", "b/c/file") => Ok(("b/c/foobar", libc::S_IFREG | 0o644)); parentdir_trailing_dot_src: hardlink("link", "b/c/./file") => Ok(("link", libc::S_IFREG | 0o644)); parentdir_trailing_dotdot_dst: hardlink("b/c/../foobar", "b/c/file") => Ok(("b/foobar", libc::S_IFREG | 0o644)); parentdir_trailing_dotdot_src: hardlink("link", "b/c/d/../file") => Ok(("link", libc::S_IFREG | 0o644)); trailing_slash_dst1: hardlink("foobar/", "b/c/file") => Err(ErrorKind::InvalidArgument); trailing_slash_dst2: hardlink("foobar///", "b/c/file") => Err(ErrorKind::InvalidArgument); trailing_slash_src1: hardlink("link", "foobar/") => Err(ErrorKind::InvalidArgument); trailing_slash_src2: hardlink("link", "foobar///") => Err(ErrorKind::InvalidArgument); trailing_dot_dst: hardlink("foobar/.", "b/c/file") => Err(ErrorKind::InvalidArgument); trailing_dot_src: hardlink("link", "foobar/.") => Err(ErrorKind::InvalidArgument); trailing_dotdot_dst: hardlink("foobar/..", "b/c/file") => Err(ErrorKind::InvalidArgument); trailing_dotdot_src: hardlink("link", "foobar/..") => Err(ErrorKind::InvalidArgument); root_slash_src: hardlink("/", "b-file") => Err(ErrorKind::InvalidArgument); root_slash_dst: hardlink("b-file", "/") => Err(ErrorKind::InvalidArgument); root_slash2_src: hardlink("//", "b-file") => Err(ErrorKind::InvalidArgument); root_slash2_dst: hardlink("b-file", "//") => Err(ErrorKind::InvalidArgument); root_dot_src: hardlink(".", "b-file") => Err(ErrorKind::InvalidArgument); root_dot_dst: hardlink("b-file", ".") => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash_srt: hardlink("./", "b-file") => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash_dst: hardlink("b-file", "./") => Err(ErrorKind::InvalidArgument); root_dotdot_src: hardlink("..", "b-file") => Err(ErrorKind::InvalidArgument); root_dotdot_dst: hardlink("b-file", "..") => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash_src: hardlink("../", "b-file") => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash_dst: hardlink("b-file", "../") => Err(ErrorKind::InvalidArgument); plain: mkfifo("abc", 0o222) => Ok(("abc", libc::S_IFIFO | 0o222)); exist_file: mkfifo("b/c/file", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: mkfifo("a", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_symlink: mkfifo("b-file", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dangling_symlink: mkfifo("a-fake1", 0o444) => Err(ErrorKind::OsError(Some(libc::EEXIST))); parentdir_trailing_slash: mkfifo("b/c//foobar", 0o123) => Ok(("b/c/foobar", libc::S_IFIFO | 0o123)); parentdir_trailing_dot: mkfifo("b/c/./foobar", 0o456) => Ok(("b/c/foobar", libc::S_IFIFO | 0o456)); parentdir_trailing_dotdot: mkfifo("b/c/../foobar", 0o321) => Ok(("b/foobar", libc::S_IFIFO | 0o321)); trailing_slash1: mkfifo("foobar/", 0o755) => Err(ErrorKind::InvalidArgument); trailing_slash2: mkfifo("foobar///", 0o755) => Err(ErrorKind::InvalidArgument); trailing_dot: mkfifo("foobar/.", 0o755) => Err(ErrorKind::InvalidArgument); trailing_dotdot: mkfifo("foobar/..", 0o755) => Err(ErrorKind::InvalidArgument); root_slash: mkfifo("/", 0o755) => Err(ErrorKind::InvalidArgument); root_slash2: mkfifo("//", 0o755) => Err(ErrorKind::InvalidArgument); root_dot: mkfifo(".", 0o755) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: mkfifo("./", 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot: mkfifo("..", 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: mkfifo("../", 0o755) => Err(ErrorKind::InvalidArgument); plain: mkblk("abc", 0o001, 123, 456) => Ok(("abc", libc::S_IFBLK | 0o001)); exist_file: mkblk("b/c/file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: mkblk("a", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_symlink: mkblk("b-file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dangling_symlink: mkblk("a-fake1", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); parentdir_trailing_slash: mkblk("b/c//foobar", 0o123, 123, 456) => Ok(("b/c/foobar", libc::S_IFBLK | 0o123)); parentdir_trailing_dot: mkblk("b/c/./foobar", 0o456, 123, 456) => Ok(("b/c/foobar", libc::S_IFBLK | 0o456)); parentdir_trailing_dotdot: mkblk("b/c/../foobar", 0o321, 123, 456) => Ok(("b/foobar", libc::S_IFBLK | 0o321)); trailing_slash1: mkblk("foobar/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); trailing_slash2: mkblk("foobar///", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); trailing_dot: mkblk("foobar/.", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); trailing_dotdot: mkblk("foobar/..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_slash: mkblk("/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_slash2: mkblk("//", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dot: mkblk(".", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: mkblk("./", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dotdot: mkblk("..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: mkblk("../", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); plain: mkchar("abc", 0o010, 111, 222) => Ok(("abc", libc::S_IFCHR | 0o010)); exist_file: mkchar("b/c/file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: mkchar("a", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_symlink: mkchar("b-file", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dangling_symlink: mkchar("a-fake1", 0o444, 123, 456) => Err(ErrorKind::OsError(Some(libc::EEXIST))); parentdir_trailing_slash: mkchar("b/c//foobar", 0o123, 123, 456) => Ok(("b/c/foobar", libc::S_IFCHR | 0o123)); parentdir_trailing_dot: mkchar("b/c/./foobar", 0o456, 123, 456) => Ok(("b/c/foobar", libc::S_IFCHR | 0o456)); parentdir_trailing_dotdot: mkchar("b/c/../foobar", 0o321, 123, 456) => Ok(("b/foobar", libc::S_IFCHR | 0o321)); trailing_slash1: mkchar("foobar/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); trailing_slash2: mkchar("foobar///", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); trailing_dot: mkchar("foobar/.", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); trailing_dotdot: mkchar("foobar/..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_slash: mkchar("/", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_slash2: mkchar("//", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dot: mkchar(".", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: mkchar("./", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dotdot: mkchar("..", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: mkchar("../", 0o755, 123, 456) => Err(ErrorKind::InvalidArgument); plain: create_file("abc", O_RDONLY, 0o100) => Ok("abc"); trailing_slash1: create_file("b/c/abc/", O_RDONLY, 0o222) => Err(ErrorKind::InvalidArgument); trailing_slash2: create_file("b/c/abc///", O_RDONLY, 0o222) => Err(ErrorKind::InvalidArgument); trailing_slash3: create_file("b/c/abc//./", O_RDONLY, 0o222) => Err(ErrorKind::InvalidArgument); oexcl_plain: create_file("abc", O_EXCL|O_RDONLY, 0o100) => Ok("abc"); exist: create_file("b/c/file", O_RDONLY, 0o100) => Ok("b/c/file"); oexcl_exist: create_file("a", O_EXCL|O_RDONLY, 0o100) => Err(ErrorKind::OsError(Some(libc::EEXIST))); exist_dir: create_file("b/c/d", O_RDONLY, 0o100) => Err(ErrorKind::OsError(Some(libc::EISDIR))); symlink: create_file("b-file", O_RDONLY, 0o100) => Err(ErrorKind::OsError(Some(libc::ELOOP))); oexcl_symlink: create_file("b-file", O_EXCL|O_RDONLY, 0o100) => Err(ErrorKind::OsError(Some(libc::EEXIST))); oexcl_dangling_symlink: create_file("a-fake1", O_EXCL|O_RDONLY, 0o100) => Err(ErrorKind::OsError(Some(libc::EEXIST))); parentdir_trailing_slash: create_file("b/c//foobar", O_RDONLY, 0o123) => Ok("b/c/foobar"); parentdir_trailing_dot: create_file("b/c/./foobar", O_RDONLY, 0o456) => Ok("b/c/foobar"); parentdir_trailing_dotdot: create_file("b/c/../foobar", O_RDONLY, 0o321) => Ok("b/foobar"); trailing_slash: create_file("foobar/", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); trailing_dot: create_file("foobar/.", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); trailing_dotdot: create_file("foobar/..", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); // TODO: Figure out how to match the success case... //otmpfile: create_file("b/c", O_TMPFILE|O_RDWR, 0o755) => Ok(_); // has a random name otmpfile_enoent: create_file("b/c/newfile", O_TMPFILE|O_RDWR, 0o755) => Err(ErrorKind::OsError(Some(libc::ENOENT))); otmpfile_exist_file: create_file("b/c/file", O_TMPFILE|O_RDWR, 0o755) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); //otmpfile_symlink: create_file("root-link", O_TMPFILE|O_RDWR, 0o755) => Ok(_); // has a random name //otmpfile_symlink_parentdir: create_file("e/f", O_TMPFILE|O_RDWR, 0o755) => Ok(_); // has a random name //otmpfile_root_slash: create_file("/", O_TMPFILE|O_RDWR, 0o755) => Ok(_); // has a random name //otmpfile_root_dot: create_file(".", O_TMPFILE|O_RDWR, 0o755) => Ok(_); // has a random name root_slash: create_file("/", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); root_slash2: create_file("//", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); root_dot: create_file(".", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: create_file("./", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot: create_file("..", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: create_file("../", O_RDONLY, 0o755) => Err(ErrorKind::InvalidArgument); ocreat: open_subpath("abc", O_CREAT|O_RDONLY) => Err(ErrorKind::InvalidArgument); oexcl: open_subpath("abc", O_EXCL|O_RDONLY) => Err(ErrorKind::InvalidArgument); ocreat_oexcl: open_subpath("abc", O_CREAT|O_EXCL|O_RDONLY) => Err(ErrorKind::InvalidArgument); noexist: open_subpath("abc", O_RDONLY) => Err(ErrorKind::OsError(Some(libc::ENOENT))); exist_file: open_subpath("b/c/file", O_RDONLY) => Ok("b/c/file"); exist_file_trailing_slash1: open_subpath("b/c/file/", O_RDONLY) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); exist_file_trailing_slash2: open_subpath("b/c/file///", O_RDONLY) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); exist_file_trailing_slash3: open_subpath("b/c/file//./", O_RDONLY) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); exist_dir: open_subpath("b/c/d", O_RDONLY) => Ok("b/c/d"); exist_dir_trailing_slash1: open_subpath("b/c/d/", O_RDONLY) => Ok("b/c/d"); exist_dir_trailing_slash2: open_subpath("b/c/d///", O_RDONLY) => Ok("b/c/d"); exist_dir_trailing_slash3: open_subpath("b/c/d//./", O_RDONLY) => Ok("b/c/d"); symlink: open_subpath("b-file", O_RDONLY) => Ok("b/c/file"); ocreat_symlink: open_subpath("b-file", O_CREAT|O_RDONLY) => Err(ErrorKind::InvalidArgument); nofollow_symlink: open_subpath("b-file", O_NOFOLLOW|O_RDONLY) => Err(ErrorKind::OsError(Some(libc::ELOOP))); opath_nofollow_symlink: open_subpath("b-file", O_PATH|O_NOFOLLOW) => Ok("b-file"); nofollow_odir_symlink: open_subpath("b-file", O_DIRECTORY|O_NOFOLLOW) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); opath_nofollow_odir_symlink: open_subpath("b-file", O_DIRECTORY|O_PATH|O_NOFOLLOW) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling_symlink: open_subpath("a-fake1", O_RDONLY) => Err(ErrorKind::OsError(Some(libc::ENOENT))); ocreat_dangling_symlink: open_subpath("a-fake1", O_CREAT|O_RDONLY) => Err(ErrorKind::InvalidArgument); root_slash: open_subpath("/", O_RDONLY) => Ok("."); root_dot: open_subpath(".", O_RDONLY) => Ok("."); root_dotdot: open_subpath("..", O_RDONLY) => Ok("."); empty_dir: remove_dir("a") => Ok(()); empty_dir: remove_file("a") => Err(ErrorKind::OsError(Some(libc::EISDIR))); empty_dir: remove_all("a") => Ok(()); nonempty_dir: remove_dir("b") => Err(ErrorKind::OsError(Some(libc::ENOTEMPTY))); nonempty_dir: remove_file("b") => Err(ErrorKind::OsError(Some(libc::EISDIR))); nonempty_dir: remove_all("b") => Ok(()); deep_dir: remove_dir("deep-rmdir") => Err(ErrorKind::OsError(Some(libc::ENOTEMPTY))); deep_dir: remove_file("deep-rmdir") => Err(ErrorKind::OsError(Some(libc::EISDIR))); deep_dir: remove_all("deep-rmdir") => Ok(()); file: remove_dir("b/c/file") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); file: remove_file("b/c/file") => Ok(()); file: remove_all("b/c/file") => Ok(()); fifo: remove_dir("b/fifo") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); fifo: remove_file("b/fifo") => Ok(()); fifo: remove_all("b/fifo") => Ok(()); sock: remove_dir("b/sock") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); sock: remove_file("b/sock") => Ok(()); sock: remove_all("b/sock") => Ok(()); noexist: remove_dir("abc") => Err(ErrorKind::OsError(Some(libc::ENOENT))); noexist: remove_file("abc") => Err(ErrorKind::OsError(Some(libc::ENOENT))); noexist: remove_all("abc") => Ok(()); noexist_trailing_slash: remove_dir("abc/") => Err(ErrorKind::OsError(Some(libc::ENOENT))); noexist_trailing_slash: remove_file("abc/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); noexist_trailing_slash: remove_all("abc/") => Ok(()); symlink: remove_dir("b-file") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); symlink: remove_file("b-file") => Ok(()); symlink: remove_all("b-file") => Ok(()); dangling_symlink: remove_dir("a-fake1") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling_symlink: remove_file("a-fake1") => Ok(()); dangling_symlink: remove_all("a-fake1") => Ok(()); parentdir_trailing_slash1: remove_dir("b/c/d/e//f") => Ok(()); parentdir_trailing_slash1: remove_file("b/c//file") => Ok(()); parentdir_trailing_slash1: remove_all("b//c") => Ok(()); parentdir_trailing_slash2: remove_dir("b/c/d/e////f") => Ok(()); parentdir_trailing_slash2: remove_file("b/c////file") => Ok(()); parentdir_trailing_slash2: remove_all("b////c") => Ok(()); parentdir_trailing_dot: remove_dir("b/c/d/e/./f") => Ok(()); parentdir_trailing_dot: remove_file("b/c/./file") => Ok(()); parentdir_trailing_dot: remove_all("b/./c") => Ok(()); parentdir_trailing_dotdot: remove_dir("b/c/d/e/f/../f") => Ok(()); parentdir_trailing_dotdot: remove_file("b/c/d/../file") => Ok(()); parentdir_trailing_dotdot: remove_all("b/c/../c") => Ok(()); dir_trailing_slash1: remove_dir("a/") => Ok(()); dir_trailing_slash1: remove_file("a/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dir_trailing_slash1: remove_all("a/") => Ok(()); dir_trailing_slash2: remove_dir("a///") => Ok(()); dir_trailing_slash2: remove_file("a///") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dir_trailing_slash2: remove_all("a///") => Ok(()); dir_trailing_dot: remove_dir("a/.") => Err(ErrorKind::InvalidArgument); dir_trailing_dot: remove_file("a/.") => Err(ErrorKind::InvalidArgument); dir_trailing_dot: remove_all("a/.") => Err(ErrorKind::InvalidArgument); dir_trailing_dotdot: remove_dir("b/c/..") => Err(ErrorKind::InvalidArgument); dir_trailing_dotdot: remove_file("b/c/..") => Err(ErrorKind::InvalidArgument); dir_trailing_dotdot: remove_all("b/c/..") => Err(ErrorKind::InvalidArgument); file_trailing_slash: remove_dir("b/c/file/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); file_trailing_slash: remove_file("b/c/file/") => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); file_trailing_slash: remove_all("b/c/file/") => Ok(()); file_trailing_dot: remove_dir("b/c/file/.") => Err(ErrorKind::InvalidArgument); file_trailing_dot: remove_file("b/c/file/.") => Err(ErrorKind::InvalidArgument); file_trailing_dot: remove_all("b/c/file/.") => Err(ErrorKind::InvalidArgument); file_trailing_dotdot: remove_dir("b/c/file/..") => Err(ErrorKind::InvalidArgument); file_trailing_dotdot: remove_file("b/c/file/..") => Err(ErrorKind::InvalidArgument); file_trailing_dotdot: remove_all("b/c/file/..") => Err(ErrorKind::InvalidArgument); root_slash: remove_dir("/") => Err(ErrorKind::InvalidArgument); root_slash: remove_file("/") => Err(ErrorKind::InvalidArgument); root_slash: remove_all("/") => Err(ErrorKind::InvalidArgument); root_slash2: remove_dir("//") => Err(ErrorKind::InvalidArgument); root_slash2: remove_file("//") => Err(ErrorKind::InvalidArgument); root_slash2: remove_all("//") => Err(ErrorKind::InvalidArgument); root_dot: remove_dir(".") => Err(ErrorKind::InvalidArgument); root_dot: remove_file(".") => Err(ErrorKind::InvalidArgument); root_dot: remove_all(".") => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: remove_dir("./") => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: remove_file("./") => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash: remove_all("./") => Err(ErrorKind::InvalidArgument); root_dotdot: remove_dir("..") => Err(ErrorKind::InvalidArgument); root_dotdot: remove_file("..") => Err(ErrorKind::InvalidArgument); root_dotdot: remove_all("..") => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: remove_dir("../") => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: remove_file("../") => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash: remove_all("../") => Err(ErrorKind::InvalidArgument); empty_dir: rename("a", "aa", RenameFlags::empty()) => Ok(()); nonempty_dir: rename("b", "bb", RenameFlags::empty()) => Ok(()); file: rename("b/c/file", "bb-file", RenameFlags::empty()) => Ok(()); parentdir_trailing_slash_src1: rename("b/c//d", "aa", RenameFlags::empty()) => Ok(()); parentdir_trailing_slash_dst1: rename("a", "b//aa", RenameFlags::empty()) => Ok(()); parentdir_trailing_slash_src2: rename("b/c////d", "aa", RenameFlags::empty()) => Ok(()); parentdir_trailing_slash_dst2: rename("a", "b////aa", RenameFlags::empty()) => Ok(()); parentdir_trailing_dot_src: rename("b/c/./file", "aa", RenameFlags::empty()) => Ok(()); parentdir_trailing_dot_dst: rename("a", "b/./aa", RenameFlags::empty()) => Ok(()); parentdir_trailing_dotdot_src: rename("b/c/d/../file", "aa", RenameFlags::empty()) => Ok(()); parentdir_trailing_dotdot_dst: rename("a", "b/c/../aa", RenameFlags::empty()) => Ok(()); dir_trailing_slash_src1: rename("a/", "aa", RenameFlags::empty()) => Ok(()); dir_trailing_slash_dst1: rename("a", "aa/", RenameFlags::empty()) => Ok(()); dir_trailing_slash_src2: rename("a///", "aa", RenameFlags::empty()) => Ok(()); dir_trailing_slash_dst2: rename("a", "aa///", RenameFlags::empty()) => Ok(()); dir_trailing_dot_src: rename("a/.", "aa", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); dir_trailing_dot_dst: rename("a", "aa/.", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); dir_trailing_dotdot_src: rename("b/c/..", "aa", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); dir_trailing_dotdot_dst: rename("a", "aa/..", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); file_trailing_slash_src1: rename("b/c/file/", "aa", RenameFlags::empty()) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); file_trailing_slash_dst1: rename("b/c/file", "aa/", RenameFlags::empty()) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); file_trailing_slash_src2: rename("b/c/file///", "aa", RenameFlags::empty()) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); file_trailing_slash_dst2: rename("b/c/file", "aa///", RenameFlags::empty()) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); file_trailing_dot_src: rename("b/c/file/.", "aa", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); file_trailing_dot_dst: rename("b/c/file", "aa/.", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); file_trailing_dotdot_src: rename("b/c/file/..", "aa", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); file_trailing_dotdot_dst: rename("b/c/file", "aa/..", RenameFlags::empty()) => Err(ErrorKind::InvalidArgument); noreplace_plain: rename("a", "aa", RenameFlags::RENAME_NOREPLACE) => Ok(()); noreplace_dir_trailing_slash_from: rename("a/", "aa", RenameFlags::RENAME_NOREPLACE) => Ok(()); noreplace_dir_trailing_slash_to: rename("a", "aa/", RenameFlags::RENAME_NOREPLACE) => Ok(()); noreplace_dir_trailing_slash_fromto: rename("a/", "aa/", RenameFlags::RENAME_NOREPLACE) => Ok(()); noreplace_dir_trailing_slash_many: rename("a///", "aa///", RenameFlags::RENAME_NOREPLACE) => Ok(()); 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))); whiteout_dir: rename("a", "aa", RenameFlags::RENAME_WHITEOUT) => Ok(()); 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(()); exchange_dir_trailing_slash_to: rename("a", "b/", RenameFlags::RENAME_EXCHANGE) => Ok(()); exchange_dir_trailing_slash_fromto: rename("a/", "b/", RenameFlags::RENAME_EXCHANGE) => Ok(()); exchange_dir_trailing_slash_many: rename("a///", "b///", RenameFlags::RENAME_EXCHANGE) => Ok(()); exchange_difftype: rename("a", "e", RenameFlags::RENAME_EXCHANGE) => Ok(()); exchange_difftype_trailing_slash_from: rename("a/", "e", RenameFlags::RENAME_EXCHANGE) => Ok(()); exchange_difftype_trailing_slash_to: rename("a", "e/", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); exchange_difftype_trailing_slash_fromto: rename("a/", "e/", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); exchange_noexist: rename("a", "aa", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::OsError(Some(libc::ENOENT))); root_slash_src: rename("/", "b-file", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_slash_dst: rename("b-file", "/", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_slash2_src: rename("//", "b-file", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_slash2_dst: rename("b-file", "//", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dot_src: rename(".", "b-file", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dot_dst: rename("b-file", ".", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash_srt: rename("./", "b-file", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dot_trailing_slash_dst: rename("b-file", "./", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dotdot_src: rename("..", "b-file", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dotdot_dst: rename("b-file", "..", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash_src: rename("../", "b-file", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); root_dotdot_trailing_slash_dst: rename("b-file", "../", RenameFlags::RENAME_EXCHANGE) => Err(ErrorKind::InvalidArgument); invalid_mode_type: mkdir_all("foo", libc::S_IFDIR | 0o777) => Err(ErrorKind::InvalidArgument); invalid_mode_garbage: mkdir_all("foo", 0o12340777) => Err(ErrorKind::InvalidArgument); invalid_mode_setuid: mkdir_all("foo", libc::S_ISUID | 0o777) => Err(ErrorKind::InvalidArgument); invalid_mode_setgid: mkdir_all("foo", libc::S_ISGID | 0o777) => Err(ErrorKind::InvalidArgument); existing: mkdir_all("a", 0o711) => Ok(()); basic: mkdir_all("a/b/c/d/e/f/g/h/i/j", 0o711) => Ok(()); trailing_slash_basic: mkdir_all("a/b/c/d/e/f/g/", 0o711) => Ok(()); trailing_slash_many: mkdir_all("a/b/c/d/e/f/g/////////", 0o711) => Ok(()); trailing_slash_complex: mkdir_all("a/b/c/d/e/f/g////./////", 0o711) => Ok(()); sticky: mkdir_all("foo", libc::S_ISVTX | 0o711) => Ok(()); dotdot_in_nonexisting: mkdir_all("a/b/c/d/e/f/g/h/i/j/k/../lmnop", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOENT))); dotdot_in_existing: mkdir_all("b/c/../c/./d/e/f/g/h", 0o711) => Ok(()); dotdot_after_symlink: mkdir_all("e/../dd/ee/ff", 0o711) => Ok(()); // Check that trying to create under a file fails. nondir_trailing: mkdir_all("b/c/file", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); nondir_dotdot: mkdir_all("b/c/file/../d", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); nondir_subdir: mkdir_all("b/c/file/subdir", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); nondir_symlink_trailing: mkdir_all("b-file", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); nondir_symlink_dotdot: mkdir_all("b-file/../d", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); nondir_symlink_subdir: mkdir_all("b-file/subdir", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); // Dangling symlinks are not followed. dangling1_trailing: mkdir_all("a-fake1", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling1_basic: mkdir_all("a-fake1/foo", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling1_dotdot: mkdir_all("a-fake1/../bar/baz", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling2_trailing: mkdir_all("a-fake2", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling2_basic: mkdir_all("a-fake2/foo", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling2_dotdot: mkdir_all("a-fake2/../bar/baz", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling3_trailing: mkdir_all("a-fake3", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling3_basic: mkdir_all("a-fake3/foo", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling3_dotdot: mkdir_all("a-fake3/../bar/baz", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOENT))); // Non-lexical symlinks should work. nonlexical_basic: mkdir_all("target/foo", 0o711) => Ok(()); nonlexical_level1_abs: mkdir_all("link1/target_abs/foo", 0o711) => Ok(()); nonlexical_level1_rel: mkdir_all("link1/target_rel/foo", 0o711) => Ok(()); nonlexical_level2_abs_abs: mkdir_all("link2/link1_abs/target_abs/foo", 0o711) => Ok(()); nonlexical_level2_abs_rel: mkdir_all("link2/link1_abs/target_rel/foo", 0o711) => Ok(()); nonlexical_level2_abs_open: mkdir_all("link2/link1_abs/../target/foo", 0o711) => Ok(()); nonlexical_level2_rel_abs: mkdir_all("link2/link1_rel/target_abs/foo", 0o711) => Ok(()); nonlexical_level2_rel_rel: mkdir_all("link2/link1_rel/target_rel/foo", 0o711) => Ok(()); nonlexical_level2_rel_open: mkdir_all("link2/link1_rel/../target/foo", 0o711) => Ok(()); nonlexical_level3_abs: mkdir_all("link3/target_abs/foo", 0o711) => Ok(()); nonlexical_level3_rel: mkdir_all("link3/target_rel/foo", 0o711) => Ok(()); // But really tricky dangling symlinks should fail. dangling_tricky1_trailing: mkdir_all("link3/deep_dangling1", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling_tricky1_basic: mkdir_all("link3/deep_dangling1/foo", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling_tricky1_dotdot: mkdir_all("link3/deep_dangling1/../bar", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOENT))); dangling_tricky2_trailing: mkdir_all("link3/deep_dangling2", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling_tricky2_basic: mkdir_all("link3/deep_dangling2/foo", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOTDIR))); dangling_tricky2_dotdot: mkdir_all("link3/deep_dangling2/../bar", 0o711) => Err(ErrorKind::OsError(Some(libc::ENOENT))); // And trying to mkdir inside a loop should fail. loop_trailing: mkdir_all("loop/link", 0o711) => Err(ErrorKind::OsError(Some(libc::ELOOP))); loop_basic: mkdir_all("loop/link/foo", 0o711) => Err(ErrorKind::OsError(Some(libc::ELOOP))); loop_dotdot: mkdir_all("loop/link/../foo", 0o711) => Err(ErrorKind::OsError(Some(libc::ELOOP))); // Make sure the S_ISGID handling is correct. setgid_selfdir: mkdir_all("setgid-self/a/b/c/d", 0o711) => Ok(()); #[cfg(feature = "_test_as_root")] setgid_otherdir: mkdir_all("setgid-other/a/b/c/d", 0o711) => Ok(()); parentdir_trailing_slash: mkdir_all("b/c//foobar", 0o711) => Ok(()); parentdir_trailing_dot: mkdir_all("b/c/./foobar", 0o711) => Ok(()); parentdir_trailing_dotdot: mkdir_all("b/c/../foobar", 0o711) => Ok(()); trailing_slash: mkdir_all("foobar/", 0o755) => Ok(()); trailing_dot: mkdir_all("foobar/.", 0o755) => Ok(()); trailing_dotdot: mkdir_all("foobar/..", 0o755) => Err(ErrorKind::OsError(Some(libc::ENOENT))); root_slash: mkdir_all("/", 0o755) => Ok(()); root_slash2: mkdir_all("//", 0o755) => Ok(()); root_dot: mkdir_all(".", 0o755) => Ok(()); root_dot_trailing_slash: mkdir_all("./", 0o755) => Ok(()); root_dotdot: mkdir_all("..", 0o755) => Ok(()); root_dotdot_trailing_slash: mkdir_all("../", 0o755) => Ok(()); // Check that multiple mkdir_alls racing against each other will not result // in a spurious error. plain: mkdir_all_racing("a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q/r/s/t/u/v/w/x/y/z", 0o711) => Ok(()); // Check that multiple rmdir_alls racing against each other will not result // in a spurious error. plain: remove_all_racing("deep-rmdir") => Ok(()); } mod utils { use crate::{ error::{ErrorExt, ErrorKind}, flags::{OpenFlags, RenameFlags}, resolvers::PartialLookup, syscalls, tests::{ common as tests_common, traits::{ErrorImpl, RootImpl}, }, utils::{self, FdExt, PathIterExt}, Handle, InodeType, }; use std::{ fs::Permissions, os::unix::{ fs::{MetadataExt, PermissionsExt}, io::{AsFd, OwnedFd}, }, path::{Path, PathBuf}, sync::{Arc, Barrier}, thread, }; use anyhow::{Context, Error}; use pretty_assertions::{assert_eq, assert_ne}; use rustix::{ fs::{Mode, RawMode}, process as rustix_process, }; fn root_roundtrip(root: R) -> Result { let root_clone = root.try_clone()?; assert_eq!( root.resolver(), root_clone.resolver(), "cloned root should have the same resolver settings" ); let root_fd: OwnedFd = root_clone.into(); Ok(R::from_fd(root_fd, root.resolver())) } pub(super) fn check_root_create( root: R, path: impl AsRef, inode_type: InodeType, expected_result: Result<(&str, RawMode), ErrorKind>, ) -> Result<(), Error> { let path = path.as_ref(); // Just clear the umask so all of the tests can use all of the // permission bits. 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 expected_result = expected_result.map(|(path, mode)| (root_dir.join(path), mode)); match root.create(path, &inode_type) { Err(err) => { tests_common::check_err(&Err::<(), _>(err), &expected_result) .with_context(|| format!("root create {path:?}"))?; } Ok(_) => { let root = root_roundtrip(root)?; let created = root.resolve_nofollow(path)?; let meta = created.metadata()?; let actual_path = created.as_fd().as_unsafe_path_unchecked()?; let actual_mode = meta.mode(); assert_eq!( Ok((actual_path.clone(), actual_mode)), expected_result, "unexpected mode 0o{actual_mode:o} or path {actual_path:?}", ); match inode_type { // No need for extra checks for these types. InodeType::File(_) | InodeType::Directory(_) | InodeType::Fifo(_) => (), // Check InodeType::CharacterDevice(_, dev) | InodeType::BlockDevice(_, dev) => { assert_eq!(meta.rdev(), dev, "device type of mknod mismatch"); } // Check hardlink is the same inode. InodeType::Hardlink(target) => { let target_meta = root.resolve_nofollow(target)?.as_fd().metadata()?; assert_eq!( meta.ino(), target_meta.ino(), "inode number of hard link doesn't match" ); } // Check symlink is correct. InodeType::Symlink(target) => { // Check using the a resolved handle. let actual_target = syscalls::readlinkat(&created, "")?; assert_eq!( target, actual_target, "readlinkat(handle) link target mismatch" ); // Double-check with Root::readlink. let actual_target = root.readlink(path)?; assert_eq!( target, actual_target, "root.readlink(path) link target mismatch" ); } } } } Ok(()) } pub(super) fn check_root_create_file( root: R, path: impl AsRef, oflags: OpenFlags, perm: &Permissions, expected_result: Result<&str, ErrorKind>, ) -> Result<(), Error> { let path = path.as_ref(); // Just clear the umask so all of the tests can use all of the // permission bits. let _ = rustix_process::umask(Mode::empty()); // Get a handle to the original path if it existed beforehand. 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 expected_result = expected_result.map(|path| root_dir.join(path)); match root.create_file(path, oflags, perm) { Err(err) => { tests_common::check_err(&Err::<(), _>(err), &expected_result) .with_context(|| format!("root create file {path:?}"))?; } Ok(file) => { let actual_path = file.as_fd().as_unsafe_path_unchecked()?; assert_eq!( Ok(actual_path.clone()), expected_result, "create file had unexpected path {actual_path:?}", ); let root = root_roundtrip(root)?; let new_lookup = root .resolve_nofollow(path) .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()?, "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() } else { libc::S_IFREG | perm.mode() }; let orig_meta = file.as_fd().metadata()?; assert_eq!( orig_meta.mode(), expect_mode, "create file had unexpected mode 0o{:o}", orig_meta.mode(), ); let new_meta = new_lookup.as_fd().metadata()?; assert_eq!( orig_meta.ino(), new_meta.ino(), "expected ino of {path:?} handles to be the same", ); // 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)?; } } Ok(()) } pub(super) fn check_root_open_subpath( root: R, path: impl AsRef, oflags: OpenFlags, expected_result: Result<&str, ErrorKind>, ) -> Result<(), Error> { 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 expected_result = expected_result.map(|path| root_dir.join(path)); match root.open_subpath(path, oflags) { Err(err) => { tests_common::check_err(&Err::<(), _>(err), &expected_result) .with_context(|| format!("root open subpath {path:?}"))?; } Ok(file) => { let actual_path = file.as_fd().as_unsafe_path_unchecked()?; assert_eq!( Ok(actual_path.clone()), expected_result, "create file had unexpected path {actual_path:?}", ); let root = root_roundtrip(root)?; let new_lookup = if oflags.contains(OpenFlags::O_NOFOLLOW) { root.resolve_nofollow(path) } else { root.resolve(path) } .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()?, "expected real path of {path:?} handles to be the same", ); tests_common::check_oflags(&file, oflags)?; } } Ok(()) } fn check_root_remove( root: R, path: &Path, remove_fn: F, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> where F: FnOnce(&R, &Path) -> Result<(), R::Error>, { // Get a handle before we remove the path, to make sure the actual inode // was unlinked. let handle = root.resolve_nofollow(path); // do not unwrap let res = remove_fn(&root, path); tests_common::check_err(&res, &expected_result) .with_context(|| format!("root remove {path:?}"))?; if res.is_ok() { // 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()?; assert_eq!(meta.nlink(), 0, "deleted file should have a 0 nlink"); } let root = root_roundtrip(root)?; let new_lookup = root.resolve_nofollow(path); assert_eq!( new_lookup.as_ref().map_err(R::Error::kind).err(), Some(ErrorKind::OsError(Some(libc::ENOENT))), "path should not exist after deletion, got {new_lookup:?}" ); } Ok(()) } pub(super) fn check_root_remove_dir( root: R, path: impl AsRef, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> { check_root_remove( root, path.as_ref(), |root, path| root.remove_dir(path), expected_result, ) } pub(super) fn check_root_remove_file( root: R, path: impl AsRef, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> { check_root_remove( root, path.as_ref(), |root, path| root.remove_file(path), expected_result, ) } pub(super) fn check_root_remove_all( root: R, path: impl AsRef, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> { check_root_remove( root, path.as_ref(), |root, path| root.remove_all(path), expected_result, ) } pub(super) fn check_root_rename( root: R, src_path: impl AsRef, dst_path: impl AsRef, rflags: RenameFlags, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> { let src_path = src_path.as_ref(); let dst_path = dst_path.as_ref(); // Strip any slashes since we have tests where the paths being operated // on are not directories but have trailing slashes. let (stripped_src_path, _) = utils::path_strip_trailing_slash(src_path); let (stripped_dst_path, _) = utils::path_strip_trailing_slash(dst_path); // Get a handle before we move the paths, to make sure the right inodes // were moved. However, we *do not unwrap these here* since the paths // might not exist (it is valid for dst to not exist, but for some // failure tests we want src to not exist too). let src_handle = root.resolve_nofollow(stripped_src_path); // do not unwrap this here! let dst_handle = root.resolve_nofollow(stripped_dst_path); // do not unwrap this here! // 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()?) } else { None }; let dst_real_path = if let Ok(ref handle) = dst_handle { Some(handle.as_fd().as_unsafe_path_unchecked()?) } else { None }; let res = root.rename(src_path, dst_path, rflags); tests_common::check_err(&res, &expected_result) .with_context(|| format!("root rename {src_path:?} -> {dst_path:?} {rflags:?}"))?; if res.is_ok() { // If the operation succeeded we can expect the source to have // existed. let src_handle = src_handle.expect("rename source should have existed before rename"); 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()?; assert_ne!( src_real_path, moved_src_real_path, "expected real path of handle to move after rename" ); match rflags.intersection(RenameFlags::RENAME_EXCHANGE | RenameFlags::RENAME_WHITEOUT) { RenameFlags::RENAME_EXCHANGE => { let dst_handle = dst_handle.expect("destination should have existed for RENAME_EXCHANGE"); let dst_real_path = dst_real_path.unwrap(); // Confirm that the moved path matches the original // destination. assert_eq!( dst_real_path, moved_src_real_path, "expected real path of handle to match destination with RENAME_EXCHANGE" ); // Confirm that the destination was also moved. let moved_dst_real_path = dst_handle.as_fd().as_unsafe_path_unchecked()?; assert_eq!( src_real_path, moved_dst_real_path, "expected real path of destination to move to source with RENAME_EXCHANGE" ); } RenameFlags::RENAME_WHITEOUT => { // Verify that there is a whiteout entry where the source // used to be. let new_lookup = root .resolve_nofollow(src_path) .wrap("expected source to exist with RENAME_WHITEOUT")?; let meta = new_lookup.as_fd().metadata()?; assert_eq!( syscalls::devmajorminor(meta.rdev()), (0, 0), "whiteout should have 0:0 rdev" ); assert_eq!( meta.mode() & libc::S_IFMT, libc::S_IFCHR, "whiteout should be char device, not 0o{:0}", meta.mode() ) } _ => {} } } else if let Ok(src_handle) = src_handle { 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()?; assert_eq!( src_real_path, nonmoved_src_real_path, "expected real path of handle to not change after failed rename" ); } Ok(()) } pub(super) fn check_root_mkdir_all( root: R, unsafe_path: impl AsRef, perm: Permissions, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> { let root = &root; let unsafe_path = unsafe_path.as_ref(); // 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 expected_subdir_state: Option<((_, _), _)> = match expected_result { Err(_) => None, Ok(_) => { let expected_uid = syscalls::geteuid(); let mut expected_gid = syscalls::getegid(); // Assume the umask is 0o022. Writing code to verify the umask // is a little annoying just for the purposes of a test, so // let's just stick with the default for now. let mut expected_mode = libc::S_IFDIR | (perm.mode() & !0o022); let handle: &Handle = before_partial_lookup.as_ref(); let dir_meta = handle.metadata()?; if dir_meta.mode() & libc::S_ISGID == libc::S_ISGID { expected_gid = dir_meta.gid(); expected_mode |= libc::S_ISGID; } Some(((expected_uid, expected_gid), expected_mode)) } }; let res = root .mkdir_all(unsafe_path, &perm) .with_wrap(|| format!("mkdir_all {unsafe_path:?}")); tests_common::check_err(&res, &expected_result)?; if let PartialLookup::Partial { handle, remaining, last_error: _, } = before_partial_lookup { let mut subpaths = remaining .raw_components() .filter(|part| !part.is_empty()) .fold(vec![PathBuf::from(".")], |mut subpaths, part| { subpaths.push( subpaths .iter() .last() .expect("must have at least one entry") .join(part), ); subpaths }) .into_iter(); // Skip the first "." component. let _ = subpaths.next(); // Verify that the remaining paths match the mode we expect (either // they don't exist or it matches the mode we requested). for subpath in subpaths { let got = syscalls::fstatat(&handle, &subpath) .map(|st| ((st.st_uid, st.st_gid), st.st_mode)) .ok(); match expected_subdir_state { // We expect there to be a directory with the exact mode. Some(want) => { assert_eq!( got, Some(want), "unexpected owner + file mode for newly-created directory {subpath:?} for mkdir_all({unsafe_path:?})" ); } // Make sure there isn't directory (even errors are fine!). None => { assert_ne!( got.map(|((_, _), mode)| mode & libc::S_IFMT), Some(libc::S_IFDIR), "unexpected directory {subpath:?} for mkdir_all({unsafe_path:?}) that failed" ); } } } } Ok(()) } pub(super) fn check_root_mkdir_all_racing( num_threads: usize, root: R, unsafe_path: impl AsRef, perm: Permissions, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> { let root = &root; let unsafe_path = unsafe_path.as_ref(); // Do lots of runs to try to catch any possible races. let num_retries = 100 + 1_000 / (1 + (num_threads >> 5)); for _ in 0..num_retries { thread::scope(|s| { let start_barrier = Arc::new(Barrier::new(num_threads)); for _ in 0..num_threads { let barrier = Arc::clone(&start_barrier); let perm = perm.clone(); s.spawn(move || { barrier.wait(); let res = root .mkdir_all(unsafe_path, &perm) .with_wrap(|| format!("mkdir_all {unsafe_path:?}")); tests_common::check_err(&res, &expected_result).expect("unexpected result"); }); } }); } Ok(()) } pub(super) fn check_root_remove_all_racing( num_threads: usize, root: R, unsafe_path: impl AsRef, expected_result: Result<(), ErrorKind>, ) -> Result<(), Error> { let root = &root; let unsafe_path = unsafe_path.as_ref(); // Do lots of runs to try to catch any possible races. let num_retries = 100 + 1_000 / (1 + (num_threads >> 5)); for _ in 0..num_retries { thread::scope(|s| { let start_barrier = Arc::new(Barrier::new(num_threads)); for _ in 0..num_threads { let barrier = Arc::clone(&start_barrier); s.spawn(move || { barrier.wait(); let res = root .remove_all(unsafe_path) .with_wrap(|| format!("remove_all {unsafe_path:?}")); tests_common::check_err(&res, &expected_result).expect("unexpected result"); }); } }); } Ok(()) } } pathrs-0.2.1/src/tests/traits/error.rs000064400000000000000000000030521046102023000160630ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::error::{Error, ErrorExt, ErrorKind}; pub(in crate::tests) trait ErrorImpl: std::error::Error + ErrorExt + Send + Sync + 'static { fn kind(&self) -> ErrorKind; } impl ErrorImpl for Error { fn kind(&self) -> ErrorKind { self.kind() } } pathrs-0.2.1/src/tests/traits/handle.rs000064400000000000000000000070571046102023000161760ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{error::Error, flags::OpenFlags, tests::traits::ErrorImpl, Handle, HandleRef}; use std::{ fs::File, os::unix::io::{AsFd, OwnedFd}, }; pub(in crate::tests) trait HandleImpl: AsFd + std::fmt::Debug + Sized { type Cloned: HandleImpl + Into; type Error: ErrorImpl; // NOTE: We return Self::Cloned so that we can share types with HandleRef. fn from_fd(fd: impl Into) -> Self::Cloned; fn try_clone(&self) -> Result; fn reopen(&self, flags: impl Into) -> Result; } impl HandleImpl for Handle { type Cloned = Handle; type Error = Error; fn from_fd(fd: impl Into) -> Self::Cloned { Self::Cloned::from_fd(fd) } fn try_clone(&self) -> Result { self.as_ref().try_clone().map_err(From::from) } fn reopen(&self, flags: impl Into) -> Result { self.as_ref().reopen(flags) } } impl HandleImpl for &Handle { type Cloned = Handle; type Error = Error; fn from_fd(fd: impl Into) -> Self::Cloned { Self::Cloned::from_fd(fd) } fn try_clone(&self) -> Result { Handle::try_clone(self).map_err(From::from) } fn reopen(&self, flags: impl Into) -> Result { Handle::reopen(self, flags) } } impl HandleImpl for HandleRef<'_> { type Cloned = Handle; type Error = Error; fn from_fd(fd: impl Into) -> Self::Cloned { Self::Cloned::from_fd(fd) } fn try_clone(&self) -> Result { self.try_clone().map_err(From::from) } fn reopen(&self, flags: impl Into) -> Result { self.reopen(flags) } } impl HandleImpl for &HandleRef<'_> { type Cloned = Handle; type Error = Error; fn from_fd(fd: impl Into) -> Self::Cloned { Self::Cloned::from_fd(fd) } fn try_clone(&self) -> Result { HandleRef::try_clone(self).map_err(From::from) } fn reopen(&self, flags: impl Into) -> Result { HandleRef::reopen(self, flags) } } pathrs-0.2.1/src/tests/traits/procfs.rs000064400000000000000000000052401046102023000162270ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::Error, flags::OpenFlags, procfs::{ProcfsBase, ProcfsHandleRef}, tests::traits::ErrorImpl, }; use std::{ fs::File, path::{Path, PathBuf}, }; pub(in crate::tests) trait ProcfsHandleImpl: std::fmt::Debug { type Error: ErrorImpl; fn open_follow( &self, base: ProcfsBase, subpath: impl AsRef, flags: impl Into, ) -> Result; fn open( &self, base: ProcfsBase, subpath: impl AsRef, flags: impl Into, ) -> Result; fn readlink(&self, base: ProcfsBase, subpath: impl AsRef) -> Result; } impl<'fd> ProcfsHandleImpl for ProcfsHandleRef<'fd> { type Error = Error; fn open_follow( &self, base: ProcfsBase, subpath: impl AsRef, flags: impl Into, ) -> Result { self.open_follow(base, subpath, flags) } fn open( &self, base: ProcfsBase, subpath: impl AsRef, flags: impl Into, ) -> Result { self.open(base, subpath, flags) } fn readlink( &self, base: ProcfsBase, subpath: impl AsRef, ) -> Result { self.readlink(base, subpath) } } pathrs-0.2.1/src/tests/traits/root.rs000064400000000000000000000304721046102023000157230ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::Error, flags::{OpenFlags, RenameFlags}, resolvers::Resolver, tests::traits::{ErrorImpl, HandleImpl}, Handle, InodeType, Root, RootRef, }; use std::{ fs::{File, Permissions}, os::unix::io::{AsFd, OwnedFd}, path::{Path, PathBuf}, }; pub(in crate::tests) trait RootImpl: AsFd + std::fmt::Debug + Sized { type Cloned: RootImpl + Into; type Handle: HandleImpl + Into; type Error: ErrorImpl; // NOTE:: Not part of the actual API, only used for tests! fn resolver(&self) -> Resolver; // NOTE: We return Self::Cloned so that we can share types with RootRef. fn from_fd(fd: impl Into, resolver: Resolver) -> Self::Cloned; fn try_clone(&self) -> Result; fn resolve(&self, path: impl AsRef) -> Result; fn resolve_nofollow(&self, path: impl AsRef) -> Result; fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result; fn readlink(&self, path: impl AsRef) -> Result; fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Self::Error>; fn create_file( &self, path: impl AsRef, flags: OpenFlags, perm: &Permissions, ) -> Result; fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result; fn remove_dir(&self, path: impl AsRef) -> Result<(), Self::Error>; fn remove_file(&self, path: impl AsRef) -> Result<(), Self::Error>; fn remove_all(&self, path: impl AsRef) -> Result<(), Self::Error>; fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), Self::Error>; } impl RootImpl for Root { type Cloned = Root; type Handle = Handle; type Error = Error; fn resolver(&self) -> Resolver { Resolver { backend: self.resolver_backend(), flags: self.resolver_flags(), } } fn from_fd(fd: impl Into, resolver: Resolver) -> Self::Cloned { Self::Cloned::from_fd(fd) .with_resolver_backend(resolver.backend) .with_resolver_flags(resolver.flags) } fn try_clone(&self) -> Result { self.try_clone().map_err(From::from) } fn resolve(&self, path: impl AsRef) -> Result { self.resolve(path) } fn resolve_nofollow(&self, path: impl AsRef) -> Result { self.resolve_nofollow(path) } fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { self.open_subpath(path, flags) } fn readlink(&self, path: impl AsRef) -> Result { self.readlink(path) } fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Self::Error> { self.create(path, inode_type) } fn create_file( &self, path: impl AsRef, flags: OpenFlags, perm: &Permissions, ) -> Result { self.create_file(path, flags, perm) } fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result { self.mkdir_all(path, perm) } fn remove_dir(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_dir(path) } fn remove_file(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_file(path) } fn remove_all(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_all(path) } fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), Self::Error> { self.rename(source, destination, rflags) } } impl RootImpl for &Root { type Cloned = Root; type Handle = Handle; type Error = Error; fn resolver(&self) -> Resolver { Resolver { backend: self.resolver_backend(), flags: self.resolver_flags(), } } fn from_fd(fd: impl Into, resolver: Resolver) -> Self::Cloned { Self::Cloned::from_fd(fd) .with_resolver_backend(resolver.backend) .with_resolver_flags(resolver.flags) } fn try_clone(&self) -> Result { Root::try_clone(self).map_err(From::from) } fn resolve(&self, path: impl AsRef) -> Result { Root::resolve(self, path) } fn resolve_nofollow(&self, path: impl AsRef) -> Result { Root::resolve_nofollow(self, path) } fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { Root::open_subpath(self, path, flags) } fn readlink(&self, path: impl AsRef) -> Result { Root::readlink(self, path) } fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Self::Error> { Root::create(self, path, inode_type) } fn create_file( &self, path: impl AsRef, flags: OpenFlags, perm: &Permissions, ) -> Result { Root::create_file(self, path, flags, perm) } fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result { Root::mkdir_all(self, path, perm) } fn remove_dir(&self, path: impl AsRef) -> Result<(), Self::Error> { Root::remove_dir(self, path) } fn remove_file(&self, path: impl AsRef) -> Result<(), Self::Error> { Root::remove_file(self, path) } fn remove_all(&self, path: impl AsRef) -> Result<(), Self::Error> { Root::remove_all(self, path) } fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), Self::Error> { Root::rename(self, source, destination, rflags) } } impl RootImpl for RootRef<'_> { type Cloned = Root; type Handle = Handle; type Error = Error; fn resolver(&self) -> Resolver { Resolver { backend: self.resolver_backend(), flags: self.resolver_flags(), } } fn from_fd(fd: impl Into, resolver: Resolver) -> Self::Cloned { Self::Cloned::from_fd(fd) .with_resolver_backend(resolver.backend) .with_resolver_flags(resolver.flags) } fn try_clone(&self) -> Result { self.try_clone().map_err(From::from) } fn resolve(&self, path: impl AsRef) -> Result { self.resolve(path) } fn resolve_nofollow(&self, path: impl AsRef) -> Result { self.resolve_nofollow(path) } fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { self.open_subpath(path, flags) } fn readlink(&self, path: impl AsRef) -> Result { self.readlink(path) } fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Self::Error> { self.create(path, inode_type) } fn create_file( &self, path: impl AsRef, flags: OpenFlags, perm: &Permissions, ) -> Result { self.create_file(path, flags, perm) } fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result { self.mkdir_all(path, perm) } fn remove_dir(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_dir(path) } fn remove_file(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_file(path) } fn remove_all(&self, path: impl AsRef) -> Result<(), Self::Error> { self.remove_all(path) } fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), Self::Error> { self.rename(source, destination, rflags) } } impl RootImpl for &RootRef<'_> { type Cloned = Root; type Handle = Handle; type Error = Error; fn resolver(&self) -> Resolver { Resolver { backend: self.resolver_backend(), flags: self.resolver_flags(), } } fn from_fd(fd: impl Into, resolver: Resolver) -> Self::Cloned { Self::Cloned::from_fd(fd) .with_resolver_backend(resolver.backend) .with_resolver_flags(resolver.flags) } fn try_clone(&self) -> Result { RootRef::try_clone(self).map_err(From::from) } fn resolve(&self, path: impl AsRef) -> Result { RootRef::resolve(self, path) } fn resolve_nofollow(&self, path: impl AsRef) -> Result { RootRef::resolve_nofollow(self, path) } fn open_subpath( &self, path: impl AsRef, flags: impl Into, ) -> Result { RootRef::open_subpath(self, path, flags) } fn readlink(&self, path: impl AsRef) -> Result { RootRef::readlink(self, path) } fn create(&self, path: impl AsRef, inode_type: &InodeType) -> Result<(), Self::Error> { RootRef::create(self, path, inode_type) } fn create_file( &self, path: impl AsRef, flags: OpenFlags, perm: &Permissions, ) -> Result { RootRef::create_file(self, path, flags, perm) } fn mkdir_all( &self, path: impl AsRef, perm: &Permissions, ) -> Result { RootRef::mkdir_all(self, path, perm) } fn remove_dir(&self, path: impl AsRef) -> Result<(), Self::Error> { RootRef::remove_dir(self, path) } fn remove_file(&self, path: impl AsRef) -> Result<(), Self::Error> { RootRef::remove_file(self, path) } fn remove_all(&self, path: impl AsRef) -> Result<(), Self::Error> { RootRef::remove_all(self, path) } fn rename( &self, source: impl AsRef, destination: impl AsRef, rflags: RenameFlags, ) -> Result<(), Self::Error> { RootRef::rename(self, source, destination, rflags) } } pathrs-0.2.1/src/tests.rs000064400000000000000000000044231046102023000134270ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ pub(crate) mod common { mod root; pub(crate) use root::*; mod mntns; pub(in crate::tests) use mntns::*; mod handle; pub(in crate::tests) use handle::*; mod error; pub(in crate::tests) use error::*; } #[cfg(feature = "capi")] #[allow(unsafe_code)] pub(in crate::tests) mod capi { mod utils; mod root; pub(in crate::tests) use root::*; mod handle; pub(in crate::tests) use handle::*; mod procfs; pub(in crate::tests) use procfs::*; } pub(in crate::tests) mod traits { // TODO: Unless we can figure out a way to get Deref working, we might want // to have these traits be included in the actual library... mod root; pub(in crate::tests) use root::*; mod handle; pub(in crate::tests) use handle::*; mod procfs; pub(in crate::tests) use procfs::*; mod error; pub(in crate::tests) use error::*; } mod test_procfs; mod test_resolve; mod test_resolve_partial; mod test_root_ops; mod test_race_resolve_partial; pathrs-0.2.1/src/utils/dir.rs000064400000000000000000000211561046102023000142050ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::{Error, ErrorExt, ErrorImpl}, flags::OpenFlags, syscalls, }; use std::{ ffi::OsStr, os::unix::{ffi::OsStrExt, io::AsFd}, path::Path, }; use rustix::fs::{AtFlags, Dir}; trait RmdirResultExt { // ENOENT from a removal function should be treated the same as an Ok(()). fn ignore_enoent(self) -> Self; } impl RmdirResultExt for Result<(), Error> { fn ignore_enoent(self) -> Self { match self.map_err(|err| (err.kind().errno(), err)) { Ok(()) | Err((Some(libc::ENOENT), _)) => Ok(()), Err((_, err)) => Err(err), } } } fn remove_inode(dirfd: impl AsFd, name: impl AsRef) -> Result<(), Error> { let dirfd = dirfd.as_fd(); let name = name.as_ref(); // To ensure we return a useful error, we try both unlink and rmdir and // try to avoid returning EISDIR/ENOTDIR if both failed. syscalls::unlinkat(dirfd, name, AtFlags::empty()) .or_else(|unlink_err| { syscalls::unlinkat(dirfd, name, AtFlags::REMOVEDIR).map_err(|rmdir_err| { if rmdir_err.root_cause().raw_os_error() == Some(libc::ENOTDIR) { unlink_err } else { rmdir_err } }) }) .map_err(|err| { ErrorImpl::RawOsError { operation: "remove inode".into(), source: err, } .into() }) } pub(crate) fn remove_all(dirfd: impl AsFd, name: impl AsRef) -> Result<(), Error> { let dirfd = dirfd.as_fd(); let name = name.as_ref(); if name.as_os_str().as_bytes().contains(&b'/') { Err(ErrorImpl::SafetyViolation { description: "remove_all reached a component containing '/'".into(), })?; } // Fast path -- try to remove it with unlink/rmdir. if remove_inode(dirfd, name).ignore_enoent().is_ok() { return Ok(()); } // Try to delete all children. We need to re-do the iteration until there // are no components left because deleting entries while iterating over a // directory can lead to the iterator skipping components. An attacker could // try to make this loop forever by consistently creating inodes, but // there's not much we can do about it and I suspect they would eventually // lose the race. let subdir = match syscalls::openat(dirfd, name, OpenFlags::O_DIRECTORY, 0).map_err(|err| { ErrorImpl::RawOsError { operation: "open directory to scan entries".into(), source: err, } }) { Ok(fd) => fd, Err(err) => match err.kind().errno() { // The path was deleted between us trying to with remove_inode() and // now -- just return as if we were the ones that deleted it. Some(libc::ENOENT) => return Ok(()), _ => Err(err)?, }, }; loop { // TODO: Dir creates a new file descriptor rather than reusing the one // we have, and RawDir can't be used as an Iterator yet (rustix // needs GAT to make that work). But this is okay for now... let mut iter = match Dir::read_from(&subdir) .map_err(|err| ErrorImpl::OsError { operation: "create directory iterator".into(), source: err.into(), }) .with_wrap(|| format!("scan directory {name:?} for deletion")) { Ok(iter) => iter, Err(err) => match err.kind().errno() { // If we got ENOENT that means the directory got deleted after // we opened it, so stop iterating (maybe another thread did "rm // -rf"). An attacker might've also replaced the directory but // we're not going retry opening it because that could lead to a // DoS. remove_inode will error out in that case, and that's // fine. Some(libc::ENOENT) => break, // TODO: Maybe we want to just break out of the loop here as // well, rather than return an error? If remove_inode() // again succeeds we're golden. _ => Err(err)?, }, } .filter(|res| { !matches!( res.as_ref().map(|dentry| dentry.file_name().to_bytes()), Ok(b".") | Ok(b"..") ) }) .peekable(); // We can stop iterating when a fresh directory iterator is empty. if iter.peek().is_none() { break; } // Recurse into all of the children and try to delete them. for child in iter { // TODO: We probably want to break out of the scan loop here if this // is an error as well. let child = child.map_err(|err| ErrorImpl::OsError { operation: format!("scan directory {name:?}").into(), source: err.into(), })?; let name: &Path = OsStr::from_bytes(child.file_name().to_bytes()).as_ref(); remove_all(&subdir, name).ignore_enoent()? } } // We have deleted all of the children of the directory, let's try to delete // the inode again (it should be empty now -- an attacker could add things // but we can just error out in that case, and if they swapped it to a file // then remove_inode will take care of that). remove_inode(dirfd, name) .ignore_enoent() .with_wrap(|| format!("deleting emptied directory {name:?}")) } #[cfg(test)] mod tests { use super::remove_all; use crate::{error::ErrorKind, tests::common as tests_common, Root}; use std::{os::unix::io::OwnedFd, path::Path}; use anyhow::Error; use pretty_assertions::assert_eq; #[test] fn remove_all_basic() -> Result<(), Error> { let dir = tests_common::create_basic_tree()?; let dirfd: OwnedFd = Root::open(&dir)?.into(); assert_eq!( remove_all(&dirfd, Path::new("a")).map_err(|err| err.kind()), Ok(()), "removeall(root, 'a') should work", ); assert_eq!( remove_all(&dirfd, Path::new("b")).map_err(|err| err.kind()), Ok(()), "removeall(root, 'b') should work", ); assert_eq!( remove_all(&dirfd, Path::new("c")).map_err(|err| err.kind()), Ok(()), "removeall(root, 'c') should work", ); let _dir = dir; // make sure the tempdir is not dropped early Ok(()) } #[test] fn remove_all_slash_path() -> Result<(), Error> { let dir = tests_common::create_basic_tree()?; let dirfd: OwnedFd = Root::open(&dir)?.into(); assert_eq!( remove_all(&dirfd, Path::new("/")).map_err(|err| err.kind()), Err(ErrorKind::SafetyViolation), "removeall(root, '/') should fail", ); assert_eq!( remove_all(&dirfd, Path::new("./a")).map_err(|err| err.kind()), Err(ErrorKind::SafetyViolation), "removeall(root, './a') should fail", ); assert_eq!( remove_all(&dirfd, Path::new("a/")).map_err(|err| err.kind()), Err(ErrorKind::SafetyViolation), "removeall(root, 'a/') should fail", ); let _dir = dir; // make sure the tempdir is not dropped early Ok(()) } } pathrs-0.2.1/src/utils/fd.rs000064400000000000000000000617311046102023000140230ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::{Error, ErrorExt, ErrorImpl, ErrorKind}, flags::OpenFlags, procfs::{self, ProcfsBase, ProcfsHandle}, syscalls, utils::{self, MaybeOwnedFd, RawProcfsRoot}, }; use std::{ fs::{self, File}, io::Error as IOError, os::unix::{ fs::MetadataExt, io::{AsFd, AsRawFd, OwnedFd, RawFd}, }, path::{Path, PathBuf}, str::FromStr, }; use rustix::fs::{self as rustix_fs, StatxFlags}; pub(crate) struct Metadata(rustix_fs::Stat); impl Metadata { pub(crate) fn is_symlink(&self) -> bool { self.mode() & libc::S_IFMT == libc::S_IFLNK } } #[allow(clippy::useless_conversion)] // 32-bit arches impl MetadataExt for Metadata { fn dev(&self) -> u64 { self.0.st_dev.into() } fn ino(&self) -> u64 { self.0.st_ino.into() } fn mode(&self) -> u32 { self.0.st_mode } fn nlink(&self) -> u64 { self.0.st_nlink.into() } fn uid(&self) -> u32 { self.0.st_uid } fn gid(&self) -> u32 { self.0.st_gid } fn rdev(&self) -> u64 { self.0.st_rdev.into() } fn size(&self) -> u64 { self.0.st_size as u64 } fn atime(&self) -> i64 { self.0.st_atime } fn atime_nsec(&self) -> i64 { self.0.st_atime_nsec as i64 } fn mtime(&self) -> i64 { self.0.st_mtime } fn mtime_nsec(&self) -> i64 { self.0.st_mtime_nsec as i64 } fn ctime(&self) -> i64 { self.0.st_ctime } fn ctime_nsec(&self) -> i64 { self.0.st_ctime_nsec as i64 } fn blksize(&self) -> u64 { self.0.st_blksize as u64 } fn blocks(&self) -> u64 { self.0.st_blocks as u64 } } pub(crate) trait FdExt: AsFd { /// Equivalent to [`File::metadata`]. /// /// [`File::metadata`]: std::fs::File::metadata fn metadata(&self) -> Result; /// Re-open a file descriptor. fn reopen(&self, procfs: &ProcfsHandle, flags: OpenFlags) -> Result; /// Get the path this RawFd is referencing. /// /// This is done through `readlink(/proc/self/fd)` and is naturally racy /// (hence the name "unsafe"), so it's important to only use this with the /// understanding that it only provides the guarantee that "at some point /// during execution this was the path the fd pointed to" and /// no more. /// /// NOTE: This method uses the [`ProcfsHandle`] to resolve the path. This /// means that it is UNSAFE to use this method within any of our `procfs` /// code! fn as_unsafe_path(&self, procfs: &ProcfsHandle) -> Result; /// Like [`FdExt::as_unsafe_path`], except that the lookup is done using the /// basic host `/proc` mount. This is not safe against various races, and /// thus MUST ONLY be used in codepaths that are not susceptible to those /// kinds of attacks. /// /// Currently this should only be used by the `syscall::FrozenFd` logic /// which saves the path a file descriptor references for error messages, as /// well as in some test code. fn as_unsafe_path_unchecked(&self) -> Result; /// Check if the File is on a "dangerous" filesystem that might contain /// magic-links. fn is_magiclink_filesystem(&self) -> Result; /// Get information about the file descriptor from `fdinfo`. /// /// This parses the given `field` (**case-sensitive**) from /// `/proc/thread-self/fdinfo/$fd` and returns a parsed version of the /// value. If the field was not present in `fdinfo`, we return `Ok(None)`. /// /// Note that this method is not safe against an attacker that can modify /// the mount table arbitrarily, though in practice it would be quite /// difficult for an attacker to be able to consistently overmount every /// `fdinfo` file for a process. This is mainly intended to be used within /// [`fetch_mnt_id`] as a final fallback in the procfs resolver (hence no /// [`ProcfsHandle`] argument) for pre-5.8 kernels. fn get_fdinfo_field( &self, proc_rootfd: RawProcfsRoot<'_>, want_field_name: &str, ) -> Result, Error> where T::Err: Into + Into; // TODO: Add get_fdinfo which uses ProcfsHandle, for when we add // RESOLVE_NO_XDEV support to Root::resolve. } /// Shorthand for reusing [`ProcfsBase::ProcThreadSelf`]'s compatibility checks /// to get a global-`/proc`-friendly subpath. Should only ever be used for /// `*_unchecked` functions -- [`ProcfsBase::ProcThreadSelf`] is the right thing /// to use in general. pub(in crate::utils) fn proc_threadself_subpath( proc_rootfd: RawProcfsRoot<'_>, subpath: &str, ) -> PathBuf { PathBuf::from(".") .join(ProcfsBase::ProcThreadSelf.into_path(proc_rootfd)) .join(subpath.trim_start_matches('/')) } /// Get the right subpath in `/proc/self` for the given file descriptor /// (including those with "special" values, like `AT_FDCWD`). fn proc_subpath(fd: Fd) -> Result { let fd = fd.as_raw_fd(); if fd == libc::AT_FDCWD { Ok("cwd".to_string()) } else if fd.is_positive() { Ok(format!("fd/{fd}")) } else { Err(ErrorImpl::InvalidArgument { name: "fd".into(), description: "must be positive or AT_FDCWD".into(), })? } } /// Set of filesystems' magic numbers that are considered "dangerous" (in that /// they can contain magic-links). This list should hopefully be exhaustive, but /// there's no real way of being sure since `nd_jump_link()` can be used by any /// non-mainline filesystem. /// /// This list is correct from the [introduction of `nd_jump_link()` in Linux /// 3.6][kcommit-b5fb63c18315] up to Linux 6.11. Before Linux 3.6, the logic /// that became `nd_jump_link()` only existed in procfs. AppArmor [started using /// it in Linux 4.13 with the introduction of /// apparmorfs][kcommit-a481f4d91783]. /// /// [kcommit-b5fb63c18315]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b5fb63c18315c5510c1d0636179c057e0c761c77 /// [kcommit-a481f4d91783]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=a481f4d917835cad86701fc0d1e620c74bb5cd5f // TODO: Remove the explicit size once generic_arg_infer is stable. // const DANGEROUS_FILESYSTEMS: [rustix_fs::FsWord; 2] = [ rustix_fs::PROC_SUPER_MAGIC, // procfs 0x5a3c_69f0, // apparmorfs ]; impl FdExt for Fd { fn metadata(&self) -> Result { let stat = syscalls::fstatat(self.as_fd(), "").map_err(|err| ErrorImpl::RawOsError { operation: "get fd metadata".into(), source: err, })?; Ok(Metadata(stat)) } fn reopen(&self, procfs: &ProcfsHandle, mut flags: OpenFlags) -> Result { let fd = self.as_fd(); // For file descriptors referencing a symlink (i.e. opened with // O_PATH|O_NOFOLLOW) there is no logic behind trying to do a "reopen" // operation, and you just get confusing results because the reopen // itself is done through a symlink. Even with O_EMPTYPATH you probably // wouldn't ever want to re-open it (all you can get is another // O_PATH|O_EMPTYPATH). if self.metadata()?.is_symlink() { Err(Error::from(ErrorImpl::OsError { operation: "reopen".into(), source: IOError::from_raw_os_error(libc::ELOOP), })) .wrap("symlink file handles cannot be reopened")? } // Now that we are sure the file descriptor is not a symlink, we can // clear O_NOFOLLOW since it is a no-op (but due to the procfs reopening // implementation, O_NOFOLLOW will cause strange behaviour). flags.remove(OpenFlags::O_NOFOLLOW); // TODO: Add support for O_EMPTYPATH once that exists... procfs .open_follow(ProcfsBase::ProcThreadSelf, proc_subpath(fd)?, flags) .map(OwnedFd::from) } fn as_unsafe_path(&self, procfs: &ProcfsHandle) -> Result { let fd = self.as_fd(); procfs.readlink(ProcfsBase::ProcThreadSelf, proc_subpath(fd)?) } fn as_unsafe_path_unchecked(&self) -> Result { // "/proc/thread-self/fd/$n" let fd_path = PathBuf::from("/proc").join(proc_threadself_subpath( RawProcfsRoot::UnsafeGlobal, &proc_subpath(self.as_fd())?, )); // Because this code is used within syscalls, we can't even check the // filesystem type of /proc (unless we were to copy the logic here). fs::read_link(&fd_path).map_err(|err| { ErrorImpl::OsError { operation: format!("readlink fd magic-link {fd_path:?}").into(), source: err, } .into() }) } fn is_magiclink_filesystem(&self) -> Result { // There isn't a marker on a filesystem level to indicate whether // nd_jump_link() is used internally. So, we just have to make an // educated guess based on which mainline filesystems expose // magic-links. let stat = syscalls::fstatfs(self).map_err(|err| ErrorImpl::RawOsError { operation: "check fstype of fd".into(), source: err, })?; Ok(DANGEROUS_FILESYSTEMS.contains(&stat.f_type)) } fn get_fdinfo_field( &self, proc_rootfd: RawProcfsRoot<'_>, want_field_name: &str, ) -> Result, Error> where T::Err: Into + Into, { let fd = self.as_fd(); let fdinfo_path = match fd.as_raw_fd() { // MSRV(1.66): Use ..=0 (half_open_range_patterns). // MSRV(1.80): Use ..0 (exclusive_range_pattern). fd @ libc::AT_FDCWD | fd @ RawFd::MIN..=0 => Err(ErrorImpl::OsError { operation: format!("get relative procfs fdinfo path for fd {fd}").into(), source: IOError::from_raw_os_error(libc::EBADF), })?, fd => proc_threadself_subpath(proc_rootfd, &format!("fdinfo/{fd}")), }; let mut fdinfo_file: File = proc_rootfd .open_beneath(fdinfo_path, OpenFlags::O_RDONLY) .with_wrap(|| format!("open fd {} fdinfo", fd.as_raw_fd()))? .into(); // As this is called from within fetch_mnt_id as a fallback, the only // thing we can do here is verify that it is actually procfs. However, // in practice it will be quite difficult for an attacker to over-mount // every fdinfo file for a process. procfs::verify_is_procfs(&fdinfo_file)?; // Get the requested field -- this will also verify that the fdinfo // contains an inode number that matches the original fd. utils::fd_get_verify_fdinfo(&mut fdinfo_file, fd, want_field_name) } } pub(crate) fn fetch_mnt_id( proc_rootfd: RawProcfsRoot<'_>, dirfd: impl AsFd, path: impl AsRef, ) -> Result { let dirfd = dirfd.as_fd(); let path = path.as_ref(); // The most ideal method of fetching mount IDs for a file descriptor (or // subpath) is statx(2) with STATX_MNT_ID_UNIQUE, as it provides a globally // unique 64-bit identifier for a mount that cannot be recycled without // having to interact with procfs (which is important since this code is // called within procfs, so we cannot use ProcfsHandle to protect against // attacks). // // Unfortunately, STATX_MNT_ID_UNIQUE was added in Linux 6.8, so we need to // have some fallbacks. STATX_MNT_ID is (for the most part) just as good for // our usecase (since we operate relative to a file descriptor, the mount ID // shouldn't be recycled while we keep the file descriptor open). This helps // a fair bit, but STATX_MNT_ID was still only added in Linux 5.8, and so // even some post-openat2(2) systems would be insecure if we just left it at // that. // // As a fallback, we can use the "mnt_id" field from /proc/self/fdinfo/ // to get the mount ID -- unlike statx(2), this functionality has existed on // Linux since time immemorial and thus we can error out if this operation // fails. This does require us to operate on procfs in a less-safe way // (unlike the alternative approaches), however note that: // // * For openat2(2) systems, this is completely safe (fdinfo files are regular // files, and thus -- unlike magic-links -- RESOLVE_NO_XDEV can be used to // safely protect against bind-mounts). // // * For non-openat2(2) systems, an attacker can theoretically attack this by // overmounting fdinfo with something like /proc/self/environ and fill it // with a fake fdinfo file. // // However, get_fdinfo_field and fd_get_verify_fdinfo have enough extra // protections that would probably make it infeasible for an attacker to // easily bypass it in practice. You can see the comments there for more // details, but in short an attacker would probably need to be able to // predict the file descriptor numbers for several transient files as // well as the inode number of the target file, and be able to create // overmounts while racing against libpathrs -- it seems unlikely that // this would be trivial to do (especially compared to how trivial // attacks are without these protections). // // NOTE: A very old trick for getting mount IDs in a race-free way was to // (ab)use name_to_handle_at(2) -- if you request a file handle with // too small a buffer, name_to_handle_at(2) will return -EOVERFLOW but // will still give you the mount ID. Sadly, name_to_handle_at(2) did // not work on procfs (or any other pseudofilesystem) until // AT_HANDLE_FID supported was added in Linux 6.7 (at which point // there's no real benefit to using it). // // Maybe we could use this for RESOLVE_NO_XDEV emulation in the // EmulatedOpath resolver, but for procfs this approach is not useful. // // NOTE: Obvious alternatives like parsing /proc/self/mountinfo can be // dismissed out-of-hand as not being useful (mountinfo is trivially // bypassable by an attacker with mount privileges, is generally awful // to parse, and doesn't work with open_tree(2)-style detached // mounts). const STATX_MNT_ID_UNIQUE: StatxFlags = StatxFlags::from_bits_retain(0x4000); let want_mask = StatxFlags::MNT_ID | STATX_MNT_ID_UNIQUE; let mnt_id = match syscalls::statx(dirfd, path, want_mask) { Ok(stx) => { let got_mask = StatxFlags::from_bits_retain(stx.stx_mask); if got_mask.intersects(want_mask) { Some(stx.stx_mnt_id) } else { None } } Err(err) => match err.root_cause().raw_os_error() { // We have to handle STATX_MNT_ID not being supported on pre-5.8 // kernels, so treat an ENOSYS or EINVAL the same so that we can // work on pre-4.11 (pre-statx) kernels as well. Some(libc::ENOSYS) | Some(libc::EINVAL) => None, _ => Err(ErrorImpl::RawOsError { operation: "check mnt_id of filesystem".into(), source: err, })?, }, } // Kind of silly intermediate Result<_, Error> type so that we can use // Result::or_else. // TODO: In principle we could remove this once result_flattening is // stabilised... .ok_or_else(|| { ErrorImpl::NotSupported { feature: "STATX_MNT_ID".into(), } .into() }) .or_else(|_: Error| -> Result<_, Error> { // openat doesn't support O_EMPTYPATH, so if we are operating on "" we // should reuse the dirfd directly. let file = if path.as_os_str().is_empty() { MaybeOwnedFd::BorrowedFd(dirfd) } else { MaybeOwnedFd::OwnedFd(syscalls::openat(dirfd, path, OpenFlags::O_PATH, 0).map_err( |err| ErrorImpl::RawOsError { operation: "open target file for mnt_id check".into(), source: err, }, )?) }; let file = file.as_fd(); match file .get_fdinfo_field(proc_rootfd, "mnt_id") .map_err(|err| (err.kind(), err)) { Ok(Some(mnt_id)) => Ok(mnt_id), // "mnt_id" *must* exist as a field -- make sure we return a // SafetyViolation here if it is missing or an invalid value // (InternalError), otherwise an attacker could silence this check // by creating a "mnt_id"-less fdinfo. // TODO: Should we actually match for ErrorImpl::ParseIntError here? Ok(None) | Err((ErrorKind::InternalError, _)) => Err(ErrorImpl::SafetyViolation { description: format!( r#"fd {:?} has a fake fdinfo: invalid or missing "mnt_id" field"#, file.as_raw_fd(), ) .into(), } .into()), // Pass through any other errors. Err((_, err)) => Err(err), } })?; Ok(mnt_id) } #[cfg(test)] mod tests { use crate::{ flags::OpenFlags, procfs::ProcfsHandle, syscalls, utils::{FdExt, RawProcfsRoot}, }; use std::{ fs::File, os::unix::{fs::MetadataExt, io::AsFd}, path::Path, }; use anyhow::{Context, Error}; use pretty_assertions::assert_eq; use tempfile::TempDir; fn check_as_unsafe_path(fd: impl AsFd, want_path: impl AsRef) -> Result<(), Error> { let want_path = want_path.as_ref(); // Plain /proc/... lookup. let got_path = fd.as_unsafe_path_unchecked()?; assert_eq!( got_path, want_path, "expected as_unsafe_path_unchecked to give the correct path" ); // ProcfsHandle-based lookup. let got_path = fd.as_unsafe_path(&ProcfsHandle::new()?)?; assert_eq!( got_path, want_path, "expected as_unsafe_path to give the correct path" ); Ok(()) } #[test] fn as_unsafe_path_cwd() -> Result<(), Error> { let real_cwd = syscalls::getcwd()?; check_as_unsafe_path(syscalls::AT_FDCWD, real_cwd) } #[test] fn as_unsafe_path_fd() -> Result<(), Error> { let real_tmpdir = TempDir::new()?; let file = File::open(&real_tmpdir)?; check_as_unsafe_path(&file, real_tmpdir) } #[test] fn as_unsafe_path_badfd() -> Result<(), Error> { assert!( syscalls::BADFD.as_unsafe_path_unchecked().is_err(), "as_unsafe_path_unchecked should fail for bad file descriptor" ); assert!( syscalls::BADFD .as_unsafe_path(&ProcfsHandle::new()?) .is_err(), "as_unsafe_path should fail for bad file descriptor" ); Ok(()) } #[test] fn reopen_badfd() -> Result<(), Error> { assert!( syscalls::BADFD .reopen(&ProcfsHandle::new()?, OpenFlags::O_PATH) .is_err(), "reopen should fail for bad file descriptor" ); Ok(()) } #[test] fn is_magiclink_filesystem() { assert!( !File::open("/") .expect("should be able to open handle to /") .is_magiclink_filesystem() .expect("is_magiclink_filesystem should work on regular file"), "/ is not a magic-link filesystem" ); } #[test] fn is_magiclink_filesystem_badfd() { assert!( syscalls::BADFD.is_magiclink_filesystem().is_err(), "is_magiclink_filesystem should fail for bad file descriptor" ); } #[test] fn metadata_badfd() { assert!( syscalls::BADFD.metadata().is_err(), "metadata should fail for bad file descriptor" ); } #[test] fn metadata() -> Result<(), Error> { let file = File::open("/").context("open dummy file")?; let file_meta = file.metadata().context("fstat file")?; let fd_meta = file.as_fd().metadata().context("fstat fd")?; assert_eq!(file_meta.dev(), fd_meta.dev(), "dev must match"); assert_eq!(file_meta.ino(), fd_meta.ino(), "ino must match"); assert_eq!(file_meta.mode(), fd_meta.mode(), "mode must match"); assert_eq!(file_meta.nlink(), fd_meta.nlink(), "nlink must match"); assert_eq!(file_meta.uid(), fd_meta.uid(), "uid must match"); assert_eq!(file_meta.gid(), fd_meta.gid(), "gid must match"); assert_eq!(file_meta.rdev(), fd_meta.rdev(), "rdev must match"); assert_eq!(file_meta.size(), fd_meta.size(), "size must match"); assert_eq!(file_meta.atime(), fd_meta.atime(), "atime must match"); assert_eq!( file_meta.atime_nsec(), fd_meta.atime_nsec(), "atime_nsec must match" ); assert_eq!(file_meta.mtime(), fd_meta.mtime(), "mtime must match"); assert_eq!( file_meta.mtime_nsec(), fd_meta.mtime_nsec(), "mtime_nsec must match" ); assert_eq!(file_meta.ctime(), fd_meta.ctime(), "ctime must match"); assert_eq!( file_meta.ctime_nsec(), fd_meta.ctime_nsec(), "ctime_nsec must match" ); assert_eq!(file_meta.blksize(), fd_meta.blksize(), "blksize must match"); assert_eq!(file_meta.blocks(), fd_meta.blocks(), "blocks must match"); Ok(()) } #[test] fn get_fdinfo_field() -> Result<(), Error> { let file = File::open("/").context("open dummy file")?; assert_eq!( file.get_fdinfo_field::(RawProcfsRoot::UnsafeGlobal, "pos")?, Some(0), "pos should be parsed and zero for new file" ); assert_eq!( file.get_fdinfo_field::(RawProcfsRoot::UnsafeGlobal, "flags")?, Some("02100000".to_string()), "flags should be parsed for new file" ); assert_ne!( file.get_fdinfo_field::(RawProcfsRoot::UnsafeGlobal, "mnt_id")? .expect("should find mnt_id in fdinfo"), 0, "mnt_id should be parsed and non-nil for any real file" ); assert_eq!( file.get_fdinfo_field::(RawProcfsRoot::UnsafeGlobal, "non_exist")?, None, "non_exist should not be present in fdinfo" ); Ok(()) } #[test] fn get_fdinfo_field_proc_rootfd() -> Result<(), Error> { let procfs = ProcfsHandle::new().context("open procfs handle")?; let file = File::open("/").context("open dummy file")?; assert_eq!( file.get_fdinfo_field::(procfs.as_raw_procfs(), "pos")?, Some(0), "pos should be parsed and zero for new file" ); assert_eq!( file.get_fdinfo_field::(procfs.as_raw_procfs(), "flags")?, Some("02100000".to_string()), "flags should be parsed for new file" ); assert_ne!( file.get_fdinfo_field::(procfs.as_raw_procfs(), "mnt_id")? .expect("should find mnt_id in fdinfo"), 0, "mnt_id should be parsed and non-nil for any real file" ); assert_eq!( file.get_fdinfo_field::(procfs.as_raw_procfs(), "non_exist")?, None, "non_exist should not be present in fdinfo" ); Ok(()) } } pathrs-0.2.1/src/utils/fdinfo.rs000064400000000000000000000527271046102023000147040ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::{Error, ErrorExt, ErrorImpl, ErrorKind}, utils::FdExt, }; use std::{ io::{BufRead, BufReader, Read, Seek, SeekFrom}, os::unix::{ fs::MetadataExt, io::{AsFd, AsRawFd}, }, str::FromStr, }; /// Parse a `/proc/self/fdinfo` file contents and return the first value that /// matches `want_field_name`. fn parse_and_find_fdinfo_field( rdr: &mut impl Read, want_field_name: &str, ) -> Result, Error> where T: FromStr, T::Err: Into, { let rdr = BufReader::new(rdr); // The fdinfo format is: // name:\tvalue1 // othername:\tvalue2 // foo_bar_baz:\tvalue3 let want_prefix = format!("{want_field_name}:"); for line in rdr.lines() { let line = line.map_err(|err| ErrorImpl::OsError { operation: "read line from fdinfo".into(), source: err, })?; // In practice, field names won't contain colons, but we can provide // more flexibility (and a simpler implementation) if we just treat the // value section (with a colon) as a simple prefix to strip. Also, the // separator is basically always tab, but stripping all whitespace is // probably better. if let Some(value) = line.strip_prefix(&want_prefix) { // return the first line that matches return value.trim().parse().map(Some).map_err(Into::into); } } // field not found Ok(None) } /// Parse a `/proc/self/fdinfo` file, and fetch the first value that matches /// `want_field_name`, with some extra verification. /// /// This function will verify that the `fdinfo` file contains an `ino` field /// that matches the actual inode number of the passed `fd`. This is intended to /// make it very difficult for an attacker to create a convincingly fake /// `fdinfo` file (as a final fallback for `RESOLVE_NO_XDEV` emulation). pub(crate) fn fd_get_verify_fdinfo( rdr: &mut (impl Read + Seek), fd: impl AsFd, want_field_name: &str, ) -> Result, Error> where T: FromStr, T::Err: Into, { let fd = fd.as_fd(); // Verify that the "ino" field in fdinfo matches the real inode number // of our file descriptor. This makes attacks harder (if not near // impossible, outside of very constrained situations): // // * An attacker would probably struggle to always accurately guess the inode // number of files that the process is trying to operate on. Yes, if they // know the victim process's access patterns of procfs they could probably // make an educated guess, but most files do not have stable inode numbers in // procfs. // // * An attacker can no longer bind-mount their own fdinfo directory with just // a buch of handles to "/proc" open (assuming the attacker is trying to // spoof "mnt_id"), because the inode numbers won't match. // // They also can't really fake inode numbers in real procfs fdinfo // files, so they would need to create fake fdinfo files using // individual file arbitrary-data gadgets (like /proc/self/environ). // However, every program only has one environment so they would need // to create a new child process for every fd they are trying to // attack simultaneously (and accurately update their environment // data to avoid detection). // // This isn't perfect protection by any means, but it's probably the // best we can do for very old kernels (given the constraints). At the very // least, it makes exploitation _much_ harder than if we didn't do anything // at all. let actual_ino: u64 = fd.metadata().wrap("get inode number of fd")?.ino(); let fdinfo_ino: u64 = match parse_and_find_fdinfo_field(rdr, "ino").map_err(|err| (err.kind(), err)) { Ok(Some(ino)) => Ok(ino), // "ino" *must* exist as a field -- make sure we return a // SafetyViolation here if it is missing or an invalid value // (InternalError), otherwise an attacker could silence this check // by creating a "ino"-less fdinfo. // TODO: Should we actually match for ErrorImpl::ParseIntError here? Ok(None) | Err((ErrorKind::InternalError, _)) => Err(ErrorImpl::SafetyViolation { description: format!( r#"fd {:?} has a fake fdinfo: invalid or missing "ino" field"#, fd.as_raw_fd(), ) .into(), } .into()), // Pass through any other errors. Err((_, err)) => Err(err), }?; if actual_ino != fdinfo_ino { Err(ErrorImpl::SafetyViolation { description: format!( "fd {:?} has a fake fdinfo: wrong inode number (ino is {fdinfo_ino:X} not {actual_ino:X})", fd.as_raw_fd() ) .into(), })?; } // Reset the position in the fdinfo file, and re-parse it to look for // the requested field. rdr.seek(SeekFrom::Start(0)) .map_err(|err| ErrorImpl::OsError { operation: format!("seek to start of fd {:?} fdinfo", fd.as_raw_fd()).into(), source: err, })?; parse_and_find_fdinfo_field(rdr, want_field_name) } #[cfg(test)] mod tests { use super::*; use crate::error::ErrorKind; use std::{ fmt::Debug, fs::File, io::Cursor, net::{AddrParseError, Ipv4Addr, SocketAddrV4}, }; use anyhow::{bail, Context, Error}; use indoc::{formatdoc, indoc}; use pretty_assertions::{assert_matches, Comparison}; impl From for ErrorImpl { fn from(err: AddrParseError) -> Self { unimplemented!("this test-only impl is only needed for type reasons -- {err:?}") } } fn check_parse_and_find_fdinfo_field( rdr: &mut impl Read, want_field_name: &str, expected: Result, ErrorKind>, ) -> Result<(), Error> where T: FromStr + PartialEq + Debug, T::Err: Into + Into, { let got = match parse_and_find_fdinfo_field(rdr, want_field_name) { Ok(res) => Ok(res), Err(err) => { if expected.is_ok() { // Don't panic yet -- this is just for debugging purposes. eprintln!("unexpected error: {err:?}"); } Err(err.kind()) } }; if got != expected { eprintln!("{}", Comparison::new(&got, &expected)); bail!( "unexpected result when parsing {want_field_name:?} field (as {:?}) from fdinfo (should be {expected:?})", std::any::type_name::() ); } Ok(()) } #[test] fn parse_and_find_fdinfo_field_basic() { const FAKE_FDINFO: &[u8] = indoc! {b" foo:\t123456 bar_baz:\t1 invalid line that should be skipped lorem ipsum: dolor sit amet : leading colon with no tab multiple: colons: in: one: line: repeated colons:: are not:: deduped repeated:\t1 repeated:\t2 repeated:\t3 last:\t \t127.0.0.1:8080\t\t "}; // Basic integer parsing. check_parse_and_find_fdinfo_field(&mut &FAKE_FDINFO[..], "foo", Ok(Some(123456u64))) .expect(r#"parse "foo: 123456" line"#); check_parse_and_find_fdinfo_field(&mut &FAKE_FDINFO[..], "bar_baz", Ok(Some(1u8))) .expect(r#"parse "bar_baz: 1" line"#); // String "parsing". check_parse_and_find_fdinfo_field( &mut &FAKE_FDINFO[..], "", Ok(Some("leading colon with no tab".to_string())), ) .expect(r#"parse ": leading colon with no tab" line"#); // Repeated lines. check_parse_and_find_fdinfo_field(&mut &FAKE_FDINFO[..], "repeated", Ok(Some(1i16))) .expect(r#"first matching entry should be returned"#); // Parse last line. check_parse_and_find_fdinfo_field( &mut &FAKE_FDINFO[..], "last", Ok(Some(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8080))), ) .expect(r#"first matching entry should be returned"#); // Non-existent fields should give us Ok(None). check_parse_and_find_fdinfo_field::(&mut &FAKE_FDINFO[..], "does_not_exist", Ok(None)) .expect(r#"non-existent field"#); // Make sure the entire field name (and only the field name) is being // matched. check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "lorem ipsum", Ok(Some("dolor sit amet".to_string())), ) .expect(r#"parse "lorem ipsum: dolor sit amet" line"#); check_parse_and_find_fdinfo_field::(&mut &FAKE_FDINFO[..], "lorem ipsu", Ok(None)) .expect(r#"parse "lorem ipsum: dolor sit amet" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "lorem ipsum:", Ok(None), ) .expect(r#"parse "lorem ipsum: dolor sit amet" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "lorem ipsum: dolor sit amet", Ok(None), ) .expect(r#"parse "lorem ipsum: dolor sit amet" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "lorem ipsum: dolor sit amet", Ok(None), ) .expect(r#"parse "lorem ipsum: dolor sit amet" line"#); // Lines with multiple colons get parsed properly. This won't happen in // practice, but it's worth checking. check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "multiple: colons: in: one: line:", Ok(None), ) .expect(r#"parse "multiple: colons: in: one: line:" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "multiple: colons: in: one: line", Ok(Some("".to_string())), ) .expect(r#"parse "multiple: colons: in: one: line:" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "multiple: colons: in: one", Ok(Some("line:".to_string())), ) .expect(r#"parse "multiple: colons: in: one: line:" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "multiple: colons: in", Ok(Some("one: line:".to_string())), ) .expect(r#"parse "multiple: colons: in: one: line:" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "multiple: colons", Ok(Some("in: one: line:".to_string())), ) .expect(r#"parse "multiple: colons: in: one: line:" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "multiple", Ok(Some("colons: in: one: line:".to_string())), ) .expect(r#"parse "multiple: colons: in: one: line:" line"#); // Repeated colons must not be deduplicated. check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "repeated colons:: are not:", Ok(Some("deduped".to_string())), ) .expect(r#"parse "repeated colons:: are not:: deduped" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "repeated colons:: are not", Ok(Some(": deduped".to_string())), ) .expect(r#"parse "repeated colons:: are not:: deduped" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "repeated colons:: are not::", Ok(None), ) .expect(r#"parse "repeated colons:: are not:: deduped" line"#); } #[test] fn parse_and_find_fdinfo_field_parse_error() { const FAKE_FDINFO: &[u8] = indoc! {b" nonint:\tnonint nonint_leading:\ta123 nonint_trailing:\t456a nonuint: -15 "}; check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "nonint", Err(ErrorKind::InternalError), ) .expect(r#"parse "nonint: nonint" line"#); assert_matches!( parse_and_find_fdinfo_field::(&mut &FAKE_FDINFO[..], "nonint") .expect_err("should not be able to parse fdinfo for 'nonint'") .into_inner(), ErrorImpl::ParseIntError(_), "non-integer 'nonint' field should fail with ParseIntError" ); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "nonint_leading", Err(ErrorKind::InternalError), ) .expect(r#"parse "nonint_leading: a123" line"#); assert_matches!( parse_and_find_fdinfo_field::(&mut &FAKE_FDINFO[..], "nonint_leading") .expect_err("should not be able to parse fdinfo for 'nonint_leading'") .into_inner(), ErrorImpl::ParseIntError(_), "non-integer 'nonint_leading' field should fail with ParseIntError" ); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "nonint_trailing", Err(ErrorKind::InternalError), ) .expect(r#"parse "nonint_trailing: 456a" line"#); assert_matches!( parse_and_find_fdinfo_field::(&mut &FAKE_FDINFO[..], "nonint_trailing") .expect_err("should not be able to parse fdinfo for 'nonint_trailing'") .into_inner(), ErrorImpl::ParseIntError(_), "non-integer 'nonint_trailing' field should fail with ParseIntError" ); check_parse_and_find_fdinfo_field::(&mut &FAKE_FDINFO[..], "nonuint", Ok(Some(-15))) .expect(r#"parse "nonuint: -15" line"#); check_parse_and_find_fdinfo_field::( &mut &FAKE_FDINFO[..], "nonuint", Err(ErrorKind::InternalError), ) .expect(r#"parse "nonuint: -15" line"#); assert_matches!( parse_and_find_fdinfo_field::(&mut &FAKE_FDINFO[..], "nonuint") .expect_err("should not be able to parse fdinfo for 'nonuint'") .into_inner(), ErrorImpl::ParseIntError(_), "signed integer 'nonuint' field parsing as unsigned should fail with ParseIntError" ); } fn check_fd_get_verify_fdinfo( rdr: &mut (impl Read + Seek), fd: impl AsFd, want_field_name: &str, expected: Result, ErrorKind>, ) -> Result<(), Error> where T: FromStr + PartialEq + Debug, T::Err: Into + Into, { let got = match fd_get_verify_fdinfo(rdr, fd, want_field_name) { Ok(res) => Ok(res), Err(err) => { if expected.is_ok() { // Don't panic yet -- this is just for debugging purposes. eprintln!("unexpected error: {err:?}"); } Err(err.kind()) } }; if got != expected { eprintln!("{}", Comparison::new(&got, &expected)); bail!( "unexpected result when parsing {want_field_name:?} field (as {:?}) from fdinfo (should be {expected:?})", std::any::type_name::() ); } Ok(()) } #[test] fn fd_get_verify_fdinfo_real_ino() -> Result<(), Error> { let file = File::open("/").context("open dummy file")?; let real_ino = file.metadata().context("get dummy file metadata")?.ino(); let fake_fdinfo = formatdoc! {" ino:\t{real_ino} mnt_id: 12345 "}; check_fd_get_verify_fdinfo( &mut Cursor::new(&fake_fdinfo), &file, "mnt_id", Ok(Some(12345)), ) .expect(r#"get "mnt_id" from fdinfo with correct ino"#); check_fd_get_verify_fdinfo( &mut Cursor::new(&fake_fdinfo), &file, "ino", Ok(Some(real_ino)), ) .expect(r#"get "ino" from fdinfo with correct ino"#); check_fd_get_verify_fdinfo::( &mut Cursor::new(&fake_fdinfo), &file, "non_exist", Ok(None), ) .expect(r#"get "non_exist" from fdinfo with correct ino"#); Ok(()) } #[test] fn fd_get_verify_fdinfo_bad_ino() -> Result<(), Error> { let file = File::open(".").context("open dummy file")?; let fake_ino = file.metadata().context("get dummy file metadata")?.ino() + 32; let fake_fdinfo = formatdoc! {" ino:\t{fake_ino} mnt_id: 12345 "}; check_fd_get_verify_fdinfo::( &mut Cursor::new(&fake_fdinfo), &file, "mnt_id", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "mnt_id" from fdinfo with incorrect ino"#); check_fd_get_verify_fdinfo::( &mut Cursor::new(&fake_fdinfo), &file, "ino", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "ino" from fdinfo with incorrect ino"#); check_fd_get_verify_fdinfo::( &mut Cursor::new(&fake_fdinfo), &file, "non_exist", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "non_exist" from fdinfo with incorrect ino"#); Ok(()) } // Make sure that a missing "ino" entry also fails. #[test] fn fd_get_verify_fdinfo_no_ino() -> Result<(), Error> { const FAKE_FDINFO: &[u8] = indoc! {b" foo: abcdef mnt_id: 12345 "}; let file = File::open(".").context("open dummy file")?; check_fd_get_verify_fdinfo::( &mut Cursor::new(&FAKE_FDINFO), &file, "mnt_id", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "mnt_id" from fdinfo with missing ino"#); check_fd_get_verify_fdinfo::( &mut Cursor::new(&FAKE_FDINFO), &file, "ino", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "ino" from fdinfo with missing ino"#); check_fd_get_verify_fdinfo::( &mut Cursor::new(&FAKE_FDINFO), &file, "non_exist", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "non_exist" from fdinfo with missing ino"#); Ok(()) } // Make sure that an "ino" entry with the wrong type results in a // SafetyViolation error, not an integer parsing error. #[test] fn fd_get_verify_fdinfo_wrongtype_ino() -> Result<(), Error> { const FAKE_FDINFO_I64: &[u8] = indoc! {b" ino: -1234 mnt_id: 12345 "}; const FAKE_FDINFO_STR: &[u8] = indoc! {b" ino: foobar mnt_id: 12345 "}; let file = File::open(".").context("open dummy file")?; for fake_fdinfo in [&FAKE_FDINFO_I64, &FAKE_FDINFO_STR] { check_fd_get_verify_fdinfo::( &mut Cursor::new(fake_fdinfo), &file, "mnt_id", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "mnt_id" from fdinfo with non-u64 ino"#); check_fd_get_verify_fdinfo::( &mut Cursor::new(fake_fdinfo), &file, "ino", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "ino" from fdinfo with non-u64 ino"#); check_fd_get_verify_fdinfo::( &mut Cursor::new(fake_fdinfo), &file, "non_exist", Err(ErrorKind::SafetyViolation), ) .expect(r#"get "non_exist" from fdinfo with non-u64 ino"#); } Ok(()) } } pathrs-0.2.1/src/utils/maybe_owned.rs000064400000000000000000000143371046102023000157230ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use std::os::unix::io::{AsFd, BorrowedFd, OwnedFd}; /// Like [`std::borrow::Cow`] but without the [`ToOwned`] requirement, and only /// for file descriptors. /// /// This is mainly useful when you need to write a function that takes something /// equivalent to `Option>` and opens an alternative [`OwnedFd`] /// (or any other `Fd: AsFd`) if passed [`None`]. Normally you cannot really do /// this smoothly. /// /// Note that due to Rust's temporaries handling and restrictions of the /// [`AsFd`] trait, you need to do something like the following: /// /// ```ignore /// fn procfs_foobar(fd: Option>) -> Result<(), Error> { /// let fd = match fd { /// None => MaybeOwnedFd::OwnedFd(File::open("/proc")?), /// Some(fd) => MaybeOwnedFd::BorrowedFd(fd), /// }; /// let fd = fd.as_fd(); // BorrowedFd<'_> /// // do something with fd /// } /// ``` /// /// This will give you a [`BorrowedFd`] with minimal fuss. /// /// [`OwnedFd`]: std::os::unix::io::OwnedFd /// [`ToOwned`]: std::borrow::ToOwned #[derive(Debug)] pub(crate) enum MaybeOwnedFd<'fd, Fd> where Fd: AsFd, { OwnedFd(Fd), BorrowedFd(BorrowedFd<'fd>), } impl<'fd> From for MaybeOwnedFd<'fd, OwnedFd> { fn from(fd: OwnedFd) -> Self { Self::OwnedFd(fd) } } impl<'fd, Fd> From> for MaybeOwnedFd<'fd, Fd> where Fd: AsFd, { fn from(fd: BorrowedFd<'fd>) -> Self { Self::BorrowedFd(fd) } } // I wish we could make this "impl AsFd for MaybeOwnedFd" but the lifetimes // don't match, even though it really feels like it should be possible. impl<'fd, Fd> MaybeOwnedFd<'fd, Fd> where Fd: AsFd, { /// Unwrap `MaybeOwnedFd` into the `OwnedFd` variant if possible. /// /// Returns `Err(self)` if this is a shared reference. pub(crate) fn try_into_owned(self) -> Result { match self { Self::OwnedFd(fd) => Ok(fd), Self::BorrowedFd(_) => Err(self), } } /// Unwrap `MaybeOwnedFd` into the `OwnedFd` variant if possible. pub(crate) fn into_owned(self) -> Option { self.try_into_owned().ok() } /// Very similar in concept to [`AsFd::as_fd`] but with some additional /// lifetime restrictions that make it incompatible with [`AsFd`]. pub(crate) fn as_fd<'a>(&'a self) -> BorrowedFd<'a> where 'a: 'fd, { match self { Self::OwnedFd(fd) => fd.as_fd(), Self::BorrowedFd(fd) => fd.as_fd(), } } } #[cfg(test)] mod tests { use super::*; use std::{ fs::File, os::unix::io::{AsFd, AsRawFd, OwnedFd}, }; use anyhow::Error; use pretty_assertions::{assert_eq, assert_matches}; #[test] fn as_fd() -> Result<(), Error> { let f: OwnedFd = File::open(".")?.into(); let fd = f.as_raw_fd(); let owned: MaybeOwnedFd = f.into(); assert_matches!(owned, MaybeOwnedFd::OwnedFd(_)); assert_eq!(owned.as_fd().as_raw_fd(), fd); assert_matches!(owned.into_owned(), Some(_)); let f = File::open(".")?; let borrowed: MaybeOwnedFd = f.as_fd().into(); assert_matches!(borrowed, MaybeOwnedFd::BorrowedFd(_)); assert_matches!(borrowed.into_owned(), None); Ok(()) } #[test] fn into_owned() -> Result<(), Error> { let f: OwnedFd = File::open(".")?.into(); let owned: MaybeOwnedFd = f.into(); assert_matches!( owned, MaybeOwnedFd::OwnedFd(_), "MaybeOwnedFd::from(OwnedFd)" ); assert_matches!( owned.into_owned(), Some(_), "MaybeOwnedFd::from(OwnedFd).into_owned()" ); let f = File::open(".")?; let borrowed: MaybeOwnedFd = f.as_fd().into(); assert_matches!( borrowed, MaybeOwnedFd::BorrowedFd(_), "MaybeOwnedFd::from(BorrowedFd)" ); assert_matches!( borrowed.into_owned(), None, "MaybeOwnedFd::from(BorrowedFd).into_owned()" ); Ok(()) } #[test] fn try_into_owned() -> Result<(), Error> { let f: OwnedFd = File::open(".")?.into(); let owned: MaybeOwnedFd = f.into(); assert_matches!( owned, MaybeOwnedFd::OwnedFd(_), "MaybeOwnedFd::from(OwnedFd)" ); assert_matches!( owned.try_into_owned(), Ok(_), "MaybeOwnedFd::from(OwnedFd).try_into_owned()" ); let f = File::open(".")?; let borrowed: MaybeOwnedFd = f.as_fd().into(); assert_matches!( borrowed, MaybeOwnedFd::BorrowedFd(_), "MaybeOwnedFd::from(BorrowedFd)" ); assert_matches!( borrowed.try_into_owned(), Err(_), "MaybeOwnedFd::from(BorrowedFd).try_into_owned()" ); Ok(()) } } pathrs-0.2.1/src/utils/path.rs000064400000000000000000000435641046102023000143720ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::error::{Error, ErrorImpl}; use std::{ collections::VecDeque, ffi::{CString, OsStr, OsString}, os::unix::ffi::OsStrExt, path::Path, }; pub(crate) trait ToCString { /// Convert to a CStr. fn to_c_string(&self) -> CString; } impl ToCString for OsStr { fn to_c_string(&self) -> CString { let filtered: Vec<_> = self .as_bytes() .iter() .copied() .take_while(|&c| c != b'\0') .collect(); CString::new(filtered).expect("nul bytes should've been excluded") } } impl ToCString for Path { fn to_c_string(&self) -> CString { self.as_os_str().to_c_string() } } /// Helper to strip trailing / components from a path. pub(crate) fn path_strip_trailing_slash(path: &Path) -> (&Path, bool) { let path_bytes = path.as_os_str().as_bytes(); let idx = match path_bytes.iter().rposition(|c| *c != b'/') { Some(idx) => idx, None => { if path_bytes.len() > 1 { // Nothing but b'/' components -- return a single /. return (Path::new("/"), true); } else { // Either "/" or "". return (path, false); } } }; if idx == path_bytes.len() - 1 { // No slashes to strip. (path, false) } else { // Strip trailing slashes. (Path::new(OsStr::from_bytes(&path_bytes[..=idx])), true) } } /// Helper to split a Path into its parent directory and trailing path. The /// trailing component is guaranteed to not contain a directory separator. pub(crate) fn path_split(path: &'_ Path) -> Result<(&'_ Path, Option<&'_ Path>), Error> { let (dir, base) = path .partial_ancestors() .next() .expect("partial_ancestors iterator must return at least one entry"); // It's critical we are only touching the final component in the path. // If there are any other path components we must bail. if let Some(base) = base { let base_bytes = base.as_os_str().as_bytes(); if base_bytes == b"" { Err(ErrorImpl::SafetyViolation { description: "trailing component of split pathname is empty".into(), })? } if base_bytes.contains(&b'/') { Err(ErrorImpl::SafetyViolation { description: "trailing component of split pathname contains '/'".into(), })? } } Ok((dir, base)) } /// RawComponents is like [`Components`] except that no normalisation is done /// for any path components ([`Components`] normalises "/./" components), and /// all of the components are simply [`OsStr`]. /// /// [`Components`]: std::path::Components #[derive(Debug)] pub(crate) struct RawComponents<'a> { inner: Option<&'a OsStr>, } impl<'a> Iterator for RawComponents<'a> { type Item = &'a OsStr; fn next(&mut self) -> Option { match self.inner { None => None, Some(inner) => { let (next, remaining) = match memchr::memchr(b'/', inner.as_bytes()) { None => (inner, None), Some(idx) => { let (head, mut tail) = inner.as_bytes().split_at(idx); tail = &tail[1..]; // strip slash (OsStrExt::from_bytes(head), Some(OsStrExt::from_bytes(tail))) } }; self.inner = remaining; assert!( !next.as_bytes().contains(&b'/'), "individual path component {next:?} contains '/'", ); Some(next) } } } } impl DoubleEndedIterator for RawComponents<'_> { fn next_back(&mut self) -> Option { match self.inner { None => None, Some(inner) => { let (next, remaining) = match memchr::memrchr(b'/', inner.as_bytes()) { None => (inner, None), Some(idx) => { let (head, mut tail) = inner.as_bytes().split_at(idx); tail = &tail[1..]; // strip slash (OsStrExt::from_bytes(tail), Some(OsStrExt::from_bytes(head))) } }; self.inner = remaining; assert!( !next.as_bytes().contains(&b'/'), "individual path component {next:?} contains '/'", ); Some(next) } } } } impl RawComponents<'_> { pub(crate) fn prepend(&mut self, deque: &mut VecDeque) { self.map(|p| p.to_os_string()) // VecDeque doesn't have an amortized way of prepending a // Vec, so we need to do this manually. We need to rev() the // iterator since we're pushing to the front each time. .rev() .for_each(|p| deque.push_front(p)); } } #[derive(Debug)] enum AncestorsIterState { Start, Middle(usize), End, } #[derive(Debug)] pub(crate) struct Ancestors<'p> { state: AncestorsIterState, inner: &'p Path, } impl<'p> Iterator for Ancestors<'p> { // (ancestor, remaining_path) type Item = (&'p Path, Option<&'p Path>); fn next(&mut self) -> Option { let inner_bytes = self.inner.as_os_str().as_bytes(); // Search for "/" in the remaining path. let found_idx = match self.state { AncestorsIterState::End => return None, AncestorsIterState::Start => memchr::memrchr(b'/', inner_bytes), AncestorsIterState::Middle(idx) => memchr::memrchr(b'/', &inner_bytes[..idx]), }; let next_idx = match found_idx { None => { self.state = AncestorsIterState::End; return Some(( Path::new("."), if inner_bytes.is_empty() { None } else { Some(self.inner) }, )); } Some(idx) => idx, }; // TODO: Skip over multiple "//" components. // Split the path. // TODO: We probably want to move some of the None handling here to // split_path()... let (ancestor_bytes, remaining_bytes) = match inner_bytes.split_at(next_idx) { (b"", b"/") => (&b"/"[..], None), (dir, b"/") => (dir, None), (b"", base) => (&b"/"[..], Some(&base[1..])), (dir, base) => (dir, Some(&base[1..])), }; // Update the state. self.state = match ancestor_bytes { b"" | b"." | b"/" => AncestorsIterState::End, _ => AncestorsIterState::Middle(next_idx), }; // Not quite sure why we need to annotate :: since // OsStrExt::from_bytes() returns a plain OsStr. Oh well. Some(( Path::new::(OsStrExt::from_bytes(ancestor_bytes)), remaining_bytes .map(OsStrExt::from_bytes) .map(Path::new::), )) } } pub(crate) trait PathIterExt { fn raw_components(&self) -> RawComponents<'_>; fn partial_ancestors(&self) -> Ancestors<'_>; } impl PathIterExt for Path { fn raw_components(&self) -> RawComponents<'_> { RawComponents { inner: Some(self.as_os_str()), } } fn partial_ancestors(&self) -> Ancestors<'_> { Ancestors { state: AncestorsIterState::Start, inner: self, } } } impl> PathIterExt for P { fn raw_components(&self) -> RawComponents<'_> { self.as_ref().raw_components() } fn partial_ancestors(&self) -> Ancestors<'_> { self.as_ref().partial_ancestors() } } #[cfg(test)] mod tests { use super::*; use std::path::{Path, PathBuf}; use anyhow::{Context, Error}; use pretty_assertions::assert_eq; // TODO: Add propcheck tests? macro_rules! path_strip_slash_tests { // path_strip_slash_tests! { // abc("a/b" => "a/b"); // xyz("/foo/bar///" => "/foo/bar"); // xyz("//" => "/"); // } ($($test_name:ident ($path:expr => $stripped:expr, $trailing:expr));* $(;)? ) => { paste::paste! { $( #[test] fn []() { let path: PathBuf = $path.into(); let (got_path, got_trailing) = path_strip_trailing_slash(&path); let want_path: PathBuf = $stripped.into(); let want_trailing = $trailing; assert_eq!( got_path.as_os_str(), want_path.as_os_str(), "stripping {path:?} produced wrong result -- got {got_path:?}", ); assert_eq!( got_trailing, want_trailing, "expected {path:?} to have trailing_slash={want_trailing}", ); } )* } }; } path_strip_slash_tests! { empty("" => "", false); dot("." => ".", false); root("/" => "/", false); regular_notrailing1("/foo/bar/baz" => "/foo/bar/baz", false); regular_notrailing2("../../a/b/c" => "../../a/b/c", false); regular_notrailing3("/a" => "/a", false); regular_trailing1("/foo/bar/baz/" => "/foo/bar/baz", true); regular_trailing2("../../a/b/c/" => "../../a/b/c", true); regular_trailing3("/a/" => "/a", true); trailing_dot1("/foo/." => "/foo/.", false); trailing_dot2("foo/../bar/../." => "foo/../bar/../.", false); root_multi1("////////" => "/", true); root_multi2("//" => "/", true); complex1("foo//././bar/baz//./" => "foo//././bar/baz//.", true); complex2("//a/.///b/../../" => "//a/.///b/../..", true); complex3("../foo/bar/.///" => "../foo/bar/.", true); } macro_rules! path_split_tests { // path_tests! { // abc("a/b" => "a", Some("b")); // xyz("/foo/bar/baz" => "/foo/bar", Some("baz")); // xyz("/" => "/", None); // } ($($test_name:ident ($path:expr => $dir:expr, $file:expr));* $(;)? ) => { paste::paste! { $( #[test] fn []() -> Result<(), Error> { let path: PathBuf = $path.into(); let (got_dir, got_file) = path_split(&path) .with_context(|| format!("path_split({path:?})"))?; let want_dir: PathBuf = $dir.into(); let want_file = { let file: Option<&str> = $file; file.map(PathBuf::from) }; assert_eq!( (got_dir.as_os_str(), got_file.map(Path::as_os_str)), (want_dir.as_os_str(), want_file.as_ref().map(|p| p.as_os_str())) ); Ok(()) } )* } }; } path_split_tests! { empty("" => ".", None); root("/" => "/", None); single1("single" => ".", Some("single")); single2("./single" => ".", Some("single")); single_root1("/single" => "/", Some("single")); multi1("foo/bar" => "foo", Some("bar")); multi2("foo/bar/baz" => "foo/bar", Some("baz")); multi3("./foo/bar/baz" => "./foo/bar", Some("baz")); multi_root1("/foo/bar" => "/foo", Some("bar")); multi_root2("/foo/bar/baz" => "/foo/bar", Some("baz")); trailing_dot1("/foo/." => "/foo", Some(".")); trailing_dot2("foo/../bar/../." => "foo/../bar/..", Some(".")); trailing_slash1("/foo/" => "/foo", None); trailing_slash2("foo/bar///" => "foo/bar//", None); trailing_slash3("./" => ".", None); trailing_slash4("//" => "/", None); complex1("foo//././bar/baz//./xyz" => "foo//././bar/baz//.", Some("xyz")); complex2("//a/.///b/../../xyz" => "//a/.///b/../..", Some("xyz")); complex3("../foo/bar/.///baz" => "../foo/bar/.//", Some("baz")); } macro_rules! path_ancestor_tests { ($($test_name:ident ($path:expr => { $(($ancestor:expr, $remaining:expr)),* }));* $(;)? ) => { paste::paste! { $( #[test] fn []() { let path = PathBuf::from($path); let expected: Vec<(&Path, Option<&Path>)> = vec![ $({ let ancestor: &str = $ancestor; let remaining: Option<&str> = $remaining; (Path::new(ancestor), remaining.map(Path::new)) }),* ]; let got = path.partial_ancestors().collect::>(); assert_eq!(got, expected, "unexpected results from partial_ancestors"); } )* } } } path_ancestor_tests! { empty("" => { (".", None) }); root("/" => { ("/", None) }); single1("single" => { (".", Some("single")) }); single2("./single" => { (".", Some("single")) }); single_root1("/single" => { ("/", Some("single")) }); multi1("foo/bar" => { ("foo", Some("bar")), (".", Some("foo/bar")) }); multi2("foo/bar/baz" => { ("foo/bar", Some("baz")), ("foo", Some("bar/baz")), (".", Some("foo/bar/baz")) }); multi3("./foo/bar/baz" => { ("./foo/bar", Some("baz")), ("./foo", Some("bar/baz")), (".", Some("foo/bar/baz")) }); multi_root1("/foo/bar" => { ("/foo", Some("bar")), ("/", Some("foo/bar")) }); multi_root2("/foo/bar/baz" => { ("/foo/bar", Some("baz")), ("/foo", Some("bar/baz")), ("/", Some("foo/bar/baz")) }); trailing_dot1("/foo/." => { ("/foo/", Some(".")), ("/", Some("foo/.")) }); trailing_dot2("foo/../bar/../." => { ("foo/../bar/..", Some(".")), ("foo/../bar", Some("../.")), ("foo/..", Some("bar/../.")), ("foo", Some("../bar/../.")), (".", Some("foo/../bar/../.")) }); trailing_slash1("/foo/" => { ("/foo", None), ("/", Some("foo")) }); // TODO: This should probably be fixed so we skip over "//" components. trailing_slash2("foo/bar///" => { ("foo/bar//", None), ("foo/bar/", Some("/")), ("foo/bar", Some("//")), ("foo", Some("bar///")), (".", Some("foo/bar///")) }); trailing_slash3("./" => { (".", None) }); trailing_slash4("//" => { ("/", None) }); // TODO: This should probably be fixed so we skip over "//" components. complex1("foo//././bar/baz//./xyz" => { ("foo//././bar/baz//.", Some("xyz")), ("foo//././bar/baz/", Some("./xyz")), ("foo//././bar/baz", Some("/./xyz")), ("foo//././bar", Some("baz//./xyz")), ("foo//./.", Some("bar/baz//./xyz")), ("foo//.", Some("./bar/baz//./xyz")), ("foo/", Some("././bar/baz//./xyz")), ("foo", Some("/././bar/baz//./xyz")), (".", Some("foo//././bar/baz//./xyz")) }); complex2("//a/.///b/../../xyz" => { ("//a/.///b/../..", Some("xyz")), ("//a/.///b/..", Some("../xyz")), ("//a/.///b", Some("../../xyz")), ("//a/.//", Some("b/../../xyz")), ("//a/./", Some("/b/../../xyz")), ("//a/.", Some("//b/../../xyz")), ("//a", Some(".///b/../../xyz")), //("//", Some("a/.///b/../../xyz")), ("/", Some("a/.///b/../../xyz")) }); complex3("../foo/bar/.///baz" => { ("../foo/bar/.//", Some("baz")), ("../foo/bar/./", Some("/baz")), ("../foo/bar/.", Some("//baz")), ("../foo/bar", Some(".///baz")), ("../foo", Some("bar/.///baz")), ("..", Some("foo/bar/.///baz")), (".", Some("../foo/bar/.///baz")) }); } } pathrs-0.2.1/src/utils/raw_procfs.rs000064400000000000000000000367141046102023000156020ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::{Error, ErrorImpl}, flags::OpenFlags, procfs, syscalls::{self, OpenHow, ResolveFlags}, utils::{fd::proc_threadself_subpath, FdExt, MaybeOwnedFd}, }; use std::{ os::unix::{ fs::MetadataExt, io::{AsRawFd, BorrowedFd, OwnedFd}, }, path::Path, }; use rustix::fs::{Access, AtFlags}; /// "We have [`ProcfsHandle`] at home." /// /// One of the core issues when implementing [`ProcfsHandle`] is that a lot of /// helper functions used by [`ProcfsHandle`] would really like to be able to /// use [`ProcfsHandle`] in a re-entrant way to get some extra safety against /// attacks, while also supporting callers that don't even have a `/proc` handle /// ready or callers that are not within the [`ProcfsHandle`] implementation and /// thus can get full safety from [`ProcfsHandle`] itself. /// /// The main purpose of this type is to allow us to easily indicate this (as /// opposed to `Option>` everywhere) and provide some helpers to /// help reduce the complexity of the helper functions. /// /// In general, you should only be using this for core helper functions used by /// [`ProcfsHandle`] which need to take a reference to the *root* of `/proc`. /// Make sure to always use the same [`RawProcfsRoot`] consistently, lest you /// end up with very weird coherency problems. /// /// [`ProcfsHandle`]: crate::procfs::ProcfsHandle #[derive(Copy, Clone, Debug)] pub(crate) enum RawProcfsRoot<'fd> { /// Use the global `/proc`. This is unsafe against most attacks, and should /// only ever really be used for debugging purposes only, such as in /// [`FdExt::as_unsafe_path_unchecked`]. /// /// [`FdExt::as_unsafe_path_unchecked`]: crate::utils::FdExt::as_unsafe_path_unchecked UnsafeGlobal, /// Use this [`BorrowedFd`] as the rootfs of a proc and operate relative to /// it. This is still somewhat unsafe, depending on what kernel features are /// available. UnsafeFd(BorrowedFd<'fd>), } impl<'fd> RawProcfsRoot<'fd> { /// Convert this to a [`MaybeOwnedFd`]. /// /// For [`RawProcfsRoot::UnsafeGlobal`], this requires opening `/proc` and /// thus allocating a new file handle. For all other variants this should be /// a very cheap reference conversion. pub(crate) fn try_into_maybe_owned_fd<'a>(&'a self) -> Result, Error> where 'a: 'fd, { let fd = match self { Self::UnsafeGlobal => MaybeOwnedFd::OwnedFd( syscalls::openat( syscalls::BADFD, "/proc", OpenFlags::O_PATH | OpenFlags::O_DIRECTORY, 0, ) .map_err(|err| ErrorImpl::RawOsError { operation: "open /proc handle".into(), source: err, })?, ), Self::UnsafeFd(fd) => MaybeOwnedFd::BorrowedFd(*fd), }; procfs::verify_is_procfs_root(fd.as_fd())?; Ok(fd) } /// `accessat(procfs_root, path, Access:EXISTS, AtFlags::SYMLINK_NOFOLLOW)` /// /// Should only be used as an indicative check (namely to see if /// `/proc/thread-self` exists), and this has no protections against /// malicious components regardless of what kind of handle it is. /// /// The only user of this is [`ProcfsBase::into_path`] which only uses it to /// decide whether `/proc/thread-self` exists. /// /// [`ProcfsBase::into_path`]: crate::procfs::ProcfsBase::into_path pub(crate) fn exists_unchecked(&self, path: impl AsRef) -> Result<(), Error> { syscalls::accessat( self.try_into_maybe_owned_fd()?.as_fd(), path, Access::EXISTS, AtFlags::SYMLINK_NOFOLLOW, ) .map_err(|err| { ErrorImpl::RawOsError { operation: "check if subpath exists in raw procfs".into(), source: err, } .into() }) } /// Open a subpath within this [`RawProcfsRoot`] using `openat2`. /// /// `RESOLVE_NO_MAGICLINKS | RESOLVE_NO_XDEV | RESOLVE_BENEATH` are all /// auto-applied. fn openat2_beneath(&self, path: impl AsRef, oflags: OpenFlags) -> Result { let path = path.as_ref(); syscalls::openat2( self.try_into_maybe_owned_fd()?.as_fd(), path, OpenHow { flags: oflags.bits() as _, mode: 0, resolve: (ResolveFlags::RESOLVE_NO_MAGICLINKS | ResolveFlags::RESOLVE_NO_XDEV | ResolveFlags::RESOLVE_BENEATH) .bits(), }, ) .map_err(|err| { ErrorImpl::RawOsError { operation: "open raw procfs subpath".into(), source: err, } .into() }) } /// Open a subpath within this [`RawProcfsRoot`] using `openat`. /// /// A best-effort attempt is made to try to avoid getting tricked by /// overmounts, but this method does not guarantee protection against /// bind-mount overmounts. fn opath_beneath_unchecked( &self, path: impl AsRef, oflags: OpenFlags, ) -> Result { let path = path.as_ref(); let proc_rootfd = self.try_into_maybe_owned_fd()?; let proc_rootfd = proc_rootfd.as_fd(); // We split the open into O_PATH+reopen below, which means if O_NOFOLLOW // is requested (which it always is), we need to apply it to the initial // O_PATH and not the re-open. If the target is not a symlink, // everything works fine -- if the target was a symlink the re-open will // fail with -ELOOP (the same as a one-shot open). let (opath_oflags, oflags) = ( oflags & OpenFlags::O_NOFOLLOW, oflags & !OpenFlags::O_NOFOLLOW, ); // This is technically not safe, but there really is not much we can do // in practice -- we would need to have a separate copy of the procfs // resolver code without any mount-id-related protections (or add an // unsafe_disable_mnt_id_checks argument), and even then it would not // practically protect against attacks. // // An attacker could still bind-mount their own /proc/thread-self/fdinfo // (after opening hundreds of handles to /proc) on top of our // /proc/thread-self/fdinfo, at which point they could trivially fake // fdinfo without the need for symlinks or tmpfs. // // At this point, we are just trying to minimise the damage a trivial // attack on top of a static procfs path can do. An attacker that can // actively bind-mount on top of /proc/thread-self/fdinfo cannot be // protected against without openat2(2) or STATX_MNT_ID. // In order to avoid being tricked into following a trivial symlink or // bind-mount to a filesystem object that could DoS us when we try to // O_RDONLY open it below (such as a stale NFS handle), first open it // with O_PATH then double-check that it is a procfs inode. let opath = syscalls::openat(proc_rootfd, path, OpenFlags::O_PATH | opath_oflags, 0) .map_err(|err| ErrorImpl::RawOsError { operation: "preliminary open raw procfs subpath to check fstype".into(), source: err, })?; // As below, we can't use verify_same_procfs_mnt. procfs::verify_is_procfs(&opath)?; // We can't do FdExt::reopen() here because this code is potentially // called within ProcfsHandle. However, we can just re-open through // /proc/thread-self/fd/$n directly -- this is not entirely safe // against bind-mounts but this will make it harder for an attacker // (they would need to predict the fd number of the transient file // we just opened, and have the ability to bind-mount over // magic-links -- which is something that a lot of tools do not // support). let file = syscalls::openat_follow( proc_rootfd, proc_threadself_subpath(*self, &format!("fd/{}", opath.as_raw_fd())), oflags, 0, ) .map_err(|err| ErrorImpl::RawOsError { operation: "re-open raw procfs subpath".into(), source: err, })?; // As below, we can't use verify_same_procfs_mnt. procfs::verify_is_procfs(&file)?; // Finally, verify that the inode numbers match. This is not // strictly "necessary" (since the opath could be an // attacker-controlled procfs file), but this could at least detect // sloppy /proc/self/fd/* overmounts. if opath.metadata()?.ino() != file.metadata()?.ino() { Err(ErrorImpl::SafetyViolation { description: "fd has an inconsistent inode number after re-opening -- probably a manipulated procfs".into(), })?; } Ok(file) } /// Open a subpath within this [`RawProcfsRoot`]. /// /// As this method is only really used for `fdinfo`, trailing symlinks are /// not followed (i.e. [`OpenFlags::O_NOFOLLOW`] is always implied). pub(crate) fn open_beneath( &self, path: impl AsRef, mut oflags: OpenFlags, ) -> Result { oflags.insert(OpenFlags::O_NOFOLLOW); let fd = if *syscalls::OPENAT2_IS_SUPPORTED { self.openat2_beneath(path, oflags) } else { self.opath_beneath_unchecked(path, oflags) }?; // As this is called from within fetch_mnt_id as a fallback, the only // thing we can do here is verify that it is actually procfs. However, // in practice it will be quite difficult for an attacker to over-mount // every fdinfo file for a process. procfs::verify_is_procfs(&fd)?; Ok(fd) } } #[cfg(test)] mod tests { use super::*; use crate::error::ErrorKind; use pretty_assertions::assert_matches; #[test] fn exists_unchecked() { assert_matches!( RawProcfsRoot::UnsafeGlobal .exists_unchecked("nonexist") .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ENOENT))), r#"exists_unchecked("nonexist") -> ENOENT"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .exists_unchecked("uptime") .map_err(|err| err.kind()), Ok(()), r#"exists_unchecked("uptime") -> Ok"# ); } #[test] fn open_beneath() { assert_matches!( RawProcfsRoot::UnsafeGlobal .open_beneath("nonexist", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ENOENT))), r#"open_beneath("nonexist") -> ENOENT"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .open_beneath("self", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ELOOP))), r#"open_beneath("self") -> ELOOP"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .open_beneath("self/cwd", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ELOOP))), r#"open_beneath("self/cwd") -> ELOOP"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .open_beneath("self/status", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Ok(_), r#"open_beneath("self/status") -> Ok"# ); } #[test] fn openat2_beneath() { if !*syscalls::OPENAT2_IS_SUPPORTED { return; // skip } assert_matches!( RawProcfsRoot::UnsafeGlobal .openat2_beneath("nonexist", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ENOENT))), r#"openat2_beneath("nonexist") -> ENOENT"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .openat2_beneath("self", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ELOOP))), r#"openat2_beneath("self") -> ELOOP"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .openat2_beneath("self/cwd", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ELOOP))), r#"openat2_beneath("self/cwd") -> ELOOP"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .openat2_beneath("self/status", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Ok(_), r#"openat2_beneath("self/status") -> Ok"# ); } #[test] fn opath_beneath_unchecked() { assert_matches!( RawProcfsRoot::UnsafeGlobal .opath_beneath_unchecked("nonexist", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ENOENT))), r#"opath_beneath_unchecked("nonexist") -> ENOENT"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .opath_beneath_unchecked("self", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ELOOP))), r#"opath_beneath_unchecked("self") -> ELOOP"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .opath_beneath_unchecked("self/cwd", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Err(ErrorKind::OsError(Some(libc::ELOOP))), r#"opath_beneath_unchecked("self/cwd") -> ELOOP"# ); assert_matches!( RawProcfsRoot::UnsafeGlobal .opath_beneath_unchecked("self/status", OpenFlags::O_RDONLY) .map_err(|err| err.kind()), Ok(_), r#"opath_beneath_unchecked("self/status") -> Ok"# ); } } pathrs-0.2.1/src/utils/sysctl.rs000064400000000000000000000122111046102023000147400ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ use crate::{ error::{Error, ErrorExt, ErrorImpl}, flags::OpenFlags, procfs::{ProcfsBase, ProcfsHandle}, }; use std::{ io::{BufRead, BufReader}, path::PathBuf, str::FromStr, }; pub(crate) fn sysctl_read_parse(procfs: &ProcfsHandle, sysctl: &str) -> Result where T: FromStr, T::Err: Into + Into, { // "/proc/sys" let mut sysctl_path = PathBuf::from("sys"); // Convert "foo.bar.baz" to "foo/bar/baz". sysctl_path.push(sysctl.replace(".", "/")); let sysctl_file = procfs.open(ProcfsBase::ProcRoot, sysctl_path, OpenFlags::O_RDONLY)?; // Just read the first line. let mut reader = BufReader::new(sysctl_file); let mut line = String::new(); reader .read_line(&mut line) .map_err(|err| ErrorImpl::OsError { operation: format!("read first line of {sysctl:?} sysctl").into(), source: err, })?; // Strip newlines. line.trim_end_matches("\n") .parse() .map_err(Error::from) .with_wrap(|| { format!( "could not parse sysctl {sysctl:?} as {:?}", std::any::type_name::() ) }) } #[cfg(test)] mod tests { use super::*; use crate::{ error::{Error, ErrorKind}, procfs::ProcfsHandle, }; use once_cell::sync::Lazy; use pretty_assertions::assert_eq; // MSRV(1.80): Use LazyLock. static TEST_PROCFS_HANDLE: Lazy = Lazy::new(|| ProcfsHandle::new().expect("should be able to get some /proc handle")); #[test] fn bad_sysctl_file_noexist() { assert_eq!( sysctl_read_parse::(&TEST_PROCFS_HANDLE, "nonexistent.dummy.sysctl.path") .as_ref() .map_err(Error::kind), Err(ErrorKind::OsError(Some(libc::ENOENT))), "reading line from non-existent sysctl", ); assert_eq!( sysctl_read_parse::(&TEST_PROCFS_HANDLE, "nonexistent.sysctl.path") .as_ref() .map_err(Error::kind), Err(ErrorKind::OsError(Some(libc::ENOENT))), "parsing line from non-existent sysctl", ); } #[test] fn bad_sysctl_file_noread() { assert_eq!( sysctl_read_parse::(&TEST_PROCFS_HANDLE, "vm.drop_caches") .as_ref() .map_err(Error::kind), Err(ErrorKind::OsError(Some(libc::EACCES))), "reading line from non-readable sysctl", ); assert_eq!( sysctl_read_parse::(&TEST_PROCFS_HANDLE, "vm.drop_caches") .as_ref() .map_err(Error::kind), Err(ErrorKind::OsError(Some(libc::EACCES))), "parse line from non-readable sysctl", ); } #[test] fn bad_sysctl_parse_invalid_multinumber() { assert!(sysctl_read_parse::(&TEST_PROCFS_HANDLE, "kernel.printk").is_ok()); assert_eq!( sysctl_read_parse::(&TEST_PROCFS_HANDLE, "kernel.printk") .as_ref() .map_err(Error::kind), Err(ErrorKind::InternalError), "parsing line from multi-number sysctl", ); } #[test] fn bad_sysctl_parse_invalid_nonnumber() { assert!(sysctl_read_parse::(&TEST_PROCFS_HANDLE, "kernel.random.uuid").is_ok()); assert_eq!( sysctl_read_parse::(&TEST_PROCFS_HANDLE, "kernel.random.uuid") .as_ref() .map_err(Error::kind), Err(ErrorKind::InternalError), "parsing line from non-number sysctl", ); } #[test] fn sysctl_parse_int() { assert!(sysctl_read_parse::(&TEST_PROCFS_HANDLE, "kernel.pid_max").is_ok()); assert!(sysctl_read_parse::(&TEST_PROCFS_HANDLE, "kernel.pid_max").is_ok()); } } pathrs-0.2.1/src/utils.rs000064400000000000000000000031041046102023000134200ustar 00000000000000// SPDX-License-Identifier: MPL-2.0 OR LGPL-3.0-or-later /* * libpathrs: safe path resolution on Linux * Copyright (C) 2019-2025 Aleksa Sarai * Copyright (C) 2019-2025 SUSE LLC * * == 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 . */ #![forbid(unsafe_code)] mod dir; pub(crate) use dir::*; mod path; pub(crate) use path::*; mod fd; pub(crate) use fd::*; mod fdinfo; pub(crate) use fdinfo::*; mod sysctl; pub(crate) use sysctl::*; mod maybe_owned; pub(crate) use maybe_owned::*; mod raw_procfs; pub(crate) use raw_procfs::*;