landlock-0.4.4/.cargo_vcs_info.json0000644000000001360000000000100126300ustar { "git": { "sha1": "89c56e2db04cf0a4d63e192e7b4371af516a1ccc" }, "path_in_vcs": "" }landlock-0.4.4/.github/workflows/pages.yml000064400000000000000000000021711046102023000166400ustar 00000000000000name: GitHub Pages on: push: branches: [ main ] workflow_dispatch: concurrency: group: "pages" cancel-in-progress: false jobs: build: if: github.repository == 'landlock-lsm/rust-landlock' runs-on: ubuntu-24.04 env: CARGO_TERM_COLOR: always permissions: contents: read id-token: write steps: - uses: actions/checkout@v3 - name: Install Rust stable run: | rm ~/.cargo/bin/{cargo-fmt,rustfmt} || : rustup default stable rustup update - name: Build documentation run: rustup run stable cargo doc --no-deps - name: Add index run: | echo '' > target/doc/index.html - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: target/doc deploy: needs: build runs-on: ubuntu-24.04 environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} permissions: pages: write id-token: write steps: - name: Deploy to GitHub Pages uses: actions/deploy-pages@v4 landlock-0.4.4/.github/workflows/rust.yml000064400000000000000000000203441046102023000165400ustar 00000000000000name: Rust checks permissions: {} on: push: branches: [ main ] pull_request: branches: [ main ] env: CARGO_TERM_COLOR: always RUSTDOCFLAGS: -D warnings RUSTFLAGS: -D warnings LANDLOCK_TEST_TOOLS_COMMIT: fad769c39b42183fb2a2e1263fe00dfa5b9f2bda # Ubuntu versions: https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources jobs: commit_list: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Get commit list (push) id: get_commit_list_push if: ${{ github.event_name == 'push' }} run: | echo "id0=$GITHUB_SHA" > $GITHUB_OUTPUT echo "List of tested commits:" > $GITHUB_STEP_SUMMARY sed -n 's,^id[0-9]\+=\(.*\),- https://github.com/landlock-lsm/rust-landlock/commit/\1,p' -- $GITHUB_OUTPUT >> $GITHUB_STEP_SUMMARY - name: Get commit list (PR) id: get_commit_list_pr if: ${{ github.event_name == 'pull_request' }} run: | git rev-list --reverse refs/remotes/origin/${{ github.base_ref }}..${{ github.event.pull_request.head.sha }} | awk '{ print "id" NR "=" $1 }' > $GITHUB_OUTPUT git diff --quiet ${{ github.event.pull_request.head.sha }} ${{ github.sha }} || echo "id0=$GITHUB_SHA" >> $GITHUB_OUTPUT echo "List of tested commits:" > $GITHUB_STEP_SUMMARY sed -n 's,^id[0-9]\+=\(.*\),- https://github.com/landlock-lsm/rust-landlock/commit/\1,p' -- $GITHUB_OUTPUT >> $GITHUB_STEP_SUMMARY outputs: commits: ${{ toJSON(steps.*.outputs.*) }} kernel_list: runs-on: ubuntu-24.04 outputs: kernels: ${{ toJSON(steps.id.outputs.*) }} steps: - name: Identify latest Linux versions id: id run: | echo "List of tested kernels:" > $GITHUB_STEP_SUMMARY abi=0 for version in 5.10 5.15 6.1 6.4 6.7 6.10 6.12; do commit="$(git ls-remote https://github.com/landlock-lsm/linux refs/heads/linux-${version}.y | awk 'NR == 1 { print $1 }')" if [[ -z "${commit}" ]]; then echo "ERROR: Failed to fetch Linux ${version}" >&2 exit 1 fi echo "kernel_${abi}={ \"version\":\"${version}\", \"abi\":\"${abi}\", \"commit\":\"${commit}\" }" >> $GITHUB_OUTPUT echo "- Linux ${version}.y with Landlock ABI ${abi}: https://github.com/landlock-lsm/linux/commit/${commit}" >> $GITHUB_STEP_SUMMARY let abi++ || : done get_kernels: runs-on: ubuntu-24.04 needs: kernel_list strategy: fail-fast: false matrix: kernel: ${{ fromJSON(needs.kernel_list.outputs.kernels) }} steps: - name: Cache Linux ${{ fromJSON(matrix.kernel).version}}.y id: cache_linux uses: actions/cache@v4 with: path: linux-${{ fromJSON(matrix.kernel).version }} key: linux-${{ fromJSON(matrix.kernel).commit }}-${{ env.LANDLOCK_TEST_TOOLS_COMMIT }} - name: Clone Landlock test tools if: steps.cache_linux.outputs.cache-hit != 'true' uses: actions/checkout@v4 with: repository: landlock-lsm/landlock-test-tools ref: ${{ env.LANDLOCK_TEST_TOOLS_COMMIT }} path: landlock-test-tools - name: Clone Linux ${{ fromJSON(matrix.kernel).version}}.y if: steps.cache_linux.outputs.cache-hit != 'true' uses: actions/checkout@v4 with: repository: landlock-lsm/linux ref: ${{ fromJSON(matrix.kernel).commit }} path: linux - name: Build Linux ${{ fromJSON(matrix.kernel).version}}.y if: steps.cache_linux.outputs.cache-hit != 'true' working-directory: linux run: | O=. ../landlock-test-tools/check-linux.sh build_light mv linux ../linux-${{ fromJSON(matrix.kernel).version}} ubuntu_24_rust_msrv: runs-on: ubuntu-24.04 needs: commit_list strategy: fail-fast: false matrix: commit: ${{ fromJSON(needs.commit_list.outputs.commits) }} env: LANDLOCK_CRATE_TEST_ABI: 5 steps: - uses: actions/checkout@v4 with: ref: ${{ matrix.commit }} - name: Clone Landlock test tools uses: actions/checkout@v4 with: repository: landlock-lsm/landlock-test-tools ref: ${{ env.LANDLOCK_TEST_TOOLS_COMMIT }} path: landlock-test-tools - name: Get MSRV run: sed -n 's/^rust-version = "\([0-9.]\+\)"$/RUST_TOOLCHAIN=\1/p' Cargo.toml >> $GITHUB_ENV - name: Install 32-bit development libraries run: | sudo apt update sudo apt install gcc-multilib libc6-dev-i386 - name: Install Rust MSRV run: | rm ~/.cargo/bin/{cargo-fmt,rustfmt} || : rustup self update rustup default ${{ env.RUST_TOOLCHAIN }} rustup update ${{ env.RUST_TOOLCHAIN }} rustup target add i686-unknown-linux-gnu rustup target add x86_64-unknown-linux-gnu - name: Build (x86_64) run: rustup run ${{ env.RUST_TOOLCHAIN }} cargo build --target x86_64-unknown-linux-gnu --verbose - name: Build (x86) run: rustup run ${{ env.RUST_TOOLCHAIN }} cargo build --target i686-unknown-linux-gnu --verbose - name: Run tests against the local kernel (Landlock ABI ${{ env.LANDLOCK_CRATE_TEST_ABI }} on x86_64) run: rustup run ${{ env.RUST_TOOLCHAIN }} cargo test --target x86_64-unknown-linux-gnu --verbose - name: Run tests against the local kernel (Landlock ABI ${{ env.LANDLOCK_CRATE_TEST_ABI }} on x86) run: rustup run ${{ env.RUST_TOOLCHAIN }} cargo test --target i686-unknown-linux-gnu --verbose - name: Run tests against Linux 6.1 run: CARGO="rustup run ${{ env.RUST_TOOLCHAIN }} cargo" ./landlock-test-tools/test-rust.sh linux-6.1 2 ubuntu_22_rust_stable: runs-on: ubuntu-22.04 needs: commit_list strategy: fail-fast: false matrix: commit: ${{ fromJSON(needs.commit_list.outputs.commits) }} env: LANDLOCK_CRATE_TEST_ABI: 4 steps: - name: Install Rust stable run: | rm ~/.cargo/bin/{cargo-fmt,rustfmt} || : rustup self update rustup default stable rustup component add rustfmt clippy rustup update - uses: actions/checkout@v4 with: ref: ${{ matrix.commit }} - name: Build run: rustup run stable cargo build --verbose - name: Run tests against the local kernel (Landlock ABI ${{ env.LANDLOCK_CRATE_TEST_ABI }}) run: rustup run stable cargo test --verbose - name: Check format run: rustup run stable cargo fmt --all -- --check - name: Check source with Clippy run: rustup run stable cargo clippy -- --deny warnings - name: Check tests with Clippy run: rustup run stable cargo clippy --tests -- --deny warnings - name: Check documentation run: rustup run stable cargo doc --no-deps ubuntu_24_rust_stable: runs-on: ubuntu-24.04 needs: [commit_list, kernel_list, get_kernels] strategy: fail-fast: false matrix: commit: ${{ fromJSON(needs.commit_list.outputs.commits) }} kernel: ${{ fromJSON(needs.kernel_list.outputs.kernels) }} env: LANDLOCK_CRATE_TEST_ABI: 4 # $CARGO is used by landlock-test-tools/test-rust.sh CARGO: rustup run stable cargo steps: - name: Install Rust stable run: | rm ~/.cargo/bin/{cargo-fmt,rustfmt} || : rustup self update rustup default stable rustup update - name: Clone Landlock test tools uses: actions/checkout@v4 with: repository: landlock-lsm/landlock-test-tools ref: ${{ env.LANDLOCK_TEST_TOOLS_COMMIT }} path: landlock-test-tools - name: Clone rust-landlock uses: actions/checkout@v4 with: ref: ${{ matrix.commit }} path: rust-landlock - name: Get Linux ${{ fromJSON(matrix.kernel).version}}.y uses: actions/cache/restore@v4 with: path: linux-${{ fromJSON(matrix.kernel).version }} key: linux-${{ fromJSON(matrix.kernel).commit }}-${{ env.LANDLOCK_TEST_TOOLS_COMMIT }} fail-on-cache-miss: true - name: Run tests against Linux ${{ fromJSON(matrix.kernel).version }}.y working-directory: rust-landlock run: ../landlock-test-tools/test-rust.sh ../linux-${{ fromJSON(matrix.kernel).version }} ${{ fromJSON(matrix.kernel).abi }} landlock-0.4.4/CHANGELOG.md000064400000000000000000000220161046102023000132320ustar 00000000000000# Landlock changelog ## [v0.4.4](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.4.4) ### New API - Added support for all architectures ([PR #111](https://github.com/landlock-lsm/rust-landlock/pull/111)). - Added `LandlockStatus` type to query the running kernel and display information about available Landlock features ([PR #103](https://github.com/landlock-lsm/rust-landlock/pull/103) and [PR #113](https://github.com/landlock-lsm/rust-landlock/pull/113)). ### Dependencies - Bumped MSRV to Rust 1.68 ([PR #112](https://github.com/landlock-lsm/rust-landlock/pull/112)). ### Testing - Extended CI to build and test on i686 architecture ([PR #111](https://github.com/landlock-lsm/rust-landlock/pull/111)). ### Example - Enhanced sandboxer example to print helpful hints about Landlock status ([PR #103](https://github.com/landlock-lsm/rust-landlock/pull/103)). ## [v0.4.3](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.4.3) ### New API - Implemented common traits (e.g., `Debug`) for public types ([PR #108](https://github.com/landlock-lsm/rust-landlock/pull/108)). ### Documentation - Extended [CONTRIBUTING.md](CONTRIBUTING.md) documentation with additional testing and development guidelines ([PR #95](https://github.com/landlock-lsm/rust-landlock/pull/95)). - Added more background information to [`path_beneath_rules()`](https://landlock.io/rust-landlock/landlock/fn.path_beneath_rules.html) documentation ([PR #94](https://github.com/landlock-lsm/rust-landlock/pull/94)). ### Testing - Added test case for `AccessFs::from_file()` method ([PR #92](https://github.com/landlock-lsm/rust-landlock/pull/92)). ## [v0.4.2](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.4.2) ### New API - Added support for Landlock ABI 6: control abstract UNIX sockets and signal scoping with the new [`Ruleset::scope()`](https://landlock.io/rust-landlock/landlock/struct.Ruleset.html#method.scope) method taking a [`Scope`](https://landlock.io/rust-landlock/landlock/enum.Scope.html) enum ([PR #96](https://github.com/landlock-lsm/rust-landlock/pull/96) and [PR #98](https://github.com/landlock-lsm/rust-landlock/pull/98)). - Added `From` implementation for `Option` ([PR #104](https://github.com/landlock-lsm/rust-landlock/pull/104)) - Introduced a new [`HandledAccess`](https://landlock.io/rust-landlock/landlock/trait.HandledAccess.html) trait specific to `AccessFs` and `AccessNet` (commit [554217dda0b7](https://github.com/landlock-lsm/rust-landlock/commit/554217dda0b775756e38db71f471dd414b199234)). - Added a new [`Errno`](https://landlock.io/rust-landlock/landlock/struct.Errno.html) type to improve FFI support ([PR #86](https://github.com/landlock-lsm/rust-landlock/pull/86) and [PR #102](https://github.com/landlock-lsm/rust-landlock/pull/102)). - Exposed `From` implementation for [`ABI`](https://landlock.io/rust-landlock/landlock/enum.ABI.html) ([PR #88](https://github.com/landlock-lsm/rust-landlock/pull/88)). ### Documentation - Added clarifying notes about `AccessFs::WriteFile` behavior and `path_beneath_rules` usage ([PR #80](https://github.com/landlock-lsm/rust-landlock/pull/80)). - Introduced [CONTRIBUTING.md](CONTRIBUTING.md) with testing workflow explanations ([PR #76](https://github.com/landlock-lsm/rust-landlock/pull/76)). ### Testing - Enhanced test coverage for new API and added testing against Linux 6.12 ([PR #96](https://github.com/landlock-lsm/rust-landlock/pull/96)). - Updated CI configuration to use the latest Ubuntu versions ([PR #87](https://github.com/landlock-lsm/rust-landlock/pull/87) and [PR #97](https://github.com/landlock-lsm/rust-landlock/pull/97)). - Modified default `LANDLOCK_CRATE_TEST_ABI` to match the current kernel for more convenient local testing ([PR #76](https://github.com/landlock-lsm/rust-landlock/pull/76)). ### Example - Synchronized the sandboxer example with the C version ([PR #101](https://github.com/landlock-lsm/rust-landlock/pull/101)): improved error handling for inaccessible file paths and enhanced help documentation. ## [v0.4.1](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.4.1) ### New API Add support for Landlock ABI 5: control IOCTL commands on character and block devices with the new [`AccessFs::IoctlDev`](https://landlock.io/rust-landlock/landlock/enum.AccessFs.html#variant.IoctlDev) right ([PR #74](https://github.com/landlock-lsm/rust-landlock/pull/74)). ### Testing Improved the CI to better test against different kernel versions ([PR #72](https://github.com/landlock-lsm/rust-landlock/pull/72)). ## [v0.4.0](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.4.0) ### New API Add support for Landlock ABI 4: control TCP binding and connection according to specified network ports. This is now possible with the [`AccessNet`](https://landlock.io/rust-landlock/landlock/enum.AccessNet.html) rights and the [`NetPort`](https://landlock.io/rust-landlock/landlock/struct.NetPort.html) rule ([PR #55](https://github.com/landlock-lsm/rust-landlock/pull/55)). ### Breaking change The `from_read()` and `from_write()` methods moved from the `Access` trait to the `AccessFs` struct ([commit 68f066eba571](https://github.com/landlock-lsm/rust-landlock/commit/68f066eba571c1f9212f5a07016aac9ffb0d1c27)). ### Compatibility management Improve compatibility consistency and prioritize runtime errors against compatibility errors ([PR #67](https://github.com/landlock-lsm/rust-landlock/pull/67)). Fixed a corner case where a ruleset was created on a kernel not supporting Landlock, while requesting to add a rule with an access right handled by the ruleset (`BestEffort`). When trying to enforce this ruleset, this led to a runtime error (i.e. wrong file descriptor) instead of a compatibility error. To simplify compatibility management, always call `prctl(PR_SET_NO_NEW_PRIVS, 1)` by default (see `set_no_new_privs()`). This was required to get a consistent compatibility management and it should not be an issue given that this feature is supported by all LTS kernels ([commit d99f75155bec](https://github.com/landlock-lsm/rust-landlock/commit/d99f75155bec2040cf4ce1532007cd3b8a23e2fb)). ## [v0.3.1](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.3.1) ### New API Add [`RulesetCreated::try_clone()`](https://landlock.io/rust-landlock/landlock/struct.RulesetCreated.html#method.try_clone) ([PR #38](https://github.com/landlock-lsm/rust-landlock/pull/38)). ## [v0.3.0](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.3.0) ### New API Add support for Landlock ABI 3: control truncate operations with the new [`AccessFs::Truncate`](https://landlock.io/rust-landlock/landlock/enum.AccessFs.html#variant.Truncate) right ([PR #40](https://github.com/landlock-lsm/rust-landlock/pull/40)). Revamp the compatibility handling and add a new [`set_compatibility()`](https://landlock.io/rust-landlock/landlock/trait.Compatible.html#method.set_compatibility) method for `Ruleset`, `RulesetCreated`, and `PathBeneath`. We can now fine-tune the compatibility behavior according to the running kernel and then the supported features thanks to three compatible levels: best effort, soft requirement and hard requirement ([PR #12](https://github.com/landlock-lsm/rust-landlock/pull/12)). Add a new [`AccessFs::from_file()`](https://landlock.io/rust-landlock/landlock/enum.AccessFs.html#method.from_file) helper ([commit 0b3238c6dd70](https://github.com/landlock-lsm/rust-landlock/commit/0b3238c6dd70)). ### Deprecated API Deprecate the [`set_best_effort()`](https://landlock.io/rust-landlock/landlock/trait.Compatible.html#method.set_best_effort) method and replace it with `set_compatibility()` ([PR #12](https://github.com/landlock-lsm/rust-landlock/pull/12)). Deprecate [`Ruleset::new()`](https://landlock.io/rust-landlock/landlock/struct.Ruleset.html#method.new) and replace it with `Ruleset::default()` ([PR #44](https://github.com/landlock-lsm/rust-landlock/pull/44)). ### Breaking changes We now check that a ruleset really handles at least one access right, which can now cause `Ruleset::create()` to return an error if the ruleset compatibility level is `HardRequirement` or `set_best_effort(false)` ([commit 95addc13b4a8](https://github.com/landlock-lsm/rust-landlock/commit/95addc13b4a8)). We now check that access rights passed to `add_rule()` make sense according to the file type. To handle most use cases, `path_beneath_rules()` now automatically check and downgrade access rights for files (i.e. remove superfluous directory-only access rights, [commit 8e47940b3722](https://github.com/landlock-lsm/rust-landlock/commit/8e47940b3722)). ### Testing Test coverage in the CI is greatly improved by running all tests on all relevant kernel versions: Linux 5.10, 5.15, 6.1, and 6.4 ([PR #41](https://github.com/landlock-lsm/rust-landlock/pull/41)). Run each test in a dedicated thread to avoid inconsistent behavior ([PR #46](https://github.com/landlock-lsm/rust-landlock/pull/46)). ## [v0.2.0](https://github.com/landlock-lsm/rust-landlock/releases/tag/v0.2.0) This is the first major release of this crate. It brings a high-level interface to the Landlock kernel interface. landlock-0.4.4/CONTRIBUTING.md000064400000000000000000000025211046102023000136510ustar 00000000000000# Contributing Thanks for your interest in contributing to rust-landlock! ## Testing vs kernel ABI The Landlock functionality exposed differs between kernel versions. Based on the Landlock ABI version of the running system, rust-landlock runs different subsets of tests. For local development, running `cargo test` will test against your currently running kernel version (and the Landlock ABI that it ships). However, this may result in some tests being skipped. To fully test a change, it should be verified against a range of ABI versions. This is done by the Github Actions CI, but doing so locally is challenging. Using the `LANDLOCK_CRATE_TEST_ABI` variable, the tested ABI version can be overridden. For more details, take a look at the comment in [`compat.rs:current_kernel_abi()`][current-kernel-abi]. For more information about Landlock ABIs, see: [enum ABI][enum-abi] [current-kernel-abi]: https://github.com/landlock-lsm/rust-landlock/blob/main/src/compat.rs [enum-abi]: https://landlock.io/rust-landlock/landlock/enum.ABI.html ## Licensing & DCO rust-landlock is double-licensed under the terms of [Apache 2.0][apache-2.0] and [MIT][mit]. All changes submitted to rust-landlock must be [signed off][dco]. [apache-2.0]: https://spdx.org/licenses/Apache-2.0.html [mit]: https://spdx.org/licenses/MIT.html [dco]: https://github.com/apps/dco landlock-0.4.4/COPYRIGHT000064400000000000000000000005441046102023000127160ustar 00000000000000Copyright 2020 Mickaël Salaün Licensed under the Apache License, Version 2.0 or the MIT license , at your option. All files in the project carrying such notice may not be copied, modified, or distributed except according to those terms. landlock-0.4.4/Cargo.lock0000644000000067650000000000100106210ustar # 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 = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "landlock" version = "0.4.4" dependencies = [ "anyhow", "enumflags2", "lazy_static", "libc", "strum", "strum_macros", "thiserror", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "strum" version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" [[package]] name = "strum_macros" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", "syn", ] [[package]] name = "syn" version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[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", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" landlock-0.4.4/Cargo.toml0000644000000026650000000000100106370ustar # 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.68" name = "landlock" version = "0.4.4" build = false exclude = [".gitignore"] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Landlock LSM helpers" homepage = "https://landlock.io" readme = "README.md" keywords = [ "access-control", "linux", "sandbox", "security", ] categories = [ "api-bindings", "os::linux-apis", "virtualization", "filesystem", ] license = "MIT OR Apache-2.0" repository = "https://github.com/landlock-lsm/rust-landlock" [lib] name = "landlock" path = "src/lib.rs" [[example]] name = "sandboxer" path = "examples/sandboxer.rs" [dependencies.enumflags2] version = "0.7" [dependencies.libc] version = "0.2.175" [dependencies.thiserror] version = "2.0" [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.lazy_static] version = "1" [dev-dependencies.strum] version = "0.26" [dev-dependencies.strum_macros] version = "0.26" landlock-0.4.4/Cargo.toml.orig000064400000000000000000000011221046102023000143030ustar 00000000000000[package] name = "landlock" version = "0.4.4" edition = "2021" rust-version = "1.68" description = "Landlock LSM helpers" homepage = "https://landlock.io" repository = "https://github.com/landlock-lsm/rust-landlock" license = "MIT OR Apache-2.0" keywords = ["access-control", "linux", "sandbox", "security"] categories = ["api-bindings", "os::linux-apis", "virtualization", "filesystem"] exclude = [".gitignore"] readme = "README.md" [dependencies] enumflags2 = "0.7" libc = "0.2.175" thiserror = "2.0" [dev-dependencies] anyhow = "1.0" lazy_static = "1" strum = "0.26" strum_macros = "0.26" landlock-0.4.4/LICENSE-APACHE000064400000000000000000000261231046102023000133500ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2020 Mickaël Salaün Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. landlock-0.4.4/LICENSE-MIT000064400000000000000000000020611046102023000130530ustar 00000000000000MIT License Copyright (c) 2020 Mickaël Salaün Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. landlock-0.4.4/README.md000064400000000000000000000032611046102023000127010ustar 00000000000000# Rust Landlock library Landlock is a security feature available since Linux 5.13. The goal is to enable to restrict ambient rights (e.g., global filesystem access) for a set of processes by creating safe security sandboxes as new security layers in addition to the existing system-wide access-controls. This kind of sandbox is expected to help mitigate the security impact of bugs, unexpected or malicious behaviors in applications. Landlock empowers any process, including unprivileged ones, to securely restrict themselves. More information about Landlock can be found in the [official website](https://landlock.io). This Rust crate provides a safe abstraction for the Landlock system calls along with some helpers. ## Use cases This crate is especially useful to protect users' data by sandboxing: * trusted applications dealing with potentially malicious data (e.g., complex file format, network request) that could exploit security vulnerabilities; * sandbox managers, container runtimes or shells launching untrusted applications. ## Examples A simple example can be found with the [`path_beneath_rules()`](https://landlock.io/rust-landlock/landlock/fn.path_beneath_rules.html) helper. More complex examples can be found with the [`Ruleset` documentation](https://landlock.io/rust-landlock/landlock/struct.Ruleset.html) and the [sandboxer example](examples/sandboxer.rs). ## [Crate documentation](https://landlock.io/rust-landlock/landlock/) ## Changelog * [v0.4.4](CHANGELOG.md#v044) * [v0.4.3](CHANGELOG.md#v043) * [v0.4.2](CHANGELOG.md#v042) * [v0.4.1](CHANGELOG.md#v041) * [v0.4.0](CHANGELOG.md#v040) * [v0.3.1](CHANGELOG.md#v031) * [v0.3.0](CHANGELOG.md#v030) * [v0.2.0](CHANGELOG.md#v020) landlock-0.4.4/examples/sandboxer.rs000064400000000000000000000220661046102023000155770ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT // This is an idiomatic Rust rewrite of a C example: // https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/samples/landlock/sandboxer.c use anyhow::{anyhow, bail, Context}; use landlock::{ path_beneath_rules, Access, AccessFs, AccessNet, BitFlags, LandlockStatus, NetPort, PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, Scope, ABI, }; use std::env; use std::ffi::OsStr; use std::os::unix::ffi::{OsStrExt, OsStringExt}; use std::os::unix::process::CommandExt; use std::process::Command; const ENV_FS_RO_NAME: &str = "LL_FS_RO"; const ENV_FS_RW_NAME: &str = "LL_FS_RW"; const ENV_TCP_BIND_NAME: &str = "LL_TCP_BIND"; const ENV_TCP_CONNECT_NAME: &str = "LL_TCP_CONNECT"; const ENV_SCOPED_NAME: &str = "LL_SCOPED"; struct PathEnv { paths: Vec, access: BitFlags, } impl PathEnv { /// Create an object able to iterate PathBeneath rules /// /// # Arguments /// /// * `name`: String identifying an environment variable containing paths requested to be /// allowed. Paths are separated with ":", e.g. "/bin:/lib:/usr:/proc". In case an empty /// string is provided, NO restrictions are applied. /// * `access`: Set of access-rights allowed for each of the parsed paths. fn new<'a>(name: &'a str, access: BitFlags) -> anyhow::Result { Ok(Self { paths: env::var_os(name) .ok_or(anyhow!("missing environment variable {name}"))? .into_vec(), access, }) } fn iter(&self) -> impl Iterator>> + '_ { let is_empty = self.paths.is_empty(); path_beneath_rules( self.paths .split(|b| *b == b':') // Skips the first empty element of an empty string. .skip_while(move |_| is_empty) .map(OsStr::from_bytes), self.access, ) .map(|r| Ok(r?)) } } struct PortEnv { ports: Vec, access: AccessNet, } impl PortEnv { fn new<'a>(name: &'a str, access: AccessNet) -> anyhow::Result { Ok(Self { ports: env::var_os(name).unwrap_or_default().into_vec(), access, }) } fn iter(&self) -> impl Iterator> + '_ { let is_empty = self.ports.is_empty(); self.ports .split(|b| *b == b':') // Skips the first empty element of an empty string. .skip_while(move |_| is_empty) .map(OsStr::from_bytes) .map(|port| { let port = port .to_str() .context("failed to convert port string")? .parse::() .context("failed to convert port to 16-bit integer")?; Ok(NetPort::new(port, self.access)) }) } } fn main() -> anyhow::Result<()> { let mut args = env::args_os(); let program_name = args .next() .context("Missing the sandboxer program name (i.e. argv[0])")?; let cmd_name = args.next().ok_or_else(|| { let program_name = program_name.to_string_lossy(); eprintln!( "usage: {ENV_FS_RO_NAME}=\"...\" {ENV_FS_RW_NAME}=\"...\" [other environment variables] {program_name} [args]...\n" ); eprintln!("Execute the given command in a restricted environment."); eprintln!("Multi-valued settings (lists of ports, paths, scopes) are colon-delimited.\n"); eprintln!("Mandatory settings:"); eprintln!("* {ENV_FS_RO_NAME}: paths allowed to be used in a read-only way"); eprintln!("* {ENV_FS_RW_NAME}: paths allowed to be used in a read-write way\n"); eprintln!("Optional settings (when not set, their associated access check is always allowed, which is different from an empty string which means an empty list):"); eprintln!("* {ENV_TCP_BIND_NAME}: ports allowed to bind (server)"); eprintln!("* {ENV_TCP_CONNECT_NAME}: ports allowed to connect (client)"); eprintln!("* {ENV_SCOPED_NAME}: actions denied on the outside of the Landlock domain:"); eprintln!(" - \"a\" to restrict opening abstract unix sockets"); eprintln!(" - \"s\" to restrict sending signals"); eprintln!( "\nExample:\n\ {ENV_FS_RO_NAME}=\"${{PATH}}:/lib:/usr:/proc:/etc:/dev/urandom\" \ {ENV_FS_RW_NAME}=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" \ {ENV_TCP_BIND_NAME}=\"9418\" \ {ENV_TCP_CONNECT_NAME}=\"80:443\" \ {ENV_SCOPED_NAME}=\"a:s\" \ {program_name} bash -i\n" ); anyhow!("Missing command") })?; let abi = ABI::V6; let mut ruleset = Ruleset::default().handle_access(AccessFs::from_all(abi))?; let ruleset_ref = &mut ruleset; if env::var_os(ENV_TCP_BIND_NAME).is_some() { ruleset_ref.handle_access(AccessNet::BindTcp)?; } if env::var_os(ENV_TCP_CONNECT_NAME).is_some() { ruleset_ref.handle_access(AccessNet::ConnectTcp)?; } if let Some(scoped) = env::var_os(ENV_SCOPED_NAME) { let mut abstract_scoping = false; let mut signal_scoping = false; let scopes = scoped.to_string_lossy(); let is_empty = scopes.is_empty(); for scope in scopes.split(':').skip_while(move |_| is_empty) { match scope { "a" => { if abstract_scoping { bail!("Duplicate scope 'a'"); } ruleset_ref.scope(Scope::AbstractUnixSocket)?; abstract_scoping = true; } "s" => { if signal_scoping { bail!("Duplicate scope 's'"); } ruleset_ref.scope(Scope::Signal)?; signal_scoping = true; } _ => bail!("Unknown scope \"{scope}\""), } } } let status = ruleset .create()? .add_rules(PathEnv::new(ENV_FS_RO_NAME, AccessFs::from_read(abi))?.iter())? .add_rules(PathEnv::new(ENV_FS_RW_NAME, AccessFs::from_all(abi))?.iter())? .add_rules(PortEnv::new(ENV_TCP_BIND_NAME, AccessNet::BindTcp)?.iter())? .add_rules(PortEnv::new(ENV_TCP_CONNECT_NAME, AccessNet::ConnectTcp)?.iter())? .restrict_self() .expect("Failed to enforce ruleset"); match status.landlock { // This should never happen because of the previous check: LandlockStatus::NotEnabled => { eprintln!( "Hint: Landlock is currently disabled. \ It can be enabled in the kernel configuration by prepending \"landlock,\" to the content of CONFIG_LSM, or at boot time by setting the same content to the \"lsm\" kernel parameter." ); } LandlockStatus::NotImplemented => { eprintln!( "Hint: Landlock is not built into the current kernel. \ To support it, build the kernel with CONFIG_SECURITY_LANDLOCK=y and \ prepend \"landlock,\" to the content of CONFIG_LSM." ); } LandlockStatus::Available { kernel_abi: Some(raw_abi), .. } => { eprintln!( "Hint: This sandboxer only supports Landlock ABI version up to {abi} \ whereas the current kernel supports Landlock ABI version {raw_abi}. \ To leverage all Landlock features, update this sandboxer." ); } LandlockStatus::Available { kernel_abi: None, effective_abi, } => { if effective_abi < abi { eprintln!( "Hint: This sandboxer supports Landlock ABI version up to {abi} \ but the current kernel only supports Landlock ABI version {effective_abi}. \ To leverage all Landlock features, update the kernel." ); } else if effective_abi > abi { // This should not happen because the ABI used by the sandboxer // should be the latest supported by the Landlock crate, and // they should be updated at the same time. eprintln!( "Warning: This sandboxer only supports Landlock ABI version up to {abi} \ but the current kernel supports Landlock ABI version {effective_abi}. \ To leverage all Landlock features, update this sandboxer." ); } } } if status.ruleset == RulesetStatus::NotEnforced { bail!("The ruleset cannot be enforced at all"); } eprintln!("Executing the sandboxed command..."); Err(Command::new(cmd_name) .env_remove(ENV_FS_RO_NAME) .env_remove(ENV_FS_RW_NAME) .env_remove(ENV_TCP_BIND_NAME) .env_remove(ENV_TCP_CONNECT_NAME) .args(args) .exec() .into()) } landlock-0.4.4/src/access.rs000064400000000000000000000146151046102023000140250ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT use crate::{ private, AccessError, AddRuleError, AddRulesError, BitFlags, CompatError, CompatResult, HandleAccessError, HandleAccessesError, Ruleset, TailoredCompatLevel, TryCompat, ABI, }; use enumflags2::BitFlag; #[cfg(test)] use crate::{make_bitflags, AccessFs, CompatLevel, CompatState, Compatibility}; pub trait Access: BitFlag + private::Sealed { /// Gets the access rights defined by a specific [`ABI`]. fn from_all(abi: ABI) -> BitFlags; } // This HandledAccess trait is useful to document the API. pub trait HandledAccess: Access {} pub trait PrivateHandledAccess: HandledAccess { fn ruleset_handle_access( ruleset: &mut Ruleset, access: BitFlags, ) -> Result<(), HandleAccessesError> where Self: Access; fn into_add_rules_error(error: AddRuleError) -> AddRulesError where Self: Access; fn into_handle_accesses_error(error: HandleAccessError) -> HandleAccessesError where Self: Access; } // Creates an illegal/overflowed BitFlags with all its bits toggled, including undefined ones. fn full_negation(flags: BitFlags) -> BitFlags where T: Access, { unsafe { BitFlags::::from_bits_unchecked(!flags.bits()) } } #[test] fn bit_flags_full_negation() { let scoped_negation = !BitFlags::::all(); assert_eq!(scoped_negation, BitFlags::::empty()); // !BitFlags::::all() could be equal to full_negation(BitFlags::::all())) // if all the 64-bits would be used, which is not currently the case. assert_ne!(scoped_negation, full_negation(BitFlags::::all())); } impl TailoredCompatLevel for BitFlags where A: Access {} impl TryCompat for BitFlags where A: Access, { fn try_compat_inner(&mut self, abi: ABI) -> Result, CompatError> { if self.is_empty() { // Empty access-rights would result to a runtime error. Err(AccessError::Empty.into()) } else if !Self::all().contains(*self) { // Unknown access-rights (at build time) would result to a runtime error. // This can only be reached by using the unsafe BitFlags::from_bits_unchecked(). Err(AccessError::Unknown { access: *self, unknown: *self & full_negation(Self::all()), } .into()) } else { let compat = *self & A::from_all(abi); let ret = if compat.is_empty() { Ok(CompatResult::No( AccessError::Incompatible { access: *self }.into(), )) } else if compat != *self { let error = AccessError::PartiallyCompatible { access: *self, incompatible: *self & full_negation(compat), } .into(); Ok(CompatResult::Partial(error)) } else { Ok(CompatResult::Full) }; *self = compat; ret } } } #[test] fn compat_bit_flags() { use crate::ABI; let mut compat: Compatibility = ABI::V1.into(); assert!(compat.state == CompatState::Init); let ro_access = make_bitflags!(AccessFs::{Execute | ReadFile | ReadDir}); assert_eq!( ro_access, ro_access .try_compat(compat.abi(), compat.level, &mut compat.state) .unwrap() .unwrap() ); assert!(compat.state == CompatState::Full); let empty_access = BitFlags::::empty(); assert!(matches!( empty_access .try_compat(compat.abi(), compat.level, &mut compat.state) .unwrap_err(), CompatError::Access(AccessError::Empty) )); let all_unknown_access = unsafe { BitFlags::::from_bits_unchecked(1 << 63) }; assert!(matches!( all_unknown_access.try_compat(compat.abi(), compat.level, &mut compat.state).unwrap_err(), CompatError::Access(AccessError::Unknown { access, unknown }) if access == all_unknown_access && unknown == all_unknown_access )); // An error makes the state final. assert!(compat.state == CompatState::Dummy); let some_unknown_access = unsafe { BitFlags::::from_bits_unchecked(1 << 63 | 1) }; assert!(matches!( some_unknown_access.try_compat(compat.abi(), compat.level, &mut compat.state).unwrap_err(), CompatError::Access(AccessError::Unknown { access, unknown }) if access == some_unknown_access && unknown == all_unknown_access )); assert!(compat.state == CompatState::Dummy); compat = ABI::Unsupported.into(); // Tests that the ruleset is marked as unsupported. assert!(compat.state == CompatState::Init); // Access-rights are valid (but ignored) when they are not required for the current ABI. assert_eq!( None, ro_access .try_compat(compat.abi(), compat.level, &mut compat.state) .unwrap() ); assert!(compat.state == CompatState::No); // Access-rights are not valid when they are required for the current ABI. compat.level = Some(CompatLevel::HardRequirement); assert!(matches!( ro_access.try_compat(compat.abi(), compat.level, &mut compat.state).unwrap_err(), CompatError::Access(AccessError::Incompatible { access }) if access == ro_access )); compat = ABI::V1.into(); // Tests that the ruleset is marked as the unknown compatibility state. assert!(compat.state == CompatState::Init); // Access-rights are valid (but ignored) when they are not required for the current ABI. assert_eq!( ro_access, ro_access .try_compat(compat.abi(), compat.level, &mut compat.state) .unwrap() .unwrap() ); // Tests that the ruleset is in an unsupported state, which is important to be able to still // enforce no_new_privs. assert!(compat.state == CompatState::Full); let v2_access = ro_access | AccessFs::Refer; // Access-rights are not valid when they are required for the current ABI. compat.level = Some(CompatLevel::HardRequirement); assert!(matches!( v2_access.try_compat(compat.abi(), compat.level, &mut compat.state).unwrap_err(), CompatError::Access(AccessError::PartiallyCompatible { access, incompatible }) if access == v2_access && incompatible == AccessFs::Refer )); } landlock-0.4.4/src/compat.rs000064400000000000000000000747141046102023000140550ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT use crate::{uapi, Access, CompatError}; use std::fmt::{self, Display, Formatter}; use std::io::Error; #[cfg(test)] use std::convert::TryInto; #[cfg(test)] use strum::{EnumCount, IntoEnumIterator}; #[cfg(test)] use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; /// Version of the Landlock [ABI](https://en.wikipedia.org/wiki/Application_binary_interface). /// /// `ABI` enables getting the features supported by a specific Landlock ABI /// (without relying on the kernel version which may not be accessible or patched). /// For example, [`AccessFs::from_all(ABI::V1)`](Access::from_all) /// gets all the file system access rights defined by the first version. /// /// Without `ABI`, it would be hazardous to rely on the the full set of access flags /// (e.g., `BitFlags::::all()` or `BitFlags::ALL`), /// a moving target that would change the semantics of your Landlock rule /// when migrating to a newer version of this crate. /// Indeed, a simple `cargo update` or `cargo install` run by any developer /// can result in a new version of this crate (fixing bugs or bringing non-breaking changes). /// This crate cannot give any guarantee concerning the new restrictions resulting from /// these unknown bits (i.e. access rights) that would not be controlled by your application but by /// a future version of this crate instead. /// Because we cannot know what the effect on your application of an unknown restriction would be /// when handling an untested Landlock access right (i.e. denied-by-default access), /// it could trigger bugs in your application. /// /// This crate provides a set of tools to sandbox as much as possible /// while guaranteeing a consistent behavior thanks to the [`Compatible`] methods. /// You should also test with different relevant kernel versions, /// see [landlock-test-tools](https://github.com/landlock-lsm/landlock-test-tools) and /// [CI integration](https://github.com/landlock-lsm/rust-landlock/pull/41). /// /// This way, we can have the guarantee that the use of a set of tested Landlock ABI works as /// expected because features brought by newer Landlock ABI will never be enabled by default /// (cf. [Linux kernel compatibility contract](https://docs.kernel.org/userspace-api/landlock.html#compatibility)). /// /// In a nutshell, test the access rights you request on a kernel that support them and /// on a kernel that doesn't support them. /// /// Derived `Debug` formats are [not stable](https://doc.rust-lang.org/stable/std/fmt/trait.Debug.html#stability). #[cfg_attr(test, derive(EnumIter, EnumCountMacro))] #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[non_exhaustive] pub enum ABI { /// Kernel not supporting Landlock, either because it is not built with Landlock /// or Landlock is not enabled at boot. Unsupported = 0, /// First Landlock ABI, introduced with /// [Linux 5.13](https://git.kernel.org/stable/c/17ae69aba89dbfa2139b7f8024b757ab3cc42f59). V1 = 1, /// Second Landlock ABI, introduced with /// [Linux 5.19](https://git.kernel.org/stable/c/cb44e4f061e16be65b8a16505e121490c66d30d0). V2 = 2, /// Third Landlock ABI, introduced with /// [Linux 6.2](https://git.kernel.org/stable/c/299e2b1967578b1442128ba8b3e86ed3427d3651). V3 = 3, /// Fourth Landlock ABI, introduced with /// [Linux 6.7](https://git.kernel.org/stable/c/136cc1e1f5be75f57f1e0404b94ee1c8792cb07d). V4 = 4, /// Fifth Landlock ABI, introduced with /// [Linux 6.10](https://git.kernel.org/stable/c/2fc0e7892c10734c1b7c613ef04836d57d4676d5). V5 = 5, /// Sixth Landlock ABI, introduced with /// [Linux 6.12](https://git.kernel.org/stable/c/e1b061b444fb01c237838f0d8238653afe6a8094). V6 = 6, } // ABI should not be dynamically created (in other crates) according to the running kernel // to avoid inconsistent behaviors and non-determinism. Creating ABIs based on runtime detection // can lead to unreliable sandboxing where rules might differ between executions. impl ABI { #[cfg(test)] fn is_known(value: i32) -> bool { value > 0 && value < ABI::COUNT as i32 } } /// Converting from an integer to an ABI should only be used for testing. /// Indeed, manually setting the ABI can lead to inconsistent and unexpected behaviors. /// Instead, just use the appropriate access rights, this library will handle the rest. impl From for ABI { fn from(value: i32) -> ABI { match value { n if n <= 0 => ABI::Unsupported, 1 => ABI::V1, 2 => ABI::V2, 3 => ABI::V3, 4 => ABI::V4, 5 => ABI::V5, // Returns the greatest known ABI. _ => ABI::V6, } } } #[test] fn abi_from() { // EOPNOTSUPP (-95), ENOSYS (-38) for n in [-95, -38, -1, 0] { assert_eq!(ABI::from(n), ABI::Unsupported); } let mut last_i = 1; let mut last_abi = ABI::Unsupported; for (i, abi) in ABI::iter().enumerate() { last_i = i.try_into().unwrap(); last_abi = abi; assert_eq!(ABI::from(last_i), last_abi); } assert_eq!(ABI::from(last_i + 1), last_abi); assert_eq!(ABI::from(999), last_abi); } #[test] fn known_abi() { assert!(!ABI::is_known(-1)); assert!(!ABI::is_known(0)); assert!(!ABI::is_known(999)); let mut last_i = -1; for (i, _) in ABI::iter().enumerate().skip(1) { last_i = i as i32; assert!(ABI::is_known(last_i)); } assert!(!ABI::is_known(last_i + 1)); } impl Display for ABI { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { ABI::Unsupported => write!(f, "unsupported"), v => (*v as u32).fmt(f), } } } /// Status of Landlock support for the running system. /// /// This enum is used to represent the status of the Landlock support for the system where the code /// is executed. It can indicate whether Landlock is available or not. /// /// # Warning /// /// Sandboxed programs should only use this data to log or provide information to users, /// not to change their behavior according to this status. Indeed, the `Ruleset` and the other /// types are designed to handle the compatibility in a simple and safe way. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum LandlockStatus { /// Landlock is supported but not enabled (`EOPNOTSUPP`). NotEnabled, /// Landlock is not implemented (i.e. not built into the running kernel: `ENOSYS`). NotImplemented, /// Landlock is available and working on the running system. /// /// This indicates that the kernel supports Landlock and it's properly enabled. /// The crate uses the `effective_abi` for all operations, which represents /// the highest ABI version that both the kernel and this crate understand. Available { /// The effective ABI version that this crate will use for Landlock operations. /// This is the intersection of what the kernel supports and what this crate knows about. effective_abi: ABI, /// The actual kernel ABI version when it's newer than any ABI supported by this crate. /// /// If `Some(version)`, it means the running kernel supports Landlock ABI `version` /// which is higher than the latest ABI known by this crate. /// /// This field is purely informational and is never used for Landlock operations. /// The crate always and only uses `effective_abi` for all functionality. kernel_abi: Option, }, } impl LandlockStatus { // Must remain private to avoid inconsistent behavior using such unknown-at-build-time ABI // e.g., AccessFs::from_all(ABI::new_current()) // // This should not be Default::default() because the returned value would may not be the same // for all users. fn current() -> Self { // Landlock ABI version starts at 1 but errno is only set for negative values. let v = unsafe { uapi::landlock_create_ruleset( std::ptr::null(), 0, uapi::LANDLOCK_CREATE_RULESET_VERSION, ) }; if v < 0 { // The only possible error values should be EOPNOTSUPP and ENOSYS. match Error::last_os_error().raw_os_error() { Some(libc::EOPNOTSUPP) => Self::NotEnabled, _ => Self::NotImplemented, } } else { let abi = ABI::from(v); Self::Available { effective_abi: abi, kernel_abi: (v != abi as i32).then_some(v), } } } } // Test against the running kernel. #[test] fn test_current_landlock_status() { let status = LandlockStatus::current(); if *TEST_ABI == ABI::Unsupported { assert_eq!(status, LandlockStatus::NotImplemented); } else { assert!( matches!(status, LandlockStatus::Available { effective_abi, .. } if effective_abi == *TEST_ABI) ); if std::env::var(TEST_ABI_ENV_NAME).is_ok() { // We cannot reliably check for unknown kernel. assert!(matches!( status, LandlockStatus::Available { kernel_abi: None, .. } )); } } } impl From for ABI { fn from(status: LandlockStatus) -> Self { match status { // The only possible error values should be EOPNOTSUPP and ENOSYS, // but let's convert all kind of errors as unsupported. LandlockStatus::NotEnabled | LandlockStatus::NotImplemented => ABI::Unsupported, LandlockStatus::Available { effective_abi, .. } => effective_abi, } } } // This is only useful to tests and should not be exposed publicly because // the mapping can only be partial. #[cfg(test)] impl From for LandlockStatus { fn from(abi: ABI) -> Self { match abi { // Convert to ENOSYS because of check_ruleset_support() and ruleset_unsupported() tests. ABI::Unsupported => Self::NotImplemented, _ => Self::Available { effective_abi: abi, kernel_abi: None, }, } } } #[cfg(test)] static TEST_ABI_ENV_NAME: &str = "LANDLOCK_CRATE_TEST_ABI"; #[cfg(test)] lazy_static! { static ref TEST_ABI: ABI = match std::env::var("LANDLOCK_CRATE_TEST_ABI") { Ok(s) => { let n = s.parse::().unwrap(); if ABI::is_known(n) || n == 0 { ABI::from(n) } else { panic!("Unknown ABI: {n}"); } } Err(std::env::VarError::NotPresent) => LandlockStatus::current().into(), Err(e) => panic!("Failed to read LANDLOCK_CRATE_TEST_ABI: {e}"), }; } #[cfg(test)] pub(crate) fn can_emulate(mock: ABI, partial_support: ABI, full_support: Option) -> bool { mock < partial_support || mock <= *TEST_ABI || if let Some(full) = full_support { full <= *TEST_ABI } else { partial_support <= *TEST_ABI } } #[cfg(test)] pub(crate) fn get_errno_from_landlock_status() -> Option { match LandlockStatus::current() { LandlockStatus::NotImplemented | LandlockStatus::NotEnabled => { match Error::last_os_error().raw_os_error() { // Returns ENOSYS when the kernel is not built with Landlock support, // or EOPNOTSUPP when Landlock is supported but disabled at boot time. ret @ Some(libc::ENOSYS | libc::EOPNOTSUPP) => ret, // Other values can only come from bogus seccomp filters or debugging tampering. ret => { eprintln!("Current kernel should support this Landlock ABI according to $LANDLOCK_CRATE_TEST_ABI"); eprintln!("Unexpected result: {ret:?}"); unreachable!(); } } } LandlockStatus::Available { .. } => None, } } #[test] fn current_kernel_abi() { // Ensures that the tested Landlock ABI is the latest known version supported by the running // kernel. If this test failed, you need set the LANDLOCK_CRATE_TEST_ABI environment variable // to the Landlock ABI version supported by your kernel. With a missing variable, the latest // Landlock ABI version known by this crate is automatically set. // From Linux 5.13 to 5.18, you need to run: LANDLOCK_CRATE_TEST_ABI=1 cargo test let test_abi = *TEST_ABI; let current_abi = LandlockStatus::current().into(); println!( "Current kernel version: {}", std::fs::read_to_string("/proc/version") .unwrap_or_else(|_| "unknown".into()) .trim() ); println!("Expected Landlock ABI {test_abi:?} whereas the current ABI is {current_abi:#?}"); assert_eq!(test_abi, current_abi); } // CompatState is not public outside this crate. /// Returned by ruleset builder. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum CompatState { /// Initial undefined state. Init, /// All requested restrictions are enforced. Full, /// Some requested restrictions are enforced, following a best-effort approach. Partial, /// The running system doesn't support Landlock. No, /// Final unsupported state. Dummy, } impl CompatState { fn update(&mut self, other: Self) { *self = match (*self, other) { (CompatState::Init, other) => other, (CompatState::Dummy, _) => CompatState::Dummy, (_, CompatState::Dummy) => CompatState::Dummy, (CompatState::No, CompatState::No) => CompatState::No, (CompatState::Full, CompatState::Full) => CompatState::Full, (_, _) => CompatState::Partial, } } } #[test] fn compat_state_update_1() { let mut state = CompatState::Full; state.update(CompatState::Full); assert_eq!(state, CompatState::Full); state.update(CompatState::No); assert_eq!(state, CompatState::Partial); state.update(CompatState::Full); assert_eq!(state, CompatState::Partial); state.update(CompatState::Full); assert_eq!(state, CompatState::Partial); state.update(CompatState::No); assert_eq!(state, CompatState::Partial); state.update(CompatState::Dummy); assert_eq!(state, CompatState::Dummy); state.update(CompatState::Full); assert_eq!(state, CompatState::Dummy); } #[test] fn compat_state_update_2() { let mut state = CompatState::Full; state.update(CompatState::Full); assert_eq!(state, CompatState::Full); state.update(CompatState::No); assert_eq!(state, CompatState::Partial); state.update(CompatState::Full); assert_eq!(state, CompatState::Partial); } #[cfg_attr(test, derive(PartialEq))] #[derive(Copy, Clone, Debug)] pub(crate) struct Compatibility { status: LandlockStatus, pub(crate) level: Option, pub(crate) state: CompatState, } impl From for Compatibility { fn from(status: LandlockStatus) -> Self { Compatibility { status, level: Default::default(), state: CompatState::Init, } } } #[cfg(test)] impl From for Compatibility { fn from(abi: ABI) -> Self { Self::from(LandlockStatus::from(abi)) } } impl Compatibility { // Compatibility is a semi-opaque struct. #[allow(clippy::new_without_default)] pub(crate) fn new() -> Self { LandlockStatus::current().into() } pub(crate) fn update(&mut self, state: CompatState) { self.state.update(state); } pub(crate) fn abi(&self) -> ABI { self.status.into() } pub(crate) fn status(&self) -> LandlockStatus { self.status } } pub(crate) mod private { use crate::CompatLevel; pub trait OptionCompatLevelMut { fn as_option_compat_level_mut(&mut self) -> &mut Option; } } /// Properly handles runtime unsupported features. /// /// This guarantees consistent behaviors across crate users /// and runtime kernels even if this crate get new features. /// It eases backward compatibility and enables future-proofness. /// /// Landlock is a security feature designed to help improve security of a running system /// thanks to application developers. /// To protect users as much as possible, /// compatibility with the running system should then be handled in a best-effort way, /// contrary to common system features. /// In some circumstances /// (e.g. applications carefully designed to only be run with a specific set of kernel features), /// it may be required to error out if some of these features are not available /// and will then not be enforced. pub trait Compatible: Sized + private::OptionCompatLevelMut { /// To enable a best-effort security approach, /// Landlock features that are not supported by the running system /// are silently ignored by default, /// which is a sane choice for most use cases. /// However, on some rare circumstances, /// developers may want to have some guarantees that their applications /// will not run if a certain level of sandboxing is not possible. /// If we really want to error out when not all our requested requirements are met, /// then we can configure it with `set_compatibility()`. /// /// The `Compatible` trait is implemented for all object builders /// (e.g. [`Ruleset`](crate::Ruleset)). /// Such builders have a set of methods to incrementally build an object. /// These build methods rely on kernel features that may not be available at runtime. /// The `set_compatibility()` method enables to control the effect of /// the following build method calls starting after the `set_compatibility()` call. /// Such effect can be: /// * to silently ignore unsupported features /// and continue building ([`CompatLevel::BestEffort`]); /// * to silently ignore unsupported features /// and ignore the whole build ([`CompatLevel::SoftRequirement`]); /// * to return an error for any unsupported feature ([`CompatLevel::HardRequirement`]). /// /// Taking [`Ruleset`](crate::Ruleset) as an example, /// the [`handle_access()`](crate::RulesetAttr::handle_access()) build method /// returns a [`Result`] that can be [`Err(RulesetError)`](crate::RulesetError) /// with a nested [`CompatError`]. /// Such error can only occur with a running Linux kernel not supporting the requested /// Landlock accesses *and* if the current compatibility level is /// [`CompatLevel::HardRequirement`]. /// However, such error is not possible with [`CompatLevel::BestEffort`] /// nor [`CompatLevel::SoftRequirement`]. /// /// The order of this call is important because /// it defines the behavior of the following build method calls that return a [`Result`]. /// If `set_compatibility(CompatLevel::HardRequirement)` is called on an object, /// then a [`CompatError`] may be returned for the next method calls, /// until the next call to `set_compatibility()`. /// This enables to change the behavior of a set of build method calls, /// for instance to be sure that the sandbox will at least restrict some access rights. /// /// New objects inherit the compatibility configuration of their parents, if any. /// For instance, [`Ruleset::create()`](crate::Ruleset::create()) returns /// a [`RulesetCreated`](crate::RulesetCreated) object that inherits the /// `Ruleset`'s compatibility configuration. /// /// # Example with `SoftRequirement` /// /// Let's say an application legitimately needs to rename files between directories. /// Because of [previous Landlock limitations](https://docs.kernel.org/userspace-api/landlock.html#file-renaming-and-linking-abi-2), /// this was forbidden with the [first version of Landlock](ABI::V1), /// but it is now handled starting with the [second version](ABI::V2). /// For this use case, we only want the application to be sandboxed /// if we have the guarantee that it will not break a legitimate usage (i.e. rename files). /// We then create a ruleset which will either support file renaming /// (thanks to [`AccessFs::Refer`](crate::AccessFs::Refer)) or silently do nothing. /// /// ``` /// use landlock::*; /// /// fn ruleset_handling_renames() -> Result { /// Ok(Ruleset::default() /// // This ruleset must either handle the AccessFs::Refer right, /// // or it must silently ignore the whole sandboxing. /// .set_compatibility(CompatLevel::SoftRequirement) /// .handle_access(AccessFs::Refer)? /// // However, this ruleset may also handle other (future) access rights /// // if they are supported by the running kernel. /// .set_compatibility(CompatLevel::BestEffort) /// .handle_access(AccessFs::from_all(ABI::V6))? /// .create()?) /// } /// ``` /// /// # Example with `HardRequirement` /// /// Security-dedicated applications may want to ensure that /// an untrusted software component is subject to a minimum of restrictions before launching it. /// In this case, we want to create a ruleset which will at least support /// all restrictions provided by the [first version of Landlock](ABI::V1), /// and opportunistically handle restrictions supported by newer kernels. /// /// ``` /// use landlock::*; /// /// fn ruleset_fragile() -> Result { /// Ok(Ruleset::default() /// // This ruleset must either handle at least all accesses defined by /// // the first Landlock version (e.g. AccessFs::WriteFile), /// // or the following handle_access() call must return a wrapped /// // AccessError::Incompatible error. /// .set_compatibility(CompatLevel::HardRequirement) /// .handle_access(AccessFs::from_all(ABI::V1))? /// // However, this ruleset may also handle new access rights /// // (e.g. AccessFs::Refer defined by the second version of Landlock) /// // if they are supported by the running kernel, /// // but without returning any error otherwise. /// .set_compatibility(CompatLevel::BestEffort) /// .handle_access(AccessFs::from_all(ABI::V6))? /// .create()?) /// } /// ``` fn set_compatibility(mut self, level: CompatLevel) -> Self { *self.as_option_compat_level_mut() = Some(level); self } /// Cf. [`set_compatibility()`](Compatible::set_compatibility()): /// /// - `set_best_effort(true)` translates to `set_compatibility(CompatLevel::BestEffort)`. /// /// - `set_best_effort(false)` translates to `set_compatibility(CompatLevel::HardRequirement)`. #[deprecated(note = "Use set_compatibility() instead")] fn set_best_effort(self, best_effort: bool) -> Self where Self: Sized, { self.set_compatibility(match best_effort { true => CompatLevel::BestEffort, false => CompatLevel::HardRequirement, }) } } #[test] #[allow(deprecated)] fn deprecated_set_best_effort() { use crate::{CompatLevel, Compatible, Ruleset}; assert_eq!( Ruleset::default().set_best_effort(true).compat, Ruleset::default() .set_compatibility(CompatLevel::BestEffort) .compat ); assert_eq!( Ruleset::default().set_best_effort(false).compat, Ruleset::default() .set_compatibility(CompatLevel::HardRequirement) .compat ); } /// See the [`Compatible`] documentation. #[cfg_attr(test, derive(EnumIter))] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum CompatLevel { /// Takes into account the build requests if they are supported by the running system, /// or silently ignores them otherwise. /// Never returns a compatibility error. #[default] BestEffort, /// Takes into account the build requests if they are supported by the running system, /// or silently ignores the whole build object otherwise. /// Never returns a compatibility error. /// If not supported, /// the call to [`RulesetCreated::restrict_self()`](crate::RulesetCreated::restrict_self()) /// will return a /// [`RestrictionStatus { ruleset: RulesetStatus::NotEnforced, no_new_privs: false, }`](crate::RestrictionStatus). SoftRequirement, /// Takes into account the build requests if they are supported by the running system, /// or returns a compatibility error otherwise ([`CompatError`]). HardRequirement, } impl From> for CompatLevel { fn from(opt: Option) -> Self { match opt { None => CompatLevel::default(), Some(ref level) => *level, } } } // TailoredCompatLevel could be replaced with AsMut>, but only traits defined // in the current crate can be implemented for types defined outside of the crate. Furthermore it // provides a default implementation which is handy for types such as BitFlags. pub trait TailoredCompatLevel { fn tailored_compat_level(&mut self, parent_level: L) -> CompatLevel where L: Into, { parent_level.into() } } impl TailoredCompatLevel for T where Self: Compatible, { // Every Compatible trait implementation returns its own compatibility level, if set. fn tailored_compat_level(&mut self, parent_level: L) -> CompatLevel where L: Into, { // Using a mutable reference is not required but it makes the code simpler (no double AsRef // implementations for each Compatible types), and more importantly it guarantees // consistency with Compatible::set_compatibility(). match self.as_option_compat_level_mut() { None => parent_level.into(), // Returns the most constrained compatibility level. Some(ref level) => parent_level.into().max(*level), } } } #[test] fn tailored_compat_level() { use crate::{AccessFs, PathBeneath, PathFd}; fn new_path(level: CompatLevel) -> PathBeneath { PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute).set_compatibility(level) } for parent_level in CompatLevel::iter() { assert_eq!( new_path(CompatLevel::BestEffort).tailored_compat_level(parent_level), parent_level ); assert_eq!( new_path(CompatLevel::HardRequirement).tailored_compat_level(parent_level), CompatLevel::HardRequirement ); } assert_eq!( new_path(CompatLevel::SoftRequirement).tailored_compat_level(CompatLevel::SoftRequirement), CompatLevel::SoftRequirement ); for child_level in CompatLevel::iter() { assert_eq!( new_path(child_level).tailored_compat_level(CompatLevel::BestEffort), child_level ); assert_eq!( new_path(child_level).tailored_compat_level(CompatLevel::HardRequirement), CompatLevel::HardRequirement ); } } // CompatResult is not public outside this crate. pub enum CompatResult where A: Access, { // Fully matches the request. Full, // Partially matches the request. Partial(CompatError), // Doesn't matches the request. No(CompatError), } // TryCompat is not public outside this crate. pub trait TryCompat where Self: Sized + TailoredCompatLevel, A: Access, { fn try_compat_inner(&mut self, abi: ABI) -> Result, CompatError>; // Default implementation for objects without children. // // If returning something other than Ok(Some(self)), the implementation must use its own // compatibility level, if any, with self.tailored_compat_level(default_compat_level), and pass // it with the abi and compat_state to each child.try_compat(). See PathBeneath implementation // and the self.allowed_access.try_compat() call. // // # Warning // // Errors must be prioritized over incompatibility (i.e. return Err(e) over Ok(None)) for all // children. fn try_compat_children( self, _abi: ABI, _parent_level: L, _compat_state: &mut CompatState, ) -> Result, CompatError> where L: Into, { Ok(Some(self)) } // Update compat_state and return an error according to try_compat_*() error, or to the // compatibility level, i.e. either route compatible object or error. fn try_compat( mut self, abi: ABI, parent_level: L, compat_state: &mut CompatState, ) -> Result, CompatError> where L: Into, { let compat_level = self.tailored_compat_level(parent_level); let some_inner = match self.try_compat_inner(abi) { Ok(CompatResult::Full) => { compat_state.update(CompatState::Full); true } Ok(CompatResult::Partial(error)) => match compat_level { CompatLevel::BestEffort => { compat_state.update(CompatState::Partial); true } CompatLevel::SoftRequirement => { compat_state.update(CompatState::Dummy); false } CompatLevel::HardRequirement => { compat_state.update(CompatState::Dummy); return Err(error); } }, Ok(CompatResult::No(error)) => match compat_level { CompatLevel::BestEffort => { compat_state.update(CompatState::No); false } CompatLevel::SoftRequirement => { compat_state.update(CompatState::Dummy); false } CompatLevel::HardRequirement => { compat_state.update(CompatState::Dummy); return Err(error); } }, Err(error) => { // Safeguard to help for test consistency. compat_state.update(CompatState::Dummy); return Err(error); } }; // At this point, any inner error have been returned, so we can proceed with // try_compat_children()?. match self.try_compat_children(abi, compat_level, compat_state)? { Some(n) if some_inner => Ok(Some(n)), _ => Ok(None), } } } landlock-0.4.4/src/errors.rs000064400000000000000000000230421046102023000140720ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT use crate::{Access, AccessFs, AccessNet, BitFlags, HandledAccess, PrivateHandledAccess, Scope}; use libc::c_int; use std::io; use std::path::PathBuf; use thiserror::Error; /// Maps to all errors that can be returned by a ruleset action. #[derive(Debug, Error)] #[non_exhaustive] pub enum RulesetError { #[error(transparent)] HandleAccesses(#[from] HandleAccessesError), #[error(transparent)] CreateRuleset(#[from] CreateRulesetError), #[error(transparent)] AddRules(#[from] AddRulesError), #[error(transparent)] RestrictSelf(#[from] RestrictSelfError), #[error(transparent)] Scope(#[from] ScopeError), } #[test] fn ruleset_error_breaking_change() { use crate::*; // Generics are part of the API and modifying them can lead to a breaking change. let _: RulesetError = RulesetError::HandleAccesses(HandleAccessesError::Fs( HandleAccessError::Compat(CompatError::Access(AccessError::Empty)), )); } /// Identifies errors when updating the ruleset's handled access-rights. #[derive(Debug, Error)] #[non_exhaustive] pub enum HandleAccessError where T: HandledAccess, { #[error(transparent)] Compat(#[from] CompatError), } /// Identifies errors when updating the ruleset's scopes. #[derive(Debug, Error)] #[non_exhaustive] pub enum ScopeError { #[error(transparent)] Compat(#[from] CompatError), } #[derive(Debug, Error)] #[non_exhaustive] pub enum HandleAccessesError { #[error(transparent)] Fs(HandleAccessError), #[error(transparent)] Net(HandleAccessError), } // Generically implement for all the handled access implementations rather than for the cases // listed in HandleAccessesError (with #[from]). impl From> for HandleAccessesError where A: PrivateHandledAccess, { fn from(error: HandleAccessError) -> Self { A::into_handle_accesses_error(error) } } /// Identifies errors when creating a ruleset. #[derive(Debug, Error)] #[non_exhaustive] pub enum CreateRulesetError { /// The `landlock_create_ruleset()` system call failed. #[error("failed to create a ruleset: {source}")] #[non_exhaustive] CreateRulesetCall { source: io::Error }, /// Missing call to [`RulesetAttr::handle_access()`](crate::RulesetAttr::handle_access) /// or [`RulesetAttr::scope()`](crate::RulesetAttr::scope). #[error("missing access")] MissingHandledAccess, } /// Identifies errors when adding a rule to a ruleset. #[derive(Debug, Error)] #[non_exhaustive] pub enum AddRuleError where T: HandledAccess, { /// The `landlock_add_rule()` system call failed. #[error("failed to add a rule: {source}")] #[non_exhaustive] AddRuleCall { source: io::Error }, /// The rule's access-rights are not all handled by the (requested) ruleset access-rights. #[error("access-rights not handled by the ruleset: {incompatible:?}")] UnhandledAccess { access: BitFlags, incompatible: BitFlags, }, #[error(transparent)] Compat(#[from] CompatError), } // Generically implement for all the handled access implementations rather than for the cases listed // in AddRulesError (with #[from]). impl From> for AddRulesError where A: PrivateHandledAccess, { fn from(error: AddRuleError) -> Self { A::into_add_rules_error(error) } } /// Identifies errors when adding rules to a ruleset thanks to an iterator returning /// Result items. #[derive(Debug, Error)] #[non_exhaustive] pub enum AddRulesError { #[error(transparent)] Fs(AddRuleError), #[error(transparent)] Net(AddRuleError), } #[derive(Debug, Error)] #[non_exhaustive] pub enum CompatError where T: Access, { #[error(transparent)] PathBeneath(#[from] PathBeneathError), #[error(transparent)] Access(#[from] AccessError), } #[derive(Debug, Error)] #[non_exhaustive] pub enum PathBeneathError { /// To check that access-rights are consistent with a file descriptor, a call to /// [`RulesetCreatedAttr::add_rule()`](crate::RulesetCreatedAttr::add_rule) /// looks at the file type with an `fstat()` system call. #[error("failed to check file descriptor type: {source}")] #[non_exhaustive] StatCall { source: io::Error }, /// This error is returned by /// [`RulesetCreatedAttr::add_rule()`](crate::RulesetCreatedAttr::add_rule) /// if the related PathBeneath object is not set to best-effort, /// and if its allowed access-rights contain directory-only ones /// whereas the file descriptor doesn't point to a directory. #[error("incompatible directory-only access-rights: {incompatible:?}")] DirectoryAccess { access: BitFlags, incompatible: BitFlags, }, } #[derive(Debug, Error)] // Exhaustive enum pub enum AccessError where T: Access, { /// The access-rights set is empty, which doesn't make sense and would be rejected by the /// kernel. #[error("empty access-right")] Empty, /// The access-rights set was forged with the unsafe `BitFlags::from_bits_unchecked()` and it /// contains unknown bits. #[error("unknown access-rights (at build time): {unknown:?}")] Unknown { access: BitFlags, unknown: BitFlags, }, /// The best-effort approach was (deliberately) disabled and the requested access-rights are /// fully incompatible with the running kernel. #[error("fully incompatible access-rights: {access:?}")] Incompatible { access: BitFlags }, /// The best-effort approach was (deliberately) disabled and the requested access-rights are /// partially incompatible with the running kernel. #[error("partially incompatible access-rights: {incompatible:?}")] PartiallyCompatible { access: BitFlags, incompatible: BitFlags, }, } #[derive(Debug, Error)] #[non_exhaustive] pub enum RestrictSelfError { /// The `prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)` system call failed. #[error("failed to set no_new_privs: {source}")] #[non_exhaustive] SetNoNewPrivsCall { source: io::Error }, /// The `landlock_restrict_self() `system call failed. #[error("failed to restrict the calling thread: {source}")] #[non_exhaustive] RestrictSelfCall { source: io::Error }, } #[derive(Debug, Error)] #[non_exhaustive] pub enum PathFdError { /// The `open()` system call failed. #[error("failed to open \"{path}\": {source}")] #[non_exhaustive] OpenCall { source: io::Error, path: PathBuf }, } #[cfg(test)] #[derive(Debug, Error)] pub(crate) enum TestRulesetError { #[error(transparent)] Ruleset(#[from] RulesetError), #[error(transparent)] PathFd(#[from] PathFdError), #[error(transparent)] File(#[from] std::io::Error), } /// Get the underlying errno value. /// /// This helper is useful for FFI to easily translate a Landlock error into an /// errno value. #[derive(Debug, PartialEq, Eq)] pub struct Errno(c_int); impl Errno { pub fn new(value: c_int) -> Self { Self(value) } } impl From for Errno where T: std::error::Error, { fn from(error: T) -> Self { let default = libc::EINVAL; if let Some(e) = error.source() { if let Some(e) = e.downcast_ref::() { return Errno(e.raw_os_error().unwrap_or(default)); } } Errno(default) } } impl std::ops::Deref for Errno { type Target = c_int; fn deref(&self) -> &Self::Target { &self.0 } } #[cfg(test)] fn _test_ruleset_errno(expected_errno: c_int) { use std::io::Error; let handle_access_err = RulesetError::HandleAccesses(HandleAccessesError::Fs( HandleAccessError::Compat(CompatError::Access(AccessError::Empty)), )); assert_eq!(*Errno::from(handle_access_err), libc::EINVAL); let create_ruleset_err = RulesetError::CreateRuleset(CreateRulesetError::CreateRulesetCall { source: Error::from_raw_os_error(expected_errno), }); assert_eq!(*Errno::from(create_ruleset_err), expected_errno); let add_rules_fs_err = RulesetError::AddRules(AddRulesError::Fs(AddRuleError::AddRuleCall { source: Error::from_raw_os_error(expected_errno), })); assert_eq!(*Errno::from(add_rules_fs_err), expected_errno); let add_rules_net_err = RulesetError::AddRules(AddRulesError::Net(AddRuleError::AddRuleCall { source: Error::from_raw_os_error(expected_errno), })); assert_eq!(*Errno::from(add_rules_net_err), expected_errno); let add_rules_other_err = RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access: AccessFs::Execute.into(), incompatible: BitFlags::::EMPTY, })); assert_eq!(*Errno::from(add_rules_other_err), libc::EINVAL); let restrict_self_err = RulesetError::RestrictSelf(RestrictSelfError::RestrictSelfCall { source: Error::from_raw_os_error(expected_errno), }); assert_eq!(*Errno::from(restrict_self_err), expected_errno); let set_no_new_privs_err = RulesetError::RestrictSelf(RestrictSelfError::SetNoNewPrivsCall { source: Error::from_raw_os_error(expected_errno), }); assert_eq!(*Errno::from(set_no_new_privs_err), expected_errno); let create_ruleset_missing_err = RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess); assert_eq!(*Errno::from(create_ruleset_missing_err), libc::EINVAL); } #[test] fn test_ruleset_errno() { _test_ruleset_errno(libc::EACCES); _test_ruleset_errno(libc::EIO); } landlock-0.4.4/src/fs.rs000064400000000000000000000532431046102023000131740ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT use crate::compat::private::OptionCompatLevelMut; use crate::{ uapi, Access, AddRuleError, AddRulesError, CompatError, CompatLevel, CompatResult, CompatState, Compatible, HandleAccessError, HandleAccessesError, HandledAccess, PathBeneathError, PathFdError, PrivateHandledAccess, PrivateRule, Rule, Ruleset, RulesetCreated, RulesetError, TailoredCompatLevel, TryCompat, ABI, }; use enumflags2::{bitflags, make_bitflags, BitFlags}; use std::fs::OpenOptions; use std::io::Error; use std::mem::zeroed; use std::os::unix::fs::OpenOptionsExt; use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; use std::path::Path; #[cfg(test)] use crate::{RulesetAttr, RulesetCreatedAttr}; #[cfg(test)] use strum::IntoEnumIterator; /// File system access right. /// /// Each variant of `AccessFs` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights) /// for the file system. /// A set of access rights can be created with [`BitFlags`](BitFlags). /// /// # Example /// /// ``` /// use landlock::{ABI, Access, AccessFs, BitFlags, make_bitflags}; /// /// let exec = AccessFs::Execute; /// /// let exec_set: BitFlags = exec.into(); /// /// let file_content = make_bitflags!(AccessFs::{Execute | WriteFile | ReadFile}); /// /// let fs_v1 = AccessFs::from_all(ABI::V1); /// /// let without_exec = fs_v1 & !AccessFs::Execute; /// /// assert_eq!(fs_v1 | AccessFs::Refer, AccessFs::from_all(ABI::V2)); /// ``` /// /// # Warning /// /// To avoid unknown restrictions **don't use `BitFlags::::all()` nor `BitFlags::ALL`**, /// but use a version you tested and vetted instead, /// for instance [`AccessFs::from_all(ABI::V1)`](Access::from_all). /// Direct use of **the [`BitFlags`] API is deprecated**. /// See [`ABI`] for the rationale and help to test it. #[bitflags] #[repr(u64)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum AccessFs { /// Execute a file. Execute = uapi::LANDLOCK_ACCESS_FS_EXECUTE as u64, /// Open a file with write access. /// /// # Note /// /// Certain operations (such as [`std::fs::write`]) may also require [`AccessFs::Truncate`] since [`ABI::V3`]. WriteFile = uapi::LANDLOCK_ACCESS_FS_WRITE_FILE as u64, /// Open a file with read access. ReadFile = uapi::LANDLOCK_ACCESS_FS_READ_FILE as u64, /// Open a directory or list its content. ReadDir = uapi::LANDLOCK_ACCESS_FS_READ_DIR as u64, /// Remove an empty directory or rename one. RemoveDir = uapi::LANDLOCK_ACCESS_FS_REMOVE_DIR as u64, /// Unlink (or rename) a file. RemoveFile = uapi::LANDLOCK_ACCESS_FS_REMOVE_FILE as u64, /// Create (or rename or link) a character device. MakeChar = uapi::LANDLOCK_ACCESS_FS_MAKE_CHAR as u64, /// Create (or rename) a directory. MakeDir = uapi::LANDLOCK_ACCESS_FS_MAKE_DIR as u64, /// Create (or rename or link) a regular file. MakeReg = uapi::LANDLOCK_ACCESS_FS_MAKE_REG as u64, /// Create (or rename or link) a UNIX domain socket. MakeSock = uapi::LANDLOCK_ACCESS_FS_MAKE_SOCK as u64, /// Create (or rename or link) a named pipe. MakeFifo = uapi::LANDLOCK_ACCESS_FS_MAKE_FIFO as u64, /// Create (or rename or link) a block device. MakeBlock = uapi::LANDLOCK_ACCESS_FS_MAKE_BLOCK as u64, /// Create (or rename or link) a symbolic link. MakeSym = uapi::LANDLOCK_ACCESS_FS_MAKE_SYM as u64, /// Link or rename a file from or to a different directory. Refer = uapi::LANDLOCK_ACCESS_FS_REFER as u64, /// Truncate a file with `truncate(2)`, `ftruncate(2)`, `creat(2)`, or `open(2)` with `O_TRUNC`. Truncate = uapi::LANDLOCK_ACCESS_FS_TRUNCATE as u64, /// Send IOCL commands to a device file. IoctlDev = uapi::LANDLOCK_ACCESS_FS_IOCTL_DEV as u64, } impl Access for AccessFs { /// Union of [`from_read()`](AccessFs::from_read) and [`from_write()`](AccessFs::from_write). fn from_all(abi: ABI) -> BitFlags { // An empty access-right would be an error if passed to the kernel, but because the kernel // doesn't support Landlock, no Landlock syscall should be called. try_compat() should // also return RestrictionStatus::Unrestricted when called with unsupported/empty // access-rights. Self::from_read(abi) | Self::from_write(abi) } } impl AccessFs { // Roughly read (i.e. not all FS actions are handled). /// Gets the access rights identified as read-only according to a specific ABI. /// Exclusive with [`from_write()`](AccessFs::from_write). pub fn from_read(abi: ABI) -> BitFlags { match abi { ABI::Unsupported => BitFlags::EMPTY, ABI::V1 | ABI::V2 | ABI::V3 | ABI::V4 | ABI::V5 | ABI::V6 => make_bitflags!(AccessFs::{ Execute | ReadFile | ReadDir }), } } // Roughly write (i.e. not all FS actions are handled). /// Gets the access rights identified as write-only according to a specific ABI. /// Exclusive with [`from_read()`](AccessFs::from_read). pub fn from_write(abi: ABI) -> BitFlags { match abi { ABI::Unsupported => BitFlags::EMPTY, ABI::V1 => make_bitflags!(AccessFs::{ WriteFile | RemoveDir | RemoveFile | MakeChar | MakeDir | MakeReg | MakeSock | MakeFifo | MakeBlock | MakeSym }), ABI::V2 => Self::from_write(ABI::V1) | AccessFs::Refer, ABI::V3 | ABI::V4 => Self::from_write(ABI::V2) | AccessFs::Truncate, ABI::V5 | ABI::V6 => Self::from_write(ABI::V4) | AccessFs::IoctlDev, } } /// Gets the access rights legitimate for non-directory files. pub fn from_file(abi: ABI) -> BitFlags { Self::from_all(abi) & ACCESS_FILE } } #[test] fn consistent_access_fs_rw() { for abi in ABI::iter() { let access_all = AccessFs::from_all(abi); let access_read = AccessFs::from_read(abi); let access_write = AccessFs::from_write(abi); let access_file = AccessFs::from_file(abi); assert_eq!(access_read, !access_write & access_all); assert_eq!(access_read | access_write, access_all); assert_eq!(access_file, access_all & ACCESS_FILE); } } impl HandledAccess for AccessFs {} impl PrivateHandledAccess for AccessFs { fn ruleset_handle_access( ruleset: &mut Ruleset, access: BitFlags, ) -> Result<(), HandleAccessesError> { // We need to record the requested accesses for PrivateRule::check_consistency(). ruleset.requested_handled_fs |= access; ruleset.actual_handled_fs |= match access .try_compat( ruleset.compat.abi(), ruleset.compat.level, &mut ruleset.compat.state, ) .map_err(HandleAccessError::Compat)? { Some(a) => a, None => return Ok(()), }; Ok(()) } fn into_add_rules_error(error: AddRuleError) -> AddRulesError { AddRulesError::Fs(error) } fn into_handle_accesses_error(error: HandleAccessError) -> HandleAccessesError { HandleAccessesError::Fs(error) } } // TODO: Make ACCESS_FILE a property of AccessFs. // TODO: Add tests for ACCESS_FILE. const ACCESS_FILE: BitFlags = make_bitflags!(AccessFs::{ ReadFile | WriteFile | Execute | Truncate | IoctlDev }); // XXX: What should we do when a stat call failed? fn is_file(fd: F) -> Result where F: AsFd, { unsafe { let mut stat = zeroed(); match libc::fstat(fd.as_fd().as_raw_fd(), &mut stat) { 0 => Ok((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR), _ => Err(Error::last_os_error()), } } } /// Landlock rule for a file hierarchy. /// /// # Example /// /// ``` /// use landlock::{AccessFs, PathBeneath, PathFd, PathFdError}; /// /// fn home_dir() -> Result, PathFdError> { /// Ok(PathBeneath::new(PathFd::new("/home")?, AccessFs::ReadDir)) /// } /// ``` #[derive(Debug)] pub struct PathBeneath { attr: uapi::landlock_path_beneath_attr, // Ties the lifetime of a file descriptor to this object. parent_fd: F, allowed_access: BitFlags, compat_level: Option, } impl PathBeneath where F: AsFd, { /// Creates a new `PathBeneath` rule identifying the `parent` directory of a file hierarchy, /// or just a file, and allows `access` on it. /// The `parent` file descriptor will be automatically closed with the returned `PathBeneath`. pub fn new(parent: F, access: A) -> Self where A: Into>, { PathBeneath { // Invalid access rights until as_ptr() is called. attr: unsafe { zeroed() }, parent_fd: parent, allowed_access: access.into(), compat_level: None, } } } impl TryCompat for PathBeneath where F: AsFd, { fn try_compat_children( mut self, abi: ABI, parent_level: L, compat_state: &mut CompatState, ) -> Result, CompatError> where L: Into, { // Checks with our own compatibility level, if any. self.allowed_access = match self.allowed_access.try_compat( abi, self.tailored_compat_level(parent_level), compat_state, )? { Some(a) => a, None => return Ok(None), }; Ok(Some(self)) } fn try_compat_inner( &mut self, _abi: ABI, ) -> Result, CompatError> { // Gets subset of valid accesses according the FD type. let valid_access = if is_file(&self.parent_fd).map_err(|e| PathBeneathError::StatCall { source: e })? { self.allowed_access & ACCESS_FILE } else { self.allowed_access }; if self.allowed_access != valid_access { let error = PathBeneathError::DirectoryAccess { access: self.allowed_access, incompatible: self.allowed_access ^ valid_access, } .into(); self.allowed_access = valid_access; // Linux would return EINVAL. Ok(CompatResult::Partial(error)) } else { Ok(CompatResult::Full) } } } #[test] fn path_beneath_try_compat_children() { use crate::*; // AccessFs::Refer is not handled by ABI::V1 and only for directories. let access_file = AccessFs::ReadFile | AccessFs::Refer; // Test error ordering with ABI::V1 let mut ruleset = Ruleset::from(ABI::V1).handle_access(access_file).unwrap(); // Do not actually perform any syscall. ruleset.compat.state = CompatState::Dummy; assert!(matches!( RulesetCreated::new(ruleset, None) .set_compatibility(CompatLevel::HardRequirement) .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file)) .unwrap_err(), RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat( CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible }) ))) if access == access_file && incompatible == AccessFs::Refer )); // Test error ordering with ABI::V2 let mut ruleset = Ruleset::from(ABI::V2).handle_access(access_file).unwrap(); // Do not actually perform any syscall. ruleset.compat.state = CompatState::Dummy; assert!(matches!( RulesetCreated::new(ruleset, None) .set_compatibility(CompatLevel::HardRequirement) .add_rule(PathBeneath::new(PathFd::new("/dev/null").unwrap(), access_file)) .unwrap_err(), RulesetError::AddRules(AddRulesError::Fs(AddRuleError::Compat( CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible }) ))) if access == access_file && incompatible == AccessFs::Refer )); } #[test] fn path_beneath_try_compat() { use crate::*; let abi = ABI::V1; for file in &["/etc/passwd", "/dev/null"] { let mut compat_state = CompatState::Init; let ro_access = AccessFs::ReadDir | AccessFs::ReadFile; assert!(matches!( PathBeneath::new(PathFd::new(file).unwrap(), ro_access) .try_compat(abi, CompatLevel::HardRequirement, &mut compat_state) .unwrap_err(), CompatError::PathBeneath(PathBeneathError::DirectoryAccess { access, incompatible }) if access == ro_access && incompatible == AccessFs::ReadDir )); let mut compat_state = CompatState::Init; assert!(matches!( PathBeneath::new(PathFd::new(file).unwrap(), BitFlags::EMPTY) .try_compat(abi, CompatLevel::BestEffort, &mut compat_state) .unwrap_err(), CompatError::Access(AccessError::Empty) )); } let full_access = AccessFs::from_all(ABI::V1); for compat_level in &[ CompatLevel::BestEffort, CompatLevel::SoftRequirement, CompatLevel::HardRequirement, ] { let mut compat_state = CompatState::Init; let mut path_beneath = PathBeneath::new(PathFd::new("/").unwrap(), full_access) .try_compat(abi, *compat_level, &mut compat_state) .unwrap() .unwrap(); assert_eq!(compat_state, CompatState::Full); // Without synchronization. let raw_access = path_beneath.attr.allowed_access; assert_eq!(raw_access, 0); // Synchronize the inner attribute buffer. let _ = path_beneath.as_ptr(); let raw_access = path_beneath.attr.allowed_access; assert_eq!(raw_access, full_access.bits()); } } impl OptionCompatLevelMut for PathBeneath { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat_level } } impl OptionCompatLevelMut for &mut PathBeneath { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat_level } } impl Compatible for PathBeneath {} impl Compatible for &mut PathBeneath {} #[test] fn path_beneath_compatibility() { let mut path = PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::from_all(ABI::V1)); let path_ref = &mut path; let level = path_ref.as_option_compat_level_mut(); assert_eq!(level, &None); assert_eq!( as Into>::into(*level), CompatLevel::BestEffort ); path_ref.set_compatibility(CompatLevel::SoftRequirement); assert_eq!( path_ref.as_option_compat_level_mut(), &Some(CompatLevel::SoftRequirement) ); path.set_compatibility(CompatLevel::HardRequirement); } // It is useful for documentation generation to explicitely implement Rule for every types, instead // of doing it generically. impl Rule for PathBeneath where F: AsFd {} impl PrivateRule for PathBeneath where F: AsFd, { const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH; fn as_ptr(&mut self) -> *const libc::c_void { self.attr.parent_fd = self.parent_fd.as_fd().as_raw_fd(); self.attr.allowed_access = self.allowed_access.bits(); &self.attr as *const _ as _ } fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> { // Checks that this rule doesn't contain a superset of the access-rights handled by the // ruleset. This check is about requested access-rights but not actual access-rights. // Indeed, we want to get a deterministic behavior, i.e. not based on the running kernel // (which is handled by Ruleset and RulesetCreated). if ruleset.requested_handled_fs.contains(self.allowed_access) { Ok(()) } else { Err(AddRuleError::UnhandledAccess { access: self.allowed_access, incompatible: self.allowed_access & !ruleset.requested_handled_fs, } .into()) } } } #[test] fn path_beneath_check_consistency() { use crate::*; let ro_access = AccessFs::ReadDir | AccessFs::ReadFile; let rx_access = AccessFs::Execute | AccessFs::ReadFile; assert!(matches!( Ruleset::from(ABI::Unsupported) .handle_access(ro_access) .unwrap() .create() .unwrap() .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), rx_access)) .unwrap_err(), RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { access, incompatible })) if access == rx_access && incompatible == AccessFs::Execute )); } /// Simple helper to open a file or a directory with the `O_PATH` flag. /// /// This is the recommended way to identify a path /// and manage the lifetime of the underlying opened file descriptor. /// Indeed, using other [`AsFd`] implementations such as [`File`] brings more complexity /// and may lead to unexpected errors (e.g., denied access). /// /// [`File`]: std::fs::File /// /// # Example /// /// ``` /// use landlock::{AccessFs, PathBeneath, PathFd, PathFdError}; /// /// fn allowed_root_dir(access: AccessFs) -> Result, PathFdError> { /// let fd = PathFd::new("/")?; /// Ok(PathBeneath::new(fd, access)) /// } /// ``` #[derive(Debug)] pub struct PathFd { fd: OwnedFd, } impl PathFd { pub fn new(path: T) -> Result where T: AsRef, { Ok(PathFd { fd: OpenOptions::new() .read(true) // If the O_PATH is not supported, it is automatically ignored (Linux < 2.6.39). .custom_flags(libc::O_PATH | libc::O_CLOEXEC) .open(path.as_ref()) .map_err(|e| PathFdError::OpenCall { source: e, path: path.as_ref().into(), })? .into(), }) } } impl AsFd for PathFd { fn as_fd(&self) -> BorrowedFd<'_> { self.fd.as_fd() } } #[test] fn path_fd() { use std::fs::File; use std::io::Read; PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute); PathBeneath::new(File::open("/").unwrap(), AccessFs::Execute); let mut buffer = [0; 1]; // Checks that PathFd really returns an FD opened with O_PATH (Bad file descriptor error). File::from(PathFd::new("/etc/passwd").unwrap().fd) .read(&mut buffer) .unwrap_err(); } /// Helper to quickly create an iterator of PathBeneath rules. /// /// # Note /// /// From the kernel's perspective, Landlock rules operate on file descriptors, not paths. /// This is a helper to create rules based on paths. Here, `path_beneath_rules()` silently ignores /// paths that cannot be opened, hence making the obtainment of a file descriptor impossible. When /// possible and for a given path, `path_beneath_rules()` automatically adjusts [access rights](`AccessFs`), /// depending on whether a directory or a file is present at that said path. /// /// This behavior is the result of [`CompatLevel::BestEffort`], which is the default compatibility level of /// all created rulesets. Thus, it applies to the example below. However, if [`CompatLevel::HardRequirement`] /// is set using [`Compatible::set_compatibility`], attempting to create an incompatible rule at runtime will cause /// this crate to raise an error instead. /// /// # Example /// /// ``` /// use landlock::{ /// ABI, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, RulesetError, /// path_beneath_rules, /// }; /// /// fn restrict_thread() -> Result<(), RulesetError> { /// let abi = ABI::V1; /// let status = Ruleset::default() /// .handle_access(AccessFs::from_all(abi))? /// .create()? /// // Read-only access to /usr, /etc and /dev. /// .add_rules(path_beneath_rules(&["/usr", "/etc", "/dev"], AccessFs::from_read(abi)))? /// // Read-write access to /home and /tmp. /// .add_rules(path_beneath_rules(&["/home", "/tmp"], AccessFs::from_all(abi)))? /// .restrict_self()?; /// match status.ruleset { /// // The FullyEnforced case must be tested by the developer. /// RulesetStatus::FullyEnforced => println!("Fully sandboxed."), /// RulesetStatus::PartiallyEnforced => println!("Partially sandboxed."), /// // Users should be warned that they are not protected. /// RulesetStatus::NotEnforced => println!("Not sandboxed! Please update your kernel."), /// } /// Ok(()) /// } /// ``` pub fn path_beneath_rules( paths: I, access: A, ) -> impl Iterator, RulesetError>> where I: IntoIterator, P: AsRef, A: Into>, { let access = access.into(); paths.into_iter().filter_map(move |p| match PathFd::new(p) { Ok(f) => { let valid_access = match is_file(&f) { Ok(true) => access & ACCESS_FILE, // If the stat call failed, let's blindly rely on the requested access rights. Err(_) | Ok(false) => access, }; Some(Ok(PathBeneath::new(f, valid_access))) } Err(_) => None, }) } #[test] fn path_beneath_rules_iter() { let _ = Ruleset::default() .handle_access(AccessFs::from_all(ABI::V1)) .unwrap() .create() .unwrap() .add_rules(path_beneath_rules( &["/usr", "/opt", "/does-not-exist", "/root"], AccessFs::Execute, )) .unwrap(); } landlock-0.4.4/src/lib.rs000064400000000000000000000424731046102023000133350ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT //! Landlock is a security feature available since Linux 5.13. //! The goal is to enable to restrict ambient rights //! (e.g., global filesystem access) //! for a set of processes by creating safe security sandboxes as new security layers //! in addition to the existing system-wide access-controls. //! This kind of sandbox is expected to help mitigate the security impact of bugs, //! unexpected or malicious behaviors in applications. //! Landlock empowers any process, including unprivileged ones, to securely restrict themselves. //! More information about Landlock can be found in the [official website](https://landlock.io). //! //! This crate provides a safe abstraction for the Landlock system calls, along with some helpers. //! //! Minimum Supported Rust Version (MSRV): 1.68 //! //! # Use cases //! //! This crate is especially useful to protect users' data by sandboxing: //! * trusted applications dealing with potentially malicious data //! (e.g., complex file format, network request) that could exploit security vulnerabilities; //! * sandbox managers, container runtimes or shells launching untrusted applications. //! //! # Examples //! //! A simple example can be found with the [`path_beneath_rules()`] helper. //! More complex examples can be found with the [`Ruleset` documentation](Ruleset) //! and the [sandboxer example](https://github.com/landlock-lsm/rust-landlock/blob/master/examples/sandboxer.rs). //! //! # Current limitations //! //! This crate exposes the Landlock features available as of Linux 5.19 //! and then inherits some [kernel limitations](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#current-limitations) //! that will be addressed with future kernel releases //! (e.g., arbitrary mounts are always denied). //! //! # Compatibility //! //! Types defined in this crate are designed to enable the strictest Landlock configuration //! for the given kernel on which the program runs. //! In the default [best-effort](CompatLevel::BestEffort) mode, //! [`Ruleset`] will determine compatibility //! with the intersection of the currently running kernel's features //! and those required by the caller. //! This way, callers can distinguish between //! Landlock compatibility issues inherent to the current system //! (e.g., file names that don't exist) //! and misconfiguration that should be fixed in the program //! (e.g., empty or inconsistent access rights). //! [`RulesetError`] identifies such kind of errors. //! //! With [`set_compatibility(CompatLevel::BestEffort)`](Compatible::set_compatibility), //! users of the crate may mark Landlock features that are deemed required //! and other features that may be downgraded to use lower security on systems //! where they can't be enforced. //! It is discouraged to compare the system's provided [Landlock ABI](ABI) version directly, //! as it is difficult to track detailed ABI differences //! which are handled thanks to the [`Compatible`] trait. //! //! To make it easier to migrate to a new version of this library, //! we use the builder pattern //! and designed objects to require the minimal set of method arguments. //! Most `enum` are marked as `non_exhaustive` to enable backward-compatible evolutions. //! //! ## Test strategy //! //! Developers should test their sandboxed applications //! with a kernel that supports all requested Landlock features //! and check that [`RulesetCreated::restrict_self()`] returns a status matching //! [`Ok(RestrictionStatus { ruleset: RulesetStatus::FullyEnforced, no_new_privs: true, })`](RestrictionStatus) //! to make sure everything works as expected in an enforced sandbox. //! Alternatively, using [`set_compatibility(CompatLevel::HardRequirement)`](Compatible::set_compatibility) //! will immediately inform about unsupported Landlock features. //! These configurations should only depend on the test environment //! (e.g. [by checking an environment variable](https://github.com/landlock-lsm/rust-landlock/search?q=LANDLOCK_CRATE_TEST_ABI)). //! However, applications should only check that no error is returned (i.e. `Ok(_)`) //! and optionally log and inform users that the application is not fully sandboxed //! because of missing features from the running kernel. #[cfg(test)] #[macro_use] extern crate lazy_static; pub use access::{Access, HandledAccess}; pub use compat::{CompatLevel, Compatible, LandlockStatus, ABI}; pub use enumflags2::{make_bitflags, BitFlags}; pub use errors::{ AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno, HandleAccessError, HandleAccessesError, PathBeneathError, PathFdError, RestrictSelfError, RulesetError, ScopeError, }; pub use fs::{path_beneath_rules, AccessFs, PathBeneath, PathFd}; pub use net::{AccessNet, NetPort}; pub use ruleset::{ RestrictionStatus, Rule, Ruleset, RulesetAttr, RulesetCreated, RulesetCreatedAttr, RulesetStatus, }; pub use scope::Scope; use access::PrivateHandledAccess; use compat::{CompatResult, CompatState, Compatibility, TailoredCompatLevel, TryCompat}; use ruleset::PrivateRule; #[cfg(test)] use compat::{can_emulate, get_errno_from_landlock_status}; #[cfg(test)] use errors::TestRulesetError; #[cfg(test)] use strum::IntoEnumIterator; mod access; mod compat; mod errors; mod fs; mod net; mod ruleset; mod scope; mod uapi; // Makes sure private traits cannot be implemented outside of this crate. mod private { pub trait Sealed {} impl Sealed for crate::AccessFs {} impl Sealed for crate::AccessNet {} impl Sealed for crate::Scope {} } #[cfg(test)] mod tests { use crate::*; // Emulate old kernel supports. fn check_ruleset_support( partial: ABI, full: Option, check: F, error_if_abi_lt_partial: bool, ) where F: Fn(Ruleset) -> Result + Send + Copy + 'static, { // If there is no partial support, it means that `full == partial`. assert!(partial <= full.unwrap_or(partial)); for abi in ABI::iter() { // Ensures restrict_self() is called on a dedicated thread to avoid inconsistent tests. let ret = std::thread::spawn(move || check(Ruleset::from(abi))) .join() .unwrap(); // Useful for failed tests and with cargo test -- --show-output println!("Checking ABI {abi:?}: received {ret:#?}"); if can_emulate(abi, partial, full) { if abi < partial && error_if_abi_lt_partial { // TODO: Check exact error type; this may require better error types. assert!(matches!(ret, Err(TestRulesetError::Ruleset(_)))); } else { let full_support = if let Some(full_inner) = full { abi >= full_inner } else { false }; let ruleset_status = if full_support { RulesetStatus::FullyEnforced } else if abi >= partial { RulesetStatus::PartiallyEnforced } else { RulesetStatus::NotEnforced }; let landlock_status = abi.into(); println!("Expecting ruleset status {ruleset_status:?}"); println!("Expecting Landlock status {landlock_status:?}"); assert!(matches!( ret, Ok(RestrictionStatus { ruleset, landlock, no_new_privs: true, }) if ruleset == ruleset_status && landlock == landlock_status )) } } else { // The errno value should be ENOSYS, EOPNOTSUPP, EINVAL (e.g. when an unknown // access right is provided), or E2BIG (e.g. when there is an unknown field in a // Landlock syscall attribute). let errno = get_errno_from_landlock_status(); println!("Expecting error {errno:?}"); match ret { Err( ref error @ TestRulesetError::Ruleset(RulesetError::CreateRuleset( CreateRulesetError::CreateRulesetCall { ref source }, )), ) => { assert_eq!(source.raw_os_error(), Some(*Errno::from(error))); match (source.raw_os_error(), errno) { (Some(e1), Some(e2)) => assert_eq!(e1, e2), (Some(e1), None) => assert!(matches!(e1, libc::EINVAL | libc::E2BIG)), _ => unreachable!(), } } _ => unreachable!(), } } } } #[test] fn allow_root_compat() { let abi = ABI::V1; check_ruleset_support( abi, Some(abi), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::from_all(abi))? .create()? .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))? .restrict_self()?) }, false, ); } #[test] fn too_much_access_rights_for_a_file() { let abi = ABI::V1; check_ruleset_support( abi, Some(abi), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::from_all(abi))? .create()? // Same code as allow_root_compat() but with /etc/passwd instead of / .add_rule(PathBeneath::new( PathFd::new("/etc/passwd")?, // Only allow legitimate access rights on a file. AccessFs::from_file(abi), ))? .restrict_self()?) }, false, ); check_ruleset_support( abi, None, move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::from_all(abi))? .create()? // Same code as allow_root_compat() but with /etc/passwd instead of / .add_rule(PathBeneath::new( PathFd::new("/etc/passwd")?, // Tries to allow all access rights on a file. AccessFs::from_all(abi), ))? .restrict_self()?) }, false, ); } #[test] fn path_beneath_rules_with_too_much_access_rights_for_a_file() { let abi = ABI::V1; check_ruleset_support( abi, Some(abi), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::from_all(ABI::V1))? .create()? // Same code as too_much_access_rights_for_a_file() but using path_beneath_rules() .add_rules(path_beneath_rules(["/etc/passwd"], AccessFs::from_all(abi)))? .restrict_self()?) }, false, ); } #[test] fn allow_root_fragile() { let abi = ABI::V1; check_ruleset_support( abi, Some(abi), move |ruleset: Ruleset| -> _ { // Sets default support requirement: abort the whole sandboxing for any Landlock error. Ok(ruleset // Must have at least the execute check… .set_compatibility(CompatLevel::HardRequirement) .handle_access(AccessFs::Execute)? // …and possibly others. .set_compatibility(CompatLevel::BestEffort) .handle_access(AccessFs::from_all(abi))? .create()? .set_no_new_privs(true) .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::from_all(abi)))? .restrict_self()?) }, true, ); } #[test] fn ruleset_enforced() { let abi = ABI::V1; check_ruleset_support( abi, Some(abi), move |ruleset: Ruleset| -> _ { Ok(ruleset // Restricting without rule exceptions is legitimate to forbid a set of actions. .handle_access(AccessFs::Execute)? .create()? .restrict_self()?) }, false, ); } #[test] fn abi_v2_exec_refer() { check_ruleset_support( ABI::V1, Some(ABI::V2), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::Execute)? // AccessFs::Refer is not supported by ABI::V1 (best-effort). .handle_access(AccessFs::Refer)? .create()? .restrict_self()?) }, false, ); } #[test] fn abi_v2_refer_only() { // When no access is handled, do not try to create a ruleset without access. check_ruleset_support( ABI::V2, Some(ABI::V2), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::Refer)? .create()? .restrict_self()?) }, false, ); } #[test] fn abi_v3_truncate() { check_ruleset_support( ABI::V2, Some(ABI::V3), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::Refer)? .handle_access(AccessFs::Truncate)? .create()? .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Refer))? .restrict_self()?) }, false, ); } #[test] fn ruleset_created_try_clone() { check_ruleset_support( ABI::V1, Some(ABI::V1), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::Execute)? .create()? .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::Execute))? .try_clone()? .restrict_self()?) }, false, ); } #[test] fn abi_v4_tcp() { check_ruleset_support( ABI::V3, Some(ABI::V4), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::Truncate)? .handle_access(AccessNet::BindTcp | AccessNet::ConnectTcp)? .create()? .add_rule(NetPort::new(1, AccessNet::ConnectTcp))? .restrict_self()?) }, false, ); } #[test] fn abi_v5_ioctl_dev() { check_ruleset_support( ABI::V4, Some(ABI::V5), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessNet::BindTcp)? .handle_access(AccessFs::IoctlDev)? .create()? .add_rule(PathBeneath::new(PathFd::new("/")?, AccessFs::IoctlDev))? .restrict_self()?) }, false, ); } #[test] fn abi_v6_scope_mix() { check_ruleset_support( ABI::V5, Some(ABI::V6), move |ruleset: Ruleset| -> _ { Ok(ruleset .handle_access(AccessFs::IoctlDev)? .scope(Scope::AbstractUnixSocket | Scope::Signal)? .create()? .restrict_self()?) }, false, ); } #[test] fn abi_v6_scope_only() { check_ruleset_support( ABI::V6, Some(ABI::V6), move |ruleset: Ruleset| -> _ { Ok(ruleset .scope(Scope::AbstractUnixSocket | Scope::Signal)? .create()? .restrict_self()?) }, false, ); } #[test] fn ruleset_created_try_clone_ownedfd() { use std::os::unix::io::{AsRawFd, OwnedFd}; let abi = ABI::V1; check_ruleset_support( abi, Some(abi), move |ruleset: Ruleset| -> _ { let ruleset1 = ruleset.handle_access(AccessFs::from_all(abi))?.create()?; let ruleset2 = ruleset1.try_clone().unwrap(); let ruleset3 = ruleset2.try_clone().unwrap(); let some1: Option = ruleset1.into(); if let Some(fd1) = some1 { assert!(fd1.as_raw_fd() >= 0); let some2: Option = ruleset2.into(); let fd2 = some2.unwrap(); assert!(fd2.as_raw_fd() >= 0); assert_ne!(fd1.as_raw_fd(), fd2.as_raw_fd()); } Ok(ruleset3.restrict_self()?) }, false, ); } } landlock-0.4.4/src/net.rs000064400000000000000000000161001046102023000133410ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT use crate::compat::private::OptionCompatLevelMut; use crate::{ uapi, Access, AddRuleError, AddRulesError, CompatError, CompatLevel, CompatResult, CompatState, Compatible, HandleAccessError, HandleAccessesError, HandledAccess, PrivateHandledAccess, PrivateRule, Rule, Ruleset, RulesetCreated, TailoredCompatLevel, TryCompat, ABI, }; use enumflags2::{bitflags, BitFlags}; use std::mem::zeroed; /// Network access right. /// /// Each variant of `AccessNet` is an [access right](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#access-rights) /// for the network. /// A set of access rights can be created with [`BitFlags`](BitFlags). /// /// # Example /// /// ``` /// use landlock::{ABI, Access, AccessNet, BitFlags, make_bitflags}; /// /// let bind = AccessNet::BindTcp; /// /// let bind_set: BitFlags = bind.into(); /// /// let bind_connect = make_bitflags!(AccessNet::{BindTcp | ConnectTcp}); /// /// let net_v4 = AccessNet::from_all(ABI::V4); /// /// assert_eq!(bind_connect, net_v4); /// ``` /// /// # Warning /// /// To avoid unknown restrictions **don't use `BitFlags::::all()` nor `BitFlags::ALL`**, /// but use a version you tested and vetted instead, /// for instance [`AccessNet::from_all(ABI::V4)`](Access::from_all). /// Direct use of **the [`BitFlags`] API is deprecated**. /// See [`ABI`] for the rationale and help to test it. #[bitflags] #[repr(u64)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum AccessNet { /// Bind to a TCP port. BindTcp = uapi::LANDLOCK_ACCESS_NET_BIND_TCP as u64, /// Connect to a TCP port. ConnectTcp = uapi::LANDLOCK_ACCESS_NET_CONNECT_TCP as u64, } /// # Warning /// /// If `ABI <= ABI::V3`, `AccessNet::from_all()` returns an empty `BitFlags`, which /// makes `Ruleset::handle_access(AccessNet::from_all(ABI::V3))` return an error. impl Access for AccessNet { fn from_all(abi: ABI) -> BitFlags { match abi { ABI::Unsupported | ABI::V1 | ABI::V2 | ABI::V3 => BitFlags::EMPTY, ABI::V4 | ABI::V5 | ABI::V6 => AccessNet::BindTcp | AccessNet::ConnectTcp, } } } impl HandledAccess for AccessNet {} impl PrivateHandledAccess for AccessNet { fn ruleset_handle_access( ruleset: &mut Ruleset, access: BitFlags, ) -> Result<(), HandleAccessesError> { // We need to record the requested accesses for PrivateRule::check_consistency(). ruleset.requested_handled_net |= access; ruleset.actual_handled_net |= match access .try_compat( ruleset.compat.abi(), ruleset.compat.level, &mut ruleset.compat.state, ) .map_err(HandleAccessError::Compat)? { Some(a) => a, None => return Ok(()), }; Ok(()) } fn into_add_rules_error(error: AddRuleError) -> AddRulesError { AddRulesError::Net(error) } fn into_handle_accesses_error(error: HandleAccessError) -> HandleAccessesError { HandleAccessesError::Net(error) } } /// Landlock rule for a network port. /// /// # Example /// /// ``` /// use landlock::{AccessNet, NetPort}; /// /// fn bind_http() -> NetPort { /// NetPort::new(80, AccessNet::BindTcp) /// } /// ``` #[derive(Debug)] pub struct NetPort { attr: uapi::landlock_net_port_attr, // Only 16-bit port make sense for now. port: u16, allowed_access: BitFlags, compat_level: Option, } // If we need support for 32 or 64 ports, we'll add a new_32() or a new_64() method returning a // Result with a potential overflow error. impl NetPort { /// Creates a new TCP port rule. /// /// As defined by the Linux ABI, `port` with a value of `0` means that TCP bindings will be /// allowed for a port range defined by `/proc/sys/net/ipv4/ip_local_port_range`. pub fn new(port: u16, access: A) -> Self where A: Into>, { NetPort { // Invalid access-rights until as_ptr() is called. attr: unsafe { zeroed() }, port, allowed_access: access.into(), compat_level: None, } } } impl Rule for NetPort {} impl PrivateRule for NetPort { const TYPE_ID: uapi::landlock_rule_type = uapi::landlock_rule_type_LANDLOCK_RULE_NET_PORT; fn as_ptr(&mut self) -> *const libc::c_void { self.attr.port = self.port as u64; self.attr.allowed_access = self.allowed_access.bits(); &self.attr as *const _ as _ } fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError> { // Checks that this rule doesn't contain a superset of the access-rights handled by the // ruleset. This check is about requested access-rights but not actual access-rights. // Indeed, we want to get a deterministic behavior, i.e. not based on the running kernel // (which is handled by Ruleset and RulesetCreated). if ruleset.requested_handled_net.contains(self.allowed_access) { Ok(()) } else { Err(AddRuleError::UnhandledAccess { access: self.allowed_access, incompatible: self.allowed_access & !ruleset.requested_handled_net, } .into()) } } } #[test] fn net_port_check_consistency() { use crate::*; let bind = AccessNet::BindTcp; let bind_connect = bind | AccessNet::ConnectTcp; assert!(matches!( Ruleset::from(ABI::Unsupported) .handle_access(bind) .unwrap() .create() .unwrap() .add_rule(NetPort::new(1, bind_connect)) .unwrap_err(), RulesetError::AddRules(AddRulesError::Net(AddRuleError::UnhandledAccess { access, incompatible })) if access == bind_connect && incompatible == AccessNet::ConnectTcp )); } impl TryCompat for NetPort { fn try_compat_children( mut self, abi: ABI, parent_level: L, compat_state: &mut CompatState, ) -> Result, CompatError> where L: Into, { // Checks with our own compatibility level, if any. self.allowed_access = match self.allowed_access.try_compat( abi, self.tailored_compat_level(parent_level), compat_state, )? { Some(a) => a, None => return Ok(None), }; Ok(Some(self)) } fn try_compat_inner( &mut self, _abi: ABI, ) -> Result, CompatError> { Ok(CompatResult::Full) } } impl OptionCompatLevelMut for NetPort { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat_level } } impl OptionCompatLevelMut for &mut NetPort { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat_level } } impl Compatible for NetPort {} impl Compatible for &mut NetPort {} landlock-0.4.4/src/ruleset.rs000064400000000000000000001261431046102023000142470ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT use crate::compat::private::OptionCompatLevelMut; use crate::{ uapi, AccessFs, AccessNet, AddRuleError, AddRulesError, BitFlags, CompatLevel, CompatState, Compatibility, Compatible, CreateRulesetError, HandledAccess, LandlockStatus, PrivateHandledAccess, RestrictSelfError, RulesetError, Scope, ScopeError, TryCompat, }; use std::io::Error; use std::mem::size_of_val; use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd}; #[cfg(test)] use crate::*; // Public interface without methods and which is impossible to implement outside this crate. pub trait Rule: PrivateRule where T: HandledAccess, { } // PrivateRule is not public outside this crate. pub trait PrivateRule where Self: TryCompat + Compatible, T: HandledAccess, { const TYPE_ID: uapi::landlock_rule_type; /// Returns a raw pointer to the rule's inner attribute. /// /// The caller must ensure that the rule outlives the pointer this function returns, or else it /// will end up pointing to garbage. fn as_ptr(&mut self) -> *const libc::c_void; fn check_consistency(&self, ruleset: &RulesetCreated) -> Result<(), AddRulesError>; } /// Enforcement status of a ruleset. #[derive(Debug, PartialEq, Eq)] pub enum RulesetStatus { /// All requested restrictions are enforced. FullyEnforced, /// Some requested restrictions are enforced, /// following a best-effort approach. PartiallyEnforced, /// The running system doesn't support Landlock /// or a subset of the requested Landlock features. NotEnforced, } impl From for RulesetStatus { fn from(state: CompatState) -> Self { match state { CompatState::Init | CompatState::No | CompatState::Dummy => RulesetStatus::NotEnforced, CompatState::Full => RulesetStatus::FullyEnforced, CompatState::Partial => RulesetStatus::PartiallyEnforced, } } } // The Debug, PartialEq and Eq implementations are useful for crate users to debug and check the // result of a Landlock ruleset enforcement. /// Status of a [`RulesetCreated`] /// after calling [`restrict_self()`](RulesetCreated::restrict_self). #[derive(Debug, PartialEq, Eq)] #[non_exhaustive] pub struct RestrictionStatus { /// Status of the Landlock ruleset enforcement. pub ruleset: RulesetStatus, /// Status of `prctl(2)`'s `PR_SET_NO_NEW_PRIVS` enforcement. pub no_new_privs: bool, /// Status of Landlock for the running kernel. pub landlock: LandlockStatus, } fn prctl_set_no_new_privs() -> Result<(), Error> { match unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) } { 0 => Ok(()), _ => Err(Error::last_os_error()), } } fn support_no_new_privs() -> bool { // Only Linux < 3.5 or kernel with seccomp filters should return an error. matches!( unsafe { libc::prctl(libc::PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) }, 0 | 1 ) } /// Landlock ruleset builder. /// /// `Ruleset` enables to create a Landlock ruleset in a flexible way /// following the builder pattern. /// Most build steps return a [`Result`] with [`RulesetError`]. /// /// You should probably not create more than one ruleset per application. /// Creating multiple rulesets is only useful when gradually restricting an application /// (e.g., a first set of generic restrictions before reading any file, /// then a second set of tailored restrictions after reading the configuration). /// /// # Simple example /// /// Simple helper handling only Landlock-related errors. /// /// ``` /// use landlock::{ /// Access, AccessFs, PathBeneath, PathFd, RestrictionStatus, Ruleset, RulesetAttr, /// RulesetCreatedAttr, RulesetError, ABI, /// }; /// use std::os::unix::io::AsFd; /// /// fn restrict_fd(hierarchy: T) -> Result /// where /// T: AsFd, /// { /// // The Landlock ABI should be incremented (and tested) regularly. /// let abi = ABI::V1; /// let access_all = AccessFs::from_all(abi); /// let access_read = AccessFs::from_read(abi); /// Ok(Ruleset::default() /// .handle_access(access_all)? /// .create()? /// .add_rule(PathBeneath::new(hierarchy, access_read))? /// .restrict_self()?) /// } /// /// let fd = PathFd::new("/home").expect("failed to open /home"); /// let status = restrict_fd(fd).expect("failed to build the ruleset"); /// ``` /// /// # Generic example /// /// More generic helper handling a set of file hierarchies /// and multiple types of error (i.e. [`RulesetError`](crate::RulesetError) /// and [`PathFdError`](crate::PathFdError). /// /// ``` /// use landlock::{ /// Access, AccessFs, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, /// RulesetAttr, RulesetCreatedAttr, RulesetError, ABI, /// }; /// use thiserror::Error; /// /// #[derive(Debug, Error)] /// enum MyRestrictError { /// #[error(transparent)] /// Ruleset(#[from] RulesetError), /// #[error(transparent)] /// AddRule(#[from] PathFdError), /// } /// /// fn restrict_paths(hierarchies: &[&str]) -> Result { /// // The Landlock ABI should be incremented (and tested) regularly. /// let abi = ABI::V1; /// let access_all = AccessFs::from_all(abi); /// let access_read = AccessFs::from_read(abi); /// Ok(Ruleset::default() /// .handle_access(access_all)? /// .create()? /// .add_rules( /// hierarchies /// .iter() /// .map::, _>(|p| { /// Ok(PathBeneath::new(PathFd::new(p)?, access_read)) /// }), /// )? /// .restrict_self()?) /// } /// /// let status = restrict_paths(&["/usr", "/home"]).expect("failed to build the ruleset"); /// ``` #[derive(Debug)] pub struct Ruleset { pub(crate) requested_handled_fs: BitFlags, pub(crate) requested_handled_net: BitFlags, pub(crate) requested_scoped: BitFlags, pub(crate) actual_handled_fs: BitFlags, pub(crate) actual_handled_net: BitFlags, pub(crate) actual_scoped: BitFlags, pub(crate) compat: Compatibility, } impl From for Ruleset { fn from(compat: Compatibility) -> Self { Ruleset { // Non-working default handled FS accesses to force users to set them explicitely. requested_handled_fs: Default::default(), requested_handled_net: Default::default(), requested_scoped: Default::default(), actual_handled_fs: Default::default(), actual_handled_net: Default::default(), actual_scoped: Default::default(), compat, } } } #[cfg(test)] impl From for Ruleset { fn from(abi: ABI) -> Self { Ruleset::from(Compatibility::from(abi)) } } #[test] fn ruleset_add_rule_iter() { assert!(matches!( Ruleset::from(ABI::Unsupported) .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap() .add_rule(PathBeneath::new( PathFd::new("/").unwrap(), AccessFs::ReadFile )) .unwrap_err(), RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { .. })) )); } impl Default for Ruleset { /// Returns a new `Ruleset`. /// This call automatically probes the running kernel to know if it supports Landlock. /// /// To be able to successfully call [`create()`](Ruleset::create), /// it is required to set the handled accesses with /// [`handle_access()`](Ruleset::handle_access). fn default() -> Self { // The API should be future-proof: one Rust program or library should have the same // behavior if built with an old or a newer crate (e.g. with an extended ruleset_attr // enum). It should then not be possible to give an "all-possible-handled-accesses" to the // Ruleset builder because this value would be relative to the running kernel. Compatibility::new().into() } } impl Ruleset { #[allow(clippy::new_without_default)] #[deprecated(note = "Use Ruleset::default() instead")] pub fn new() -> Self { Ruleset::default() } /// Attempts to create a real Landlock ruleset (if supported by the running kernel). /// The returned [`RulesetCreated`] is also a builder. /// /// On error, returns a wrapped [`CreateRulesetError`]. pub fn create(mut self) -> Result { let body = || -> Result { match self.compat.state { CompatState::Init => { // Checks that there is at least one requested access (e.g. // requested_handled_fs): one call to handle_access(). Err(CreateRulesetError::MissingHandledAccess) } CompatState::No | CompatState::Dummy => { // There is at least one requested access. #[cfg(test)] assert!( !self.requested_handled_fs.is_empty() || !self.requested_handled_net.is_empty() || !self.requested_scoped.is_empty() ); // CompatState::No should be handled as CompatState::Dummy because it is not // possible to create an actual ruleset. self.compat.update(CompatState::Dummy); match self.compat.level.into() { CompatLevel::HardRequirement => { Err(CreateRulesetError::MissingHandledAccess) } _ => Ok(RulesetCreated::new(self, None)), } } CompatState::Full | CompatState::Partial => { // There is at least one actual handled access. #[cfg(test)] assert!( !self.actual_handled_fs.is_empty() || !self.actual_handled_net.is_empty() || !self.actual_scoped.is_empty() ); let attr = uapi::landlock_ruleset_attr { handled_access_fs: self.actual_handled_fs.bits(), handled_access_net: self.actual_handled_net.bits(), scoped: self.actual_scoped.bits(), }; match unsafe { uapi::landlock_create_ruleset(&attr, size_of_val(&attr), 0) } { fd if fd >= 0 => Ok(RulesetCreated::new( self, Some(unsafe { OwnedFd::from_raw_fd(fd) }), )), _ => Err(CreateRulesetError::CreateRulesetCall { source: Error::last_os_error(), }), } } } }; Ok(body()?) } } impl OptionCompatLevelMut for Ruleset { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat.level } } impl OptionCompatLevelMut for &mut Ruleset { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat.level } } impl Compatible for Ruleset {} impl Compatible for &mut Ruleset {} impl AsMut for Ruleset { fn as_mut(&mut self) -> &mut Ruleset { self } } // Tests unambiguous type. #[test] fn ruleset_as_mut() { let mut ruleset = Ruleset::from(ABI::Unsupported); let _ = ruleset.as_mut(); let mut ruleset_created = Ruleset::from(ABI::Unsupported) .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap(); let _ = ruleset_created.as_mut(); } pub trait RulesetAttr: Sized + AsMut + Compatible { /// Attempts to add a set of access rights that will be supported by this ruleset. /// By default, all actions requiring these access rights will be denied. /// Consecutive calls to `handle_access()` will be interpreted as logical ORs /// with the previous handled accesses. /// /// On error, returns a wrapped [`HandleAccessesError`](crate::HandleAccessesError). /// E.g., `RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError))` fn handle_access(mut self, access: T) -> Result where T: Into>, U: HandledAccess + PrivateHandledAccess, { U::ruleset_handle_access(self.as_mut(), access.into())?; Ok(self) } /// Attempts to add a set of scopes that will be supported by this ruleset. /// Consecutive calls to `scope()` will be interpreted as logical ORs /// with the previous scopes. /// /// On error, returns a wrapped [`ScopeError`](crate::ScopeError). /// E.g., `RulesetError::Scope(ScopeError)` fn scope(mut self, scope: T) -> Result where T: Into>, { let scope = scope.into(); let ruleset = self.as_mut(); ruleset.requested_scoped |= scope; if let Some(a) = scope .try_compat( ruleset.compat.abi(), ruleset.compat.level, &mut ruleset.compat.state, ) .map_err(ScopeError::Compat)? { ruleset.actual_scoped |= a; } Ok(self) } } impl RulesetAttr for Ruleset {} impl RulesetAttr for &mut Ruleset {} #[test] fn ruleset_attr() { let mut ruleset = Ruleset::from(ABI::Unsupported); let ruleset_ref = &mut ruleset; // Can pass this reference to prepare the ruleset... ruleset_ref .set_compatibility(CompatLevel::BestEffort) .handle_access(AccessFs::Execute) .unwrap() .handle_access(AccessFs::ReadFile) .unwrap(); // ...and finally create the ruleset (thanks to non-lexical lifetimes). ruleset .set_compatibility(CompatLevel::BestEffort) .handle_access(AccessFs::Execute) .unwrap() .handle_access(AccessFs::WriteFile) .unwrap() .create() .unwrap(); } #[test] fn ruleset_created_handle_access_fs() { let access = make_bitflags!(AccessFs::{Execute | ReadDir}); // Tests AccessFs::ruleset_handle_access() let ruleset = Ruleset::from(ABI::V1).handle_access(access).unwrap(); assert_eq!(ruleset.requested_handled_fs, access); assert_eq!(ruleset.actual_handled_fs, access); // Tests composition (binary OR) of handled accesses. let ruleset = Ruleset::from(ABI::V1) .handle_access(AccessFs::Execute) .unwrap() .handle_access(AccessFs::ReadDir) .unwrap() .handle_access(AccessFs::Execute) .unwrap(); assert_eq!(ruleset.requested_handled_fs, access); assert_eq!(ruleset.actual_handled_fs, access); // Tests that only the required handled accesses are reported as incompatible: // access should not contains AccessFs::Execute. assert!(matches!(Ruleset::from(ABI::Unsupported) .handle_access(AccessFs::Execute) .unwrap() .set_compatibility(CompatLevel::HardRequirement) .handle_access(AccessFs::ReadDir) .unwrap_err(), RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat( CompatError::Access(AccessError::Incompatible { access }) ))) if access == AccessFs::ReadDir )); } #[test] fn ruleset_created_handle_access_net_tcp() { let access = make_bitflags!(AccessNet::{BindTcp | ConnectTcp}); // Tests AccessNet::ruleset_handle_access() with ABI that doesn't support TCP rights. let ruleset = Ruleset::from(ABI::V3).handle_access(access).unwrap(); assert_eq!(ruleset.requested_handled_net, access); assert_eq!(ruleset.actual_handled_net, BitFlags::::EMPTY); // Tests AccessNet::ruleset_handle_access() with ABI that supports TCP rights. let ruleset = Ruleset::from(ABI::V4).handle_access(access).unwrap(); assert_eq!(ruleset.requested_handled_net, access); assert_eq!(ruleset.actual_handled_net, access); // Tests composition (binary OR) of handled accesses. let ruleset = Ruleset::from(ABI::V4) .handle_access(AccessNet::BindTcp) .unwrap() .handle_access(AccessNet::ConnectTcp) .unwrap() .handle_access(AccessNet::BindTcp) .unwrap(); assert_eq!(ruleset.requested_handled_net, access); assert_eq!(ruleset.actual_handled_net, access); // Tests that only the required handled accesses are reported as incompatible: // access should not contains AccessNet::BindTcp. assert!(matches!(Ruleset::from(ABI::Unsupported) .handle_access(AccessNet::BindTcp) .unwrap() .set_compatibility(CompatLevel::HardRequirement) .handle_access(AccessNet::ConnectTcp) .unwrap_err(), RulesetError::HandleAccesses(HandleAccessesError::Net(HandleAccessError::Compat( CompatError::Access(AccessError::Incompatible { access }) ))) if access == AccessNet::ConnectTcp )); } #[test] fn ruleset_created_scope() { let scopes = make_bitflags!(Scope::{AbstractUnixSocket | Signal}); // Tests Ruleset::scope() with ABI that doesn't support scopes. let ruleset = Ruleset::from(ABI::V5).scope(scopes).unwrap(); assert_eq!(ruleset.requested_scoped, scopes); assert_eq!(ruleset.actual_scoped, BitFlags::::EMPTY); // Tests Ruleset::scope() with ABI that supports scopes. let ruleset = Ruleset::from(ABI::V6).scope(scopes).unwrap(); assert_eq!(ruleset.requested_scoped, scopes); assert_eq!(ruleset.actual_scoped, scopes); // Tests composition (binary OR) of scopes. let ruleset = Ruleset::from(ABI::V6) .scope(Scope::AbstractUnixSocket) .unwrap() .scope(Scope::Signal) .unwrap() .scope(Scope::AbstractUnixSocket) .unwrap(); assert_eq!(ruleset.requested_scoped, scopes); assert_eq!(ruleset.actual_scoped, scopes); // Tests that only the required scopes are reported as incompatible: // scope should not contain Scope::AbstractUnixSocket. assert!(matches!(Ruleset::from(ABI::Unsupported) .scope(Scope::AbstractUnixSocket) .unwrap() .set_compatibility(CompatLevel::HardRequirement) .scope(Scope::Signal) .unwrap_err(), RulesetError::Scope(ScopeError::Compat( CompatError::Access(AccessError::Incompatible { access }) )) if access == Scope::Signal )); } #[test] fn ruleset_created_fs_net_scope() { let access_fs = make_bitflags!(AccessFs::{Execute | ReadDir}); let access_net = make_bitflags!(AccessNet::{BindTcp | ConnectTcp}); let scopes = make_bitflags!(Scope::{AbstractUnixSocket | Signal}); // Tests composition (binary OR) of handled accesses. let ruleset = Ruleset::from(ABI::V5) .handle_access(access_fs) .unwrap() .scope(scopes) .unwrap() .handle_access(access_net) .unwrap(); assert_eq!(ruleset.requested_handled_fs, access_fs); assert_eq!(ruleset.actual_handled_fs, access_fs); assert_eq!(ruleset.requested_handled_net, access_net); assert_eq!(ruleset.actual_handled_net, access_net); assert_eq!(ruleset.requested_scoped, scopes); assert_eq!(ruleset.actual_scoped, BitFlags::::EMPTY); // Tests composition (binary OR) of handled accesses and scopes. let ruleset = Ruleset::from(ABI::V6) .handle_access(access_fs) .unwrap() .scope(scopes) .unwrap() .handle_access(access_net) .unwrap(); assert_eq!(ruleset.requested_handled_fs, access_fs); assert_eq!(ruleset.actual_handled_fs, access_fs); assert_eq!(ruleset.requested_handled_net, access_net); assert_eq!(ruleset.actual_handled_net, access_net); assert_eq!(ruleset.requested_scoped, scopes); assert_eq!(ruleset.actual_scoped, scopes); } impl OptionCompatLevelMut for RulesetCreated { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat.level } } impl OptionCompatLevelMut for &mut RulesetCreated { fn as_option_compat_level_mut(&mut self) -> &mut Option { &mut self.compat.level } } impl Compatible for RulesetCreated {} impl Compatible for &mut RulesetCreated {} pub trait RulesetCreatedAttr: Sized + AsMut + Compatible { /// Attempts to add a new rule to the ruleset. /// /// On error, returns a wrapped [`AddRulesError`]. fn add_rule(mut self, rule: T) -> Result where T: Rule, U: HandledAccess + PrivateHandledAccess, { let body = || -> Result { let self_ref = self.as_mut(); rule.check_consistency(self_ref)?; let mut compat_rule = match rule .try_compat( self_ref.compat.abi(), self_ref.compat.level, &mut self_ref.compat.state, ) .map_err(AddRuleError::Compat)? { Some(r) => r, None => return Ok(self), }; match self_ref.compat.state { CompatState::Init | CompatState::No | CompatState::Dummy => Ok(self), CompatState::Full | CompatState::Partial => { #[cfg(test)] assert!(self_ref.fd.is_some()); let fd = self_ref.fd.as_ref().map(|f| f.as_raw_fd()).unwrap_or(-1); match unsafe { uapi::landlock_add_rule(fd, T::TYPE_ID, compat_rule.as_ptr(), 0) } { 0 => Ok(self), _ => Err(AddRuleError::::AddRuleCall { source: Error::last_os_error(), } .into()), } } } }; Ok(body()?) } /// Attempts to add a set of new rules to the ruleset. /// /// On error, returns a (double) wrapped [`AddRulesError`]. /// /// # Example /// /// Create a custom iterator to read paths from environment variable. /// /// ``` /// use landlock::{ /// Access, AccessFs, BitFlags, PathBeneath, PathFd, PathFdError, RestrictionStatus, Ruleset, /// RulesetAttr, RulesetCreatedAttr, RulesetError, ABI, /// }; /// use std::env; /// use std::ffi::OsStr; /// use std::os::unix::ffi::{OsStrExt, OsStringExt}; /// use thiserror::Error; /// /// #[derive(Debug, Error)] /// enum PathEnvError<'a> { /// #[error(transparent)] /// Ruleset(#[from] RulesetError), /// #[error(transparent)] /// AddRuleIter(#[from] PathFdError), /// #[error("missing environment variable {0}")] /// MissingVar(&'a str), /// } /// /// struct PathEnv { /// paths: Vec, /// access: BitFlags, /// } /// /// impl PathEnv { /// // env_var is the name of an environment variable /// // containing paths requested to be allowed. /// // Paths are separated with ":", e.g. "/bin:/lib:/usr:/proc". /// // In case an empty string is provided, /// // no restrictions are applied. /// // `access` is the set of access rights allowed for each of the parsed paths. /// fn new<'a>( /// env_var: &'a str, access: BitFlags /// ) -> Result> { /// Ok(Self { /// paths: env::var_os(env_var) /// .ok_or(PathEnvError::MissingVar(env_var))? /// .into_vec(), /// access, /// }) /// } /// /// fn iter( /// &self, /// ) -> impl Iterator, PathEnvError<'static>>> + '_ { /// let is_empty = self.paths.is_empty(); /// self.paths /// .split(|b| *b == b':') /// // Skips the first empty element from of an empty string. /// .skip_while(move |_| is_empty) /// .map(OsStr::from_bytes) /// .map(move |path| /// Ok(PathBeneath::new(PathFd::new(path)?, self.access))) /// } /// } /// /// fn restrict_env() -> Result> { /// Ok(Ruleset::default() /// .handle_access(AccessFs::from_all(ABI::V1))? /// .create()? /// // In the shell: export EXECUTABLE_PATH="/usr:/bin:/sbin" /// .add_rules(PathEnv::new("EXECUTABLE_PATH", AccessFs::Execute.into())?.iter())? /// .restrict_self()?) /// } /// ``` fn add_rules(mut self, rules: I) -> Result where I: IntoIterator>, T: Rule, U: HandledAccess + PrivateHandledAccess, E: From, { for rule in rules { self = self.add_rule(rule?)?; } Ok(self) } /// Configures the ruleset to call `prctl(2)` with the `PR_SET_NO_NEW_PRIVS` command /// in [`restrict_self()`](RulesetCreated::restrict_self). /// /// This `prctl(2)` call is never ignored, even if an error was encountered on a [`Ruleset`] or /// [`RulesetCreated`] method call while [`CompatLevel::SoftRequirement`] was set. fn set_no_new_privs(mut self, no_new_privs: bool) -> Self { >::as_mut(&mut self).no_new_privs = no_new_privs; self } } /// Ruleset created with [`Ruleset::create()`]. #[derive(Debug)] pub struct RulesetCreated { fd: Option, no_new_privs: bool, pub(crate) requested_handled_fs: BitFlags, pub(crate) requested_handled_net: BitFlags, compat: Compatibility, } impl RulesetCreated { pub(crate) fn new(ruleset: Ruleset, fd: Option) -> Self { // The compatibility state is initialized by Ruleset::create(). #[cfg(test)] assert!(!matches!(ruleset.compat.state, CompatState::Init)); RulesetCreated { fd, no_new_privs: true, requested_handled_fs: ruleset.requested_handled_fs, requested_handled_net: ruleset.requested_handled_net, compat: ruleset.compat, } } /// Attempts to restrict the calling thread with the ruleset /// according to the best-effort configuration /// (see [`RulesetCreated::set_compatibility()`] and [`CompatLevel::BestEffort`]). /// Call `prctl(2)` with the `PR_SET_NO_NEW_PRIVS` /// according to the ruleset configuration. /// /// On error, returns a wrapped [`RestrictSelfError`]. pub fn restrict_self(mut self) -> Result { let mut body = || -> Result { // Enforce no_new_privs even if something failed with SoftRequirement. The rationale is // that no_new_privs should not be an issue on its own if it is not explicitly // deactivated. let enforced_nnp = if self.no_new_privs { if let Err(e) = prctl_set_no_new_privs() { match self.compat.level.into() { CompatLevel::BestEffort => {} CompatLevel::SoftRequirement => { self.compat.update(CompatState::Dummy); } CompatLevel::HardRequirement => { return Err(RestrictSelfError::SetNoNewPrivsCall { source: e }); } } // To get a consistent behavior, calls this prctl whether or not // Landlock is supported by the running kernel. let support_nnp = support_no_new_privs(); match self.compat.state { // It should not be an error for kernel (older than 3.5) not supporting // no_new_privs. CompatState::Init | CompatState::No | CompatState::Dummy => { if support_nnp { // The kernel seems to be between 3.5 (included) and 5.13 (excluded), // or Landlock is not enabled; no_new_privs should be supported anyway. return Err(RestrictSelfError::SetNoNewPrivsCall { source: e }); } } // A kernel supporting Landlock should also support no_new_privs (unless // filtered by seccomp). CompatState::Full | CompatState::Partial => { return Err(RestrictSelfError::SetNoNewPrivsCall { source: e }) } } false } else { true } } else { false }; match self.compat.state { CompatState::Init | CompatState::No | CompatState::Dummy => Ok(RestrictionStatus { ruleset: self.compat.state.into(), landlock: self.compat.status(), no_new_privs: enforced_nnp, }), CompatState::Full | CompatState::Partial => { #[cfg(test)] assert!(self.fd.is_some()); // Does not consume ruleset FD, which will be automatically closed after this block. let fd = self.fd.as_ref().map(|f| f.as_raw_fd()).unwrap_or(-1); match unsafe { uapi::landlock_restrict_self(fd, 0) } { 0 => { self.compat.update(CompatState::Full); Ok(RestrictionStatus { ruleset: self.compat.state.into(), landlock: self.compat.status(), no_new_privs: enforced_nnp, }) } // TODO: match specific Landlock restrict self errors _ => Err(RestrictSelfError::RestrictSelfCall { source: Error::last_os_error(), }), } } } }; Ok(body()?) } /// Creates a new `RulesetCreated` instance by duplicating the underlying file descriptor. /// Rule modification will affect both `RulesetCreated` instances simultaneously. /// /// On error, returns [`std::io::Error`]. pub fn try_clone(&self) -> std::io::Result { Ok(RulesetCreated { fd: self.fd.as_ref().map(|f| f.try_clone()).transpose()?, no_new_privs: self.no_new_privs, requested_handled_fs: self.requested_handled_fs, requested_handled_net: self.requested_handled_net, compat: self.compat, }) } } impl From for Option { fn from(ruleset: RulesetCreated) -> Self { ruleset.fd } } #[test] fn ruleset_created_ownedfd_none() { let ruleset = Ruleset::from(ABI::Unsupported) .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap(); let fd: Option = ruleset.into(); assert!(fd.is_none()); } impl AsMut for RulesetCreated { fn as_mut(&mut self) -> &mut RulesetCreated { self } } impl RulesetCreatedAttr for RulesetCreated {} impl RulesetCreatedAttr for &mut RulesetCreated {} #[test] fn ruleset_created_attr() { let mut ruleset_created = Ruleset::from(ABI::Unsupported) .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap(); let ruleset_created_ref = &mut ruleset_created; // Can pass this reference to populate the ruleset... ruleset_created_ref .set_compatibility(CompatLevel::BestEffort) .add_rule(PathBeneath::new( PathFd::new("/usr").unwrap(), AccessFs::Execute, )) .unwrap() .add_rule(PathBeneath::new( PathFd::new("/etc").unwrap(), AccessFs::Execute, )) .unwrap(); // ...and finally restrict with the last rules (thanks to non-lexical lifetimes). assert_eq!( ruleset_created .set_compatibility(CompatLevel::BestEffort) .add_rule(PathBeneath::new( PathFd::new("/tmp").unwrap(), AccessFs::Execute, )) .unwrap() .add_rule(PathBeneath::new( PathFd::new("/var").unwrap(), AccessFs::Execute, )) .unwrap() .restrict_self() .unwrap(), RestrictionStatus { ruleset: RulesetStatus::NotEnforced, landlock: LandlockStatus::NotImplemented, no_new_privs: true, } ); } #[test] fn ruleset_compat_dummy() { for level in [CompatLevel::BestEffort, CompatLevel::SoftRequirement] { println!("level: {:?}", level); // ABI:Unsupported does not support AccessFs::Execute. let ruleset = Ruleset::from(ABI::Unsupported); assert_eq!(ruleset.compat.state, CompatState::Init); let ruleset = ruleset.set_compatibility(level); assert_eq!(ruleset.compat.state, CompatState::Init); let ruleset = ruleset.handle_access(AccessFs::Execute).unwrap(); assert_eq!( ruleset.compat.state, match level { CompatLevel::BestEffort => CompatState::No, CompatLevel::SoftRequirement => CompatState::Dummy, _ => unreachable!(), } ); let ruleset_created = ruleset.create().unwrap(); // Because the compatibility state was either No or Dummy, calling create() updates it to // Dummy. assert_eq!(ruleset_created.compat.state, CompatState::Dummy); let ruleset_created = ruleset_created .add_rule(PathBeneath::new( PathFd::new("/usr").unwrap(), AccessFs::Execute, )) .unwrap(); assert_eq!(ruleset_created.compat.state, CompatState::Dummy); } } #[test] fn ruleset_compat_partial() { // CompatLevel::BestEffort let ruleset = Ruleset::from(ABI::V1); assert_eq!(ruleset.compat.state, CompatState::Init); // ABI::V1 does not support AccessFs::Refer. let ruleset = ruleset.handle_access(AccessFs::Refer).unwrap(); assert_eq!(ruleset.compat.state, CompatState::No); let ruleset = ruleset.handle_access(AccessFs::Execute).unwrap(); assert_eq!(ruleset.compat.state, CompatState::Partial); // Requesting to handle another unsupported handled access does not change anything. let ruleset = ruleset.handle_access(AccessFs::Refer).unwrap(); assert_eq!(ruleset.compat.state, CompatState::Partial); } #[test] fn ruleset_unsupported() { assert_eq!( Ruleset::from(ABI::Unsupported) // BestEffort for Ruleset. .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap() .restrict_self() .unwrap(), RestrictionStatus { ruleset: RulesetStatus::NotEnforced, landlock: LandlockStatus::NotImplemented, // With BestEffort, no_new_privs is still enabled. no_new_privs: true, } ); assert_eq!( Ruleset::from(ABI::Unsupported) // SoftRequirement for Ruleset. .set_compatibility(CompatLevel::SoftRequirement) .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap() .restrict_self() .unwrap(), RestrictionStatus { ruleset: RulesetStatus::NotEnforced, landlock: LandlockStatus::NotImplemented, // With SoftRequirement, no_new_privs is still enabled. no_new_privs: true, } ); // Missing handled access because of the compatibility level. matches!( Ruleset::from(ABI::Unsupported) // HardRequirement for Ruleset. .set_compatibility(CompatLevel::HardRequirement) .handle_access(AccessFs::Execute) .unwrap_err(), RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess) ); // Missing scope access because of the compatibility level. matches!( Ruleset::from(ABI::Unsupported) // HardRequirement for Ruleset. .set_compatibility(CompatLevel::HardRequirement) .scope(Scope::Signal) .unwrap_err(), RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess) ); assert_eq!( Ruleset::from(ABI::Unsupported) .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap() // SoftRequirement for RulesetCreated without any rule. .set_compatibility(CompatLevel::SoftRequirement) .restrict_self() .unwrap(), RestrictionStatus { ruleset: RulesetStatus::NotEnforced, landlock: LandlockStatus::NotImplemented, // With SoftRequirement, no_new_privs is untouched if there is no error (e.g. no rule). no_new_privs: true, } ); // Don't explicitly call create() on a CI that doesn't support Landlock. if compat::can_emulate(ABI::V1, ABI::V1, Some(ABI::V2)) { assert_eq!( Ruleset::from(ABI::V1) .handle_access(make_bitflags!(AccessFs::{Execute | Refer})) .unwrap() .create() .unwrap() // SoftRequirement for RulesetCreated with a rule. .set_compatibility(CompatLevel::SoftRequirement) .add_rule(PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Refer)) .unwrap() .restrict_self() .unwrap(), RestrictionStatus { ruleset: RulesetStatus::NotEnforced, landlock: LandlockStatus::Available { effective_abi: ABI::V1, kernel_abi: None, }, // With SoftRequirement, no_new_privs is still enabled, even if there is an error // (e.g. unsupported access right). no_new_privs: true, } ); } assert_eq!( Ruleset::from(ABI::Unsupported) .handle_access(AccessFs::Execute) .unwrap() .create() .unwrap() .set_no_new_privs(false) .restrict_self() .unwrap(), RestrictionStatus { ruleset: RulesetStatus::NotEnforced, landlock: LandlockStatus::NotImplemented, no_new_privs: false, } ); // Checks empty handled access with moot ruleset. assert!(matches!( Ruleset::from(ABI::Unsupported) // Empty access-rights .handle_access(AccessFs::from_all(ABI::Unsupported)) .unwrap_err(), RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat( CompatError::Access(AccessError::Empty) ))) )); assert!(matches!( Ruleset::from(ABI::Unsupported) // No handle_access() nor scope() call. .create() .unwrap_err(), RulesetError::CreateRuleset(CreateRulesetError::MissingHandledAccess) )); // Checks empty handled access with minimal ruleset. assert!(matches!( Ruleset::from(ABI::V1) // Empty access-rights .handle_access(AccessFs::from_all(ABI::Unsupported)) .unwrap_err(), RulesetError::HandleAccesses(HandleAccessesError::Fs(HandleAccessError::Compat( CompatError::Access(AccessError::Empty) ))) )); // Checks empty scope with moot ruleset. assert!(matches!( Ruleset::from(ABI::Unsupported) .scope(Scope::from_all(ABI::Unsupported)) .unwrap_err(), RulesetError::Scope(ScopeError::Compat(CompatError::Access(AccessError::Empty))) )); // Checks empty scope with minimal ruleset. assert!(matches!( Ruleset::from(ABI::V1) .scope(Scope::from_all(ABI::Unsupported)) .unwrap_err(), RulesetError::Scope(ScopeError::Compat(CompatError::Access(AccessError::Empty))) )); // Tests inconsistency between the ruleset handled access-rights and the rule access-rights. for handled_access in &[ make_bitflags!(AccessFs::{Execute | WriteFile}), AccessFs::Execute.into(), ] { let ruleset = Ruleset::from(ABI::V1) .handle_access(*handled_access) .unwrap(); // Fakes a call to create() to test without involving the kernel (i.e. no // landlock_ruleset_create() call). let ruleset_created = RulesetCreated::new(ruleset, None); assert!(matches!( ruleset_created .add_rule(PathBeneath::new( PathFd::new("/").unwrap(), AccessFs::ReadFile )) .unwrap_err(), RulesetError::AddRules(AddRulesError::Fs(AddRuleError::UnhandledAccess { .. })) )); } } #[test] fn ignore_abi_v2_with_abi_v1() { // We don't need kernel/CI support for Landlock because no related syscalls should actually be // performed. assert_eq!( Ruleset::from(ABI::V1) .set_compatibility(CompatLevel::HardRequirement) .handle_access(AccessFs::from_all(ABI::V1)) .unwrap() .set_compatibility(CompatLevel::SoftRequirement) // Because Ruleset only supports V1, Refer will be ignored. .handle_access(AccessFs::Refer) .unwrap() .create() .unwrap() .add_rule(PathBeneath::new( PathFd::new("/tmp").unwrap(), AccessFs::from_all(ABI::V2) )) .unwrap() .add_rule(PathBeneath::new( PathFd::new("/usr").unwrap(), make_bitflags!(AccessFs::{ReadFile | ReadDir}) )) .unwrap() .restrict_self() .unwrap(), RestrictionStatus { ruleset: RulesetStatus::NotEnforced, landlock: LandlockStatus::Available { effective_abi: ABI::V1, kernel_abi: None, }, no_new_privs: true, } ); } #[test] fn unsupported_handled_access() { matches!( Ruleset::from(ABI::V3) .handle_access(AccessNet::from_all(ABI::V3)) .unwrap_err(), RulesetError::HandleAccesses(HandleAccessesError::Net(HandleAccessError::Compat( CompatError::Access(AccessError::Empty) ))) ); } #[test] fn unsupported_handled_access_errno() { assert_eq!( Errno::from( Ruleset::from(ABI::V3) .handle_access(AccessNet::from_all(ABI::V3)) .unwrap_err() ), Errno::new(libc::EINVAL) ); } landlock-0.4.4/src/scope.rs000064400000000000000000000035731046102023000136760ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT use crate::{uapi, Access, ABI}; use enumflags2::{bitflags, BitFlags}; /// Scope right. /// /// Each variant of `Scope` is a /// [scope flag](https://www.kernel.org/doc/html/latest/userspace-api/landlock.html#scope-flags). /// A set of scopes can be created with [`BitFlags`](BitFlags). /// /// # Example /// /// ``` /// use landlock::{ABI, Access, Scope, BitFlags, make_bitflags}; /// /// let signal = Scope::Signal; /// /// let signal_set: BitFlags = signal.into(); /// /// let signal_uds = make_bitflags!(Scope::{Signal | AbstractUnixSocket}); /// /// let scope_v6 = Scope::from_all(ABI::V6); /// /// assert_eq!(signal_uds, scope_v6); /// ``` /// /// # Warning /// /// To avoid unknown restrictions **don't use `BitFlags::::all()` nor `BitFlags::ALL`**, /// but use a version you tested and vetted instead, /// for instance [`Scope::from_all(ABI::V6)`](Access::from_all). /// Direct use of **the [`BitFlags`] API is deprecated**. /// See [`ABI`] for the rationale and help to test it. #[bitflags] #[repr(u64)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Scope { /// Restrict from connecting to abstract UNIX sockets created outside the sandbox. AbstractUnixSocket = uapi::LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET as u64, /// Restrict from sending signals to processes outside the sandbox. Signal = uapi::LANDLOCK_SCOPE_SIGNAL as u64, } /// # Warning /// /// If `ABI <= ABI::V5`, `Scope::from_all()` returns an empty `BitFlags`, which /// makes `Ruleset::handle_access(AccessScope::from_all(ABI::V5))` return an error. impl Access for Scope { fn from_all(abi: ABI) -> BitFlags { match abi { ABI::Unsupported | ABI::V1 | ABI::V2 | ABI::V3 | ABI::V4 | ABI::V5 => BitFlags::EMPTY, ABI::V6 => Scope::AbstractUnixSocket | Scope::Signal, } } } landlock-0.4.4/src/uapi/bindgen.sh000075500000000000000000000021011046102023000151040ustar 00000000000000#!/usr/bin/env bash # SPDX-License-Identifier: Apache-2.0 OR MIT set -u -e -o pipefail if [[ $# -ne 1 ]]; then echo "usage $(basename -- "${BASH_SOURCE[0]}") " >&2 exit 1 fi HEADER="$(readlink -f -- "$1")/include/uapi/linux/landlock.h" if [[ ! -f "${HEADER}" ]]; then echo "File not found: ${HEADER}" >&2 exit 1 fi cd "$(dirname "${BASH_SOURCE[0]}")" MSRV="$(sed -n 's/^rust-version = "\(.*\)"/\1/p' ../../Cargo.toml)" bindgen_landlock() { local arch="$1" local output="$2" shift 2 bindgen \ "$@" \ --rust-target "${MSRV}" \ --allowlist-type "landlock_.*" \ --allowlist-var "LANDLOCK_.*" \ --no-doc-comments \ --no-derive-default \ --output "${output}" \ "${HEADER}" \ -- \ --target="${arch}-linux-gnu" } for ARCH in x86_64 i686; do echo "Generating bindings with tests for ${ARCH}." bindgen_landlock "${ARCH}" "landlock_${ARCH}.rs" done # The Landlock ABI is architecture-agnostic (except for std::os::raw and memory # alignment). echo "Generating bindings without tests." bindgen_landlock x86_64 "landlock_all.rs" --no-layout-tests landlock-0.4.4/src/uapi/landlock_all.rs000064400000000000000000000035031046102023000161330ustar 00000000000000/* automatically generated by rust-bindgen 0.72.0 */ pub const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1; pub const LANDLOCK_ACCESS_FS_EXECUTE: u32 = 1; pub const LANDLOCK_ACCESS_FS_WRITE_FILE: u32 = 2; pub const LANDLOCK_ACCESS_FS_READ_FILE: u32 = 4; pub const LANDLOCK_ACCESS_FS_READ_DIR: u32 = 8; pub const LANDLOCK_ACCESS_FS_REMOVE_DIR: u32 = 16; pub const LANDLOCK_ACCESS_FS_REMOVE_FILE: u32 = 32; pub const LANDLOCK_ACCESS_FS_MAKE_CHAR: u32 = 64; pub const LANDLOCK_ACCESS_FS_MAKE_DIR: u32 = 128; pub const LANDLOCK_ACCESS_FS_MAKE_REG: u32 = 256; pub const LANDLOCK_ACCESS_FS_MAKE_SOCK: u32 = 512; pub const LANDLOCK_ACCESS_FS_MAKE_FIFO: u32 = 1024; pub const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u32 = 2048; pub const LANDLOCK_ACCESS_FS_MAKE_SYM: u32 = 4096; pub const LANDLOCK_ACCESS_FS_REFER: u32 = 8192; pub const LANDLOCK_ACCESS_FS_TRUNCATE: u32 = 16384; pub const LANDLOCK_ACCESS_FS_IOCTL_DEV: u32 = 32768; pub const LANDLOCK_ACCESS_NET_BIND_TCP: u32 = 1; pub const LANDLOCK_ACCESS_NET_CONNECT_TCP: u32 = 2; pub const LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: u32 = 1; pub const LANDLOCK_SCOPE_SIGNAL: u32 = 2; pub type __s32 = ::std::os::raw::c_int; pub type __u64 = ::std::os::raw::c_ulonglong; #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct landlock_ruleset_attr { pub handled_access_fs: __u64, pub handled_access_net: __u64, pub scoped: __u64, } pub const landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH: landlock_rule_type = 1; pub const landlock_rule_type_LANDLOCK_RULE_NET_PORT: landlock_rule_type = 2; pub type landlock_rule_type = ::std::os::raw::c_uint; #[repr(C, packed)] #[derive(Debug, Copy, Clone)] pub struct landlock_path_beneath_attr { pub allowed_access: __u64, pub parent_fd: __s32, } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct landlock_net_port_attr { pub allowed_access: __u64, pub port: __u64, } landlock-0.4.4/src/uapi/landlock_i686.rs000064400000000000000000000110531046102023000160560ustar 00000000000000/* automatically generated by rust-bindgen 0.72.0 */ pub const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1; pub const LANDLOCK_ACCESS_FS_EXECUTE: u32 = 1; pub const LANDLOCK_ACCESS_FS_WRITE_FILE: u32 = 2; pub const LANDLOCK_ACCESS_FS_READ_FILE: u32 = 4; pub const LANDLOCK_ACCESS_FS_READ_DIR: u32 = 8; pub const LANDLOCK_ACCESS_FS_REMOVE_DIR: u32 = 16; pub const LANDLOCK_ACCESS_FS_REMOVE_FILE: u32 = 32; pub const LANDLOCK_ACCESS_FS_MAKE_CHAR: u32 = 64; pub const LANDLOCK_ACCESS_FS_MAKE_DIR: u32 = 128; pub const LANDLOCK_ACCESS_FS_MAKE_REG: u32 = 256; pub const LANDLOCK_ACCESS_FS_MAKE_SOCK: u32 = 512; pub const LANDLOCK_ACCESS_FS_MAKE_FIFO: u32 = 1024; pub const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u32 = 2048; pub const LANDLOCK_ACCESS_FS_MAKE_SYM: u32 = 4096; pub const LANDLOCK_ACCESS_FS_REFER: u32 = 8192; pub const LANDLOCK_ACCESS_FS_TRUNCATE: u32 = 16384; pub const LANDLOCK_ACCESS_FS_IOCTL_DEV: u32 = 32768; pub const LANDLOCK_ACCESS_NET_BIND_TCP: u32 = 1; pub const LANDLOCK_ACCESS_NET_CONNECT_TCP: u32 = 2; pub const LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: u32 = 1; pub const LANDLOCK_SCOPE_SIGNAL: u32 = 2; pub type __s32 = ::std::os::raw::c_int; pub type __u64 = ::std::os::raw::c_ulonglong; #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct landlock_ruleset_attr { pub handled_access_fs: __u64, pub handled_access_net: __u64, pub scoped: __u64, } #[test] fn bindgen_test_layout_landlock_ruleset_attr() { const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::(), 24usize, "Size of landlock_ruleset_attr" ); assert_eq!( ::std::mem::align_of::(), 4usize, "Alignment of landlock_ruleset_attr" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).handled_access_fs) as usize - ptr as usize }, 0usize, "Offset of field: landlock_ruleset_attr::handled_access_fs" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).handled_access_net) as usize - ptr as usize }, 8usize, "Offset of field: landlock_ruleset_attr::handled_access_net" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).scoped) as usize - ptr as usize }, 16usize, "Offset of field: landlock_ruleset_attr::scoped" ); } pub const landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH: landlock_rule_type = 1; pub const landlock_rule_type_LANDLOCK_RULE_NET_PORT: landlock_rule_type = 2; pub type landlock_rule_type = ::std::os::raw::c_uint; #[repr(C, packed)] #[derive(Debug, Copy, Clone)] pub struct landlock_path_beneath_attr { pub allowed_access: __u64, pub parent_fd: __s32, } #[test] fn bindgen_test_layout_landlock_path_beneath_attr() { const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::(), 12usize, "Size of landlock_path_beneath_attr" ); assert_eq!( ::std::mem::align_of::(), 1usize, "Alignment of landlock_path_beneath_attr" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).allowed_access) as usize - ptr as usize }, 0usize, "Offset of field: landlock_path_beneath_attr::allowed_access" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).parent_fd) as usize - ptr as usize }, 8usize, "Offset of field: landlock_path_beneath_attr::parent_fd" ); } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct landlock_net_port_attr { pub allowed_access: __u64, pub port: __u64, } #[test] fn bindgen_test_layout_landlock_net_port_attr() { const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::(), 16usize, "Size of landlock_net_port_attr" ); assert_eq!( ::std::mem::align_of::(), 4usize, "Alignment of landlock_net_port_attr" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).allowed_access) as usize - ptr as usize }, 0usize, "Offset of field: landlock_net_port_attr::allowed_access" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).port) as usize - ptr as usize }, 8usize, "Offset of field: landlock_net_port_attr::port" ); } landlock-0.4.4/src/uapi/landlock_x86_64.rs000064400000000000000000000110531046102023000163200ustar 00000000000000/* automatically generated by rust-bindgen 0.72.0 */ pub const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1; pub const LANDLOCK_ACCESS_FS_EXECUTE: u32 = 1; pub const LANDLOCK_ACCESS_FS_WRITE_FILE: u32 = 2; pub const LANDLOCK_ACCESS_FS_READ_FILE: u32 = 4; pub const LANDLOCK_ACCESS_FS_READ_DIR: u32 = 8; pub const LANDLOCK_ACCESS_FS_REMOVE_DIR: u32 = 16; pub const LANDLOCK_ACCESS_FS_REMOVE_FILE: u32 = 32; pub const LANDLOCK_ACCESS_FS_MAKE_CHAR: u32 = 64; pub const LANDLOCK_ACCESS_FS_MAKE_DIR: u32 = 128; pub const LANDLOCK_ACCESS_FS_MAKE_REG: u32 = 256; pub const LANDLOCK_ACCESS_FS_MAKE_SOCK: u32 = 512; pub const LANDLOCK_ACCESS_FS_MAKE_FIFO: u32 = 1024; pub const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u32 = 2048; pub const LANDLOCK_ACCESS_FS_MAKE_SYM: u32 = 4096; pub const LANDLOCK_ACCESS_FS_REFER: u32 = 8192; pub const LANDLOCK_ACCESS_FS_TRUNCATE: u32 = 16384; pub const LANDLOCK_ACCESS_FS_IOCTL_DEV: u32 = 32768; pub const LANDLOCK_ACCESS_NET_BIND_TCP: u32 = 1; pub const LANDLOCK_ACCESS_NET_CONNECT_TCP: u32 = 2; pub const LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET: u32 = 1; pub const LANDLOCK_SCOPE_SIGNAL: u32 = 2; pub type __s32 = ::std::os::raw::c_int; pub type __u64 = ::std::os::raw::c_ulonglong; #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct landlock_ruleset_attr { pub handled_access_fs: __u64, pub handled_access_net: __u64, pub scoped: __u64, } #[test] fn bindgen_test_layout_landlock_ruleset_attr() { const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::(), 24usize, "Size of landlock_ruleset_attr" ); assert_eq!( ::std::mem::align_of::(), 8usize, "Alignment of landlock_ruleset_attr" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).handled_access_fs) as usize - ptr as usize }, 0usize, "Offset of field: landlock_ruleset_attr::handled_access_fs" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).handled_access_net) as usize - ptr as usize }, 8usize, "Offset of field: landlock_ruleset_attr::handled_access_net" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).scoped) as usize - ptr as usize }, 16usize, "Offset of field: landlock_ruleset_attr::scoped" ); } pub const landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH: landlock_rule_type = 1; pub const landlock_rule_type_LANDLOCK_RULE_NET_PORT: landlock_rule_type = 2; pub type landlock_rule_type = ::std::os::raw::c_uint; #[repr(C, packed)] #[derive(Debug, Copy, Clone)] pub struct landlock_path_beneath_attr { pub allowed_access: __u64, pub parent_fd: __s32, } #[test] fn bindgen_test_layout_landlock_path_beneath_attr() { const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::(), 12usize, "Size of landlock_path_beneath_attr" ); assert_eq!( ::std::mem::align_of::(), 1usize, "Alignment of landlock_path_beneath_attr" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).allowed_access) as usize - ptr as usize }, 0usize, "Offset of field: landlock_path_beneath_attr::allowed_access" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).parent_fd) as usize - ptr as usize }, 8usize, "Offset of field: landlock_path_beneath_attr::parent_fd" ); } #[repr(C)] #[derive(Debug, Copy, Clone)] pub struct landlock_net_port_attr { pub allowed_access: __u64, pub port: __u64, } #[test] fn bindgen_test_layout_landlock_net_port_attr() { const UNINIT: ::std::mem::MaybeUninit = ::std::mem::MaybeUninit::uninit(); let ptr = UNINIT.as_ptr(); assert_eq!( ::std::mem::size_of::(), 16usize, "Size of landlock_net_port_attr" ); assert_eq!( ::std::mem::align_of::(), 8usize, "Alignment of landlock_net_port_attr" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).allowed_access) as usize - ptr as usize }, 0usize, "Offset of field: landlock_net_port_attr::allowed_access" ); assert_eq!( unsafe { ::std::ptr::addr_of!((*ptr).port) as usize - ptr as usize }, 8usize, "Offset of field: landlock_net_port_attr::port" ); } landlock-0.4.4/src/uapi/mod.rs000064400000000000000000000051431046102023000142750ustar 00000000000000// SPDX-License-Identifier: Apache-2.0 OR MIT // Use architecture-specific bindings for native x86_64 and x86 architectures. // They contain minimal Landlock-only bindings with layout tests. #[allow(dead_code)] #[allow(non_camel_case_types)] #[allow(non_snake_case)] #[allow(non_upper_case_globals)] #[cfg(target_arch = "x86_64")] #[path = "landlock_x86_64.rs"] mod landlock; #[allow(dead_code)] #[allow(non_camel_case_types)] #[allow(non_snake_case)] #[allow(non_upper_case_globals)] #[cfg(target_arch = "x86")] #[path = "landlock_i686.rs"] mod landlock; // For all other architectures, use the architecture-agnostic landlock_all.rs // bindings without layout tests. #[allow(dead_code)] #[allow(non_camel_case_types)] #[allow(non_snake_case)] #[allow(non_upper_case_globals)] #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))] #[path = "landlock_all.rs"] mod landlock; #[rustfmt::skip] pub use self::landlock::{ landlock_net_port_attr, landlock_path_beneath_attr, landlock_rule_type, landlock_rule_type_LANDLOCK_RULE_NET_PORT, landlock_rule_type_LANDLOCK_RULE_PATH_BENEATH, landlock_ruleset_attr, LANDLOCK_ACCESS_FS_EXECUTE, LANDLOCK_ACCESS_FS_WRITE_FILE, LANDLOCK_ACCESS_FS_READ_FILE, LANDLOCK_ACCESS_FS_READ_DIR, LANDLOCK_ACCESS_FS_REMOVE_DIR, LANDLOCK_ACCESS_FS_REMOVE_FILE, LANDLOCK_ACCESS_FS_MAKE_CHAR, LANDLOCK_ACCESS_FS_MAKE_DIR, LANDLOCK_ACCESS_FS_MAKE_REG, LANDLOCK_ACCESS_FS_MAKE_SOCK, LANDLOCK_ACCESS_FS_MAKE_FIFO, LANDLOCK_ACCESS_FS_MAKE_BLOCK, LANDLOCK_ACCESS_FS_MAKE_SYM, LANDLOCK_ACCESS_FS_REFER, LANDLOCK_ACCESS_FS_TRUNCATE, LANDLOCK_ACCESS_FS_IOCTL_DEV, LANDLOCK_ACCESS_NET_BIND_TCP, LANDLOCK_ACCESS_NET_CONNECT_TCP, LANDLOCK_CREATE_RULESET_VERSION, LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET, LANDLOCK_SCOPE_SIGNAL, }; use libc::{ __u32, c_int, c_void, size_t, syscall, SYS_landlock_add_rule, SYS_landlock_create_ruleset, SYS_landlock_restrict_self, }; #[rustfmt::skip] pub unsafe fn landlock_create_ruleset(attr: *const landlock_ruleset_attr, size: size_t, flags: __u32) -> c_int { syscall(SYS_landlock_create_ruleset, attr, size, flags) as c_int } #[rustfmt::skip] pub unsafe fn landlock_add_rule(ruleset_fd: c_int, rule_type: landlock_rule_type, rule_attr: *const c_void, flags: __u32) -> c_int { syscall(SYS_landlock_add_rule, ruleset_fd, rule_type, rule_attr, flags) as c_int } pub unsafe fn landlock_restrict_self(ruleset_fd: c_int, flags: __u32) -> c_int { syscall(SYS_landlock_restrict_self, ruleset_fd, flags) as c_int }