From 2617ee40301c361adbfe8ac8a766e0b5841beadf Mon Sep 17 00:00:00 2001 From: Zvonko Kaiser Date: Tue, 7 Jul 2026 23:10:19 +0000 Subject: [PATCH 1/7] build: make Cargo.toml the single source of codegen truth Rustflags in .cargo/config.toml override [profile.release] silently and apply to every profile: the manifest claimed opt-level "s" while musl builds shipped "z", and forced panic=abort broke cargo test for musl targets. The cargo config keeps target mechanics (linker, +crt-static); the release profile owns codegen. opt-level stays "z", codegen-units=1 keeps fat LTO effective. Signed-off-by: Zvonko Kaiser Assisted-By: Claude Opus 4.8 (1M context) --- .cargo/config.toml | 25 +++++++------------------ Cargo.toml | 3 ++- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 1dd04c6c..0ca86e26 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,22 +1,11 @@ +# Target mechanics only; codegen policy lives in [profile.release]. +# Rustflags here override the profile and apply to every profile, +# including test builds. [target.aarch64-unknown-linux-musl] -rustflags = [ - "-C", "linker=musl-gcc", - "-C", "target-feature=+crt-static", # Use static crt - "-C", "link-arg=-s", # Strip symbols - "-C", "opt-level=z", # Optimize for size - "-C", "codegen-units=1", # Enable better (though slower) optimization - "-C", "panic=abort", # Remove stack unwind code -] - +linker = "musl-gcc" +rustflags = ["-C", "target-feature=+crt-static"] [target.x86_64-unknown-linux-musl] -rustflags = [ - "-C", "linker=musl-gcc", - "-C", "target-feature=+crt-static", # Use static crt - "-C", "link-arg=-s", # Strip symbols - "-C", "opt-level=z", # Optimize for size - "-C", "codegen-units=1", # Enable better (though slower) optimization - "-C", "panic=abort", # Remove stack unwind code -] - +linker = "musl-gcc" +rustflags = ["-C", "target-feature=+crt-static"] diff --git a/Cargo.toml b/Cargo.toml index 96f15822..258adccd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,9 @@ once_cell = "1.21.3" sha2 = { version = "0.10", default-features = false } [profile.release] -opt-level = "s" +opt-level = "z" # PID 1 runs once per boot: size beats speed lto = true +codegen-units = 1 # one unit gives fat LTO full visibility and a smaller binary strip = true panic = 'abort' From 2cb04932c46caf1f49c37b7a2516794ad62c4194 Mon Sep 17 00:00:00 2001 From: Zvonko Kaiser Date: Tue, 7 Jul 2026 23:10:19 +0000 Subject: [PATCH 2/7] build: pin the toolchain rustc embeds /rustc/ stdlib paths in the binary, so the compiler version is part of the release bytes. A floating "stable minus 2 releases" cannot reproduce a tag after the release calendar moves; 1.94.0 is what it resolves to today. rustup applies the pin, musl targets included, to every cargo invocation in the repo. Signed-off-by: Zvonko Kaiser Assisted-By: Claude Opus 4.8 (1M context) --- rust-toolchain.toml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..d0149937 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,9 @@ +# The compiler is a build input: rustc embeds /rustc/ stdlib +# paths in the binary, so byte-reproducibility requires one pinned rustc +# (ARCHITECTURE.md "Provenance & Supply-Chain Security"). Bump in its own +# commit. +[toolchain] +channel = "1.94.0" +profile = "minimal" +components = ["rustfmt", "clippy"] +targets = ["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl"] From 4cd49e815c8240f48c98740f2d284eb4c9e4b450 Mon Sep 17 00:00:00 2001 From: Zvonko Kaiser Date: Tue, 7 Jul 2026 23:10:19 +0000 Subject: [PATCH 3/7] ci: pin the toolchain install and enforce it everywhere dtolnay/rust-toolchain only sets the rustup default, which rust-toolchain.toml outranks: stable-labeled jobs ran the pin via an implicit download and nightly tools broke against it. A composite action parses the pin, fails fast when unreadable, and installs what cargo will actually run; nightly-only tools (udeps, miri, fuzz) select their channel per invocation with cargo +nightly. All dtolnay refs are pinned by commit SHA like the other actions. Every job that resolves the workspace runs --locked. The miri job drops || true so a finding fails CI, capped at 30 minutes. Signed-off-by: Zvonko Kaiser Assisted-By: Claude Opus 4.8 (1M context) --- .github/actions/rust-toolchain/action.yml | 38 +++++++++++++++++ .github/workflows/ci.yaml | 7 ++- .github/workflows/coverage.yaml | 7 ++- .github/workflows/fuzzing.yaml | 7 ++- .github/workflows/static-checks.yaml | 52 +++++++++++------------ 5 files changed, 73 insertions(+), 38 deletions(-) create mode 100644 .github/actions/rust-toolchain/action.yml diff --git a/.github/actions/rust-toolchain/action.yml b/.github/actions/rust-toolchain/action.yml new file mode 100644 index 00000000..4c9e0791 --- /dev/null +++ b/.github/actions/rust-toolchain/action.yml @@ -0,0 +1,38 @@ +name: Install pinned Rust toolchain +description: > + Installs the toolchain pinned in rust-toolchain.toml. + dtolnay/rust-toolchain only sets the rustup default, which the file + outranks, so the channel is parsed from the file to keep the install and + the pin in lockstep. + +inputs: + targets: + description: Comma-separated target triples to install + required: false + default: "" + components: + description: Comma-separated components to install + required: false + default: "" + +runs: + using: composite + steps: + - name: Read pinned toolchain + id: toolchain + shell: bash + # Fail fast on an unparseable pin. + run: | + channel=$(sed -n 's/^channel = "\(.*\)"$/\1/p' rust-toolchain.toml) + if [ -z "$channel" ]; then + echo "::error::cannot parse channel from rust-toolchain.toml" >&2 + exit 1 + fi + echo "channel=$channel" >> "$GITHUB_OUTPUT" + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master 2026-06-30 + with: + toolchain: ${{ steps.toolchain.outputs.channel }} + targets: ${{ inputs.targets }} + components: ${{ inputs.components }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3c320b6e..318d87b3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -56,17 +56,16 @@ jobs: ref: ${{ inputs.commit-hash != '' && inputs.commit-hash || github.sha }} fetch-depth: 0 # This is needed in order to keep the commit ids history persist-credentials: false - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain with: - toolchain: stable minus 2 releases targets: aarch64-unknown-linux-musl - name: Build NVRC run: | GIT_REV="$(git rev-parse --short HEAD)" if [ -n "$(git status --porcelain)" ]; then GIT_REV="${GIT_REV}-dirty"; fi export GIT_REV - cargo build --release --target=aarch64-unknown-linux-musl + cargo build --release --locked --target=aarch64-unknown-linux-musl sudo cp ./target/aarch64-unknown-linux-musl/release/NVRC "$ROOTFS_IMAGE_DIR/bin/NVRC-aarch64-unknown-linux-musl" - name: Create Image with NVRC run: | diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 9bac6569..4a90c9bb 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -48,10 +48,9 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain with: - toolchain: stable components: llvm-tools-preview # cargo-llvm-cov uses LLVM's native instrumentation for accurate coverage. @@ -71,7 +70,7 @@ jobs: # src/main.rs is exempt from coverage: dispatch-only plumbing (module # wiring and the mode match) that only runs end-to-end as PID 1 in a VM. - name: Generate coverage - run: sudo -E env "PATH=$PATH" cargo llvm-cov --all-features --workspace --ignore-filename-regex 'src/main\.rs$' --lcov --output-path lcov.info -- --include-ignored --test-threads=1 + run: sudo -E env "PATH=$PATH" cargo llvm-cov --locked --all-features --workspace --ignore-filename-regex 'src/main\.rs$' --lcov --output-path lcov.info -- --include-ignored --test-threads=1 - name: Upload coverage to Coveralls uses: coverallsapp/github-action@cfd0633edbd2411b532b808ba7a8b5e04f76d2c8 # v2.3.4 diff --git a/.github/workflows/fuzzing.yaml b/.github/workflows/fuzzing.yaml index d8040f1d..b06289df 100644 --- a/.github/workflows/fuzzing.yaml +++ b/.github/workflows/fuzzing.yaml @@ -40,8 +40,10 @@ jobs: - name: Install Rust nightly # WHY nightly: libFuzzer instrumentation requires nightly compiler features - uses: dtolnay/rust-toolchain@nightly + # WHY toolchain input: a SHA-pinned ref no longer selects the channel + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master 2026-06-30 with: + toolchain: nightly components: llvm-tools-preview - name: Install cargo-fuzz @@ -50,8 +52,9 @@ jobs: - name: Run fuzzer - ${{ matrix.target }} # WHY max_total_time: prevent infinite runs, control CI costs # WHY -jobs=2: parallel fuzzing instances for better coverage + # +nightly outranks rust-toolchain.toml; libFuzzer needs nightly rustc run: | - cargo fuzz run ${{ matrix.target }} -- \ + cargo +nightly fuzz run ${{ matrix.target }} -- \ -max_total_time=${{ env.FUZZ_DURATION }} \ -jobs=2 diff --git a/.github/workflows/static-checks.yaml b/.github/workflows/static-checks.yaml index d62bc49b..68949803 100644 --- a/.github/workflows/static-checks.yaml +++ b/.github/workflows/static-checks.yaml @@ -46,12 +46,10 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain - - run: sudo -E env "PATH=$PATH" cargo test --all-features -- --include-ignored + - run: sudo -E env "PATH=$PATH" cargo test --locked --all-features -- --include-ignored # Check code formatting against Rust style guidelines formatting: @@ -64,10 +62,9 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain with: - toolchain: stable components: rustfmt - run: cargo fmt --all -- --check @@ -83,13 +80,12 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain with: - toolchain: stable components: clippy - - run: cargo clippy --all-features -- -D warnings + - run: cargo clippy --locked --all-features -- -D warnings # Check dependencies for known security vulnerabilities security: @@ -102,10 +98,8 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain - run: cargo install cargo-audit - run: cargo audit @@ -121,10 +115,8 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain - run: cargo install cargo-deny - run: cargo deny check @@ -141,12 +133,13 @@ jobs: fetch-depth: 0 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master 2026-06-30 with: toolchain: nightly - run: cargo install cargo-udeps --locked - - run: cargo udeps --all-features + # +nightly outranks rust-toolchain.toml; udeps needs nightly-only -Z flags + - run: cargo +nightly udeps --locked --all-features # Analyze binary size and identify largest functions bloat: @@ -159,10 +152,8 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain - run: cargo install cargo-bloat - run: cargo bloat --release --all-features -n 20 @@ -170,6 +161,7 @@ jobs: # Detect undefined behavior in unsafe code using Miri interpreter miri: name: cargo miri + timeout-minutes: 30 needs: changes if: ${{ needs.changes.outputs.code == 'true' || github.event_name == 'schedule' }} runs-on: ubuntu-latest @@ -179,9 +171,13 @@ jobs: fetch-depth: 0 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master + uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88 # master 2026-06-30 with: toolchain: nightly components: miri - - run: cargo miri test || true # ignore errors since miri is still experimental and complains about the std toolchain \ No newline at end of file + # +nightly outranks rust-toolchain.toml; miri only exists on nightly. + # -Zmiri-disable-isolation: tests touch the real filesystem. + - run: cargo +nightly miri test --locked + env: + MIRIFLAGS: -Zmiri-disable-isolation \ No newline at end of file From 6cc48fe9a4131dea39a12ffb3473acee78219395 Mon Sep 17 00:00:00 2001 From: Zvonko Kaiser Date: Tue, 7 Jul 2026 23:10:19 +0000 Subject: [PATCH 4/7] build: make release builds byte-reproducible Panic Location strings embed $CARGO_HOME/registry paths and, with the rust-src component installed, the sysroot source tree; both vary per machine and change the sha256, breaking the boot-line to Rekor correlation from #166. scripts/build-release.sh remaps both onto stable forms, rejects unparseable rustc commit hashes, unsets GIT_REV, owns SOURCE_DATE_EPOCH, and builds --locked. Workspace paths are compiled relative and do not leak. Both the release workflow and the reproducibility check call this script. VERIFY.md documents the rebuild procedure; README points at it. No apt-get update: the image index suffices and third-party repo breakage fails jobs. Verified: builds from different directories with different CARGO_HOMEs produce identical sha256; the binary remains static-PIE. Signed-off-by: Zvonko Kaiser Assisted-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yaml | 15 +++++-------- README.md | 14 +++++++++--- VERIFY.md | 31 +++++++++++++++++++++++++ scripts/build-release.sh | 41 ++++++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 12 deletions(-) create mode 100755 scripts/build-release.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index bb6790d5..58107db3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -73,26 +73,23 @@ jobs: with: fetch-depth: 0 - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master # v1.70 + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain with: - toolchain: stable minus 2 releases targets: ${{ matrix.target }} - name: Install dependencies - run: | - sudo apt-get update - sudo apt-get install -y musl-tools jq + # No apt-get update: the image index suffices; refreshing pulls + # third-party repos whose breakage fails the job. + run: sudo apt-get install -y musl-tools jq - name: Build (${{ matrix.target }}) shell: bash run: | set -euxo pipefail mkdir -p dist - SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) - export SOURCE_DATE_EPOCH - cargo build --release --target "${{ matrix.target }}" + ./scripts/build-release.sh "${{ matrix.target }}" cp "target/${{ matrix.target }}/release/NVRC" "dist/NVRC-${{ matrix.target }}" - name: Compute digest (sha256sum format) diff --git a/README.md b/README.md index 30fbd531..5c1a3652 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,10 @@ nvrc.mode=gpu nvrc.fm.mode=0 nvrc.log=debug ## Build -NVRC is compiled as a statically-linked musl binary for minimal dependencies: +NVRC is compiled as a statically-linked musl binary for minimal dependencies. +The compiler version is pinned in `rust-toolchain.toml`; rustup installs it, +musl targets included, on first use. Requires `musl-gcc` (Debian/Ubuntu: +`musl-tools`). ```bash # x86_64 @@ -149,8 +152,13 @@ cargo build --release --target x86_64-unknown-linux-musl cargo build --release --target aarch64-unknown-linux-musl ``` -Build configuration in `.cargo/config.toml` enables aggressive size -optimization and static linking. +Release artifacts are built with `./scripts/build-release.sh `, which +additionally remaps machine-specific paths so the binary is byte-reproducible; +see [VERIFY.md](VERIFY.md) for reproducing and verifying a release. + +Codegen policy (size optimization, LTO, `panic=abort`) lives in +`[profile.release]` in `Cargo.toml`; `.cargo/config.toml` holds the musl +linker and static-linking flags. ## Testing diff --git a/VERIFY.md b/VERIFY.md index 21414059..6ad1cbe8 100644 --- a/VERIFY.md +++ b/VERIFY.md @@ -259,6 +259,37 @@ If you run releases via `workflow_dispatch`, you can also pin the trigger: --- +## 8) Reproduce the build + +Signature checks prove *who* built the artifact; a rebuild proves *what* was +built. The release binary is byte-reproducible: building the tag yourself +must yield exactly the sha256 recorded in Rekor — the same digest NVRC logs +at boot (`NVRC version=... sha256=...`), so a dmesg line can be traced all +the way back to public source. + +Requirements: `rustup` and `musl-gcc` (Debian/Ubuntu: `musl-tools`). The +compiler version is taken from `rust-toolchain.toml` automatically — +rustup installs the pinned toolchain and musl targets on first use. + +Run this from the directory that holds the extracted `NVRC-${TARGET}` +binary from steps 2–3; the final comparison refers back to it as +`../NVRC-${TARGET}`: + +```bash +git clone --branch "$TAG" --depth 1 https://github.com/NVIDIA/nvrc +cd nvrc +# Remaps machine-specific paths (CARGO_HOME, toolchain sysroot) so your +# bytes match CI's regardless of directory layout: +./scripts/build-release.sh "$TARGET" +sha256sum "target/${TARGET}/release/NVRC" "../NVRC-${TARGET}" +``` + +The two hashes must be identical. A mismatch means your toolchain deviates +from `rust-toolchain.toml` (check `rustc -V`) or the artifact was not built +from this tag. + +--- + ## Notes & troubleshooting - If **identity check fails**, ensure `REPO` and the regex diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 00000000..613ec813 --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) NVIDIA CORPORATION + +# Builds a release binary whose sha256 is independent of the build machine +# (ARCHITECTURE.md "Provenance & Supply-Chain Security", VERIFY.md +# "Reproduce the build"). Two machine-specific paths reach .rodata via panic +# Location strings and are remapped: +# +# $CARGO_HOME/registry/src/... dependency code +# $SYSROOT/lib/rustlib/src/... monomorphized std when the rust-src +# component is installed; mapped onto the +# /rustc/ form embedded without it +# +# Workspace paths are compiled relative and do not leak. +# +# The remaps use `cargo --config`, which joins same-key arrays with +# .cargo/config.toml. RUSTFLAGS would replace them: Cargo reads exactly one +# of CARGO_ENCODED_RUSTFLAGS, RUSTFLAGS, target.*.rustflags, +# build.rustflags (Cargo book, "build.rustflags"). +set -euo pipefail + +target="${1:?usage: $0 }" + +# A stray dev stamp would change release bytes; release identity is the bare +# Cargo version (see src/hash.rs). +unset GIT_REV + +# Owned here so the release build and the reproducibility check share one +# environment; the release tarball step consumes it. +SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(git log -1 --pretty=%ct)}" +export SOURCE_DATE_EPOCH + +cargo_home="${CARGO_HOME:-$HOME/.cargo}" +sysroot="$(rustc --print sysroot)" +rustc_commit="$(rustc -vV | sed -n 's/^commit-hash: //p')" +# An empty hash would remap onto /rustc/ and produce unmatchable bytes. +[ -n "$rustc_commit" ] || { echo "$0: cannot parse commit-hash from rustc -vV" >&2; exit 1; } + +exec cargo build --release --locked --target "$target" --config \ + "target.\"$target\".rustflags=[\"--remap-path-prefix=$cargo_home=/cargo\",\"--remap-path-prefix=$sysroot/lib/rustlib/src/rust=/rustc/$rustc_commit\"]" From 726b205c549593fd30433d4934afb3be17306408 Mon Sep 17 00:00:00 2001 From: Zvonko Kaiser Date: Tue, 7 Jul 2026 23:10:19 +0000 Subject: [PATCH 5/7] ci: verify reproducibility on every pull request Build the tree twice, varying the inputs a verifier cannot copy from CI (workspace directory, CARGO_HOME), and fail on hash mismatch with a strings diff. Runs the same script as the release workflow. Signed-off-by: Zvonko Kaiser Assisted-By: Claude Opus 4.8 (1M context) --- .github/workflows/reproducible.yaml | 65 +++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/reproducible.yaml diff --git a/.github/workflows/reproducible.yaml b/.github/workflows/reproducible.yaml new file mode 100644 index 00000000..f5cf073f --- /dev/null +++ b/.github/workflows/reproducible.yaml @@ -0,0 +1,65 @@ +name: Reproducible build + +# Build the tree twice, varying the inputs a verifier cannot copy from CI +# (workspace directory, CARGO_HOME), and require identical sha256. Guards +# the dmesg <-> Rekor correlation promised in VERIFY.md and ARCHITECTURE.md. + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + double-build: + strategy: + matrix: + include: + - target: x86_64-unknown-linux-musl + runner: ubuntu-24.04 + - target: aarch64-unknown-linux-musl + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: Install pinned Rust toolchain + uses: ./.github/actions/rust-toolchain + with: + targets: ${{ matrix.target }} + + - name: Install dependencies + # No apt-get update: the image index suffices; refreshing pulls + # third-party repos whose breakage fails the job. + run: sudo apt-get install -y musl-tools + + - name: Build twice from different locations + shell: bash + # Same script the release workflow runs. + run: | + set -euxo pipefail + for build in first second-deliberately-longer-path; do + src="${RUNNER_TEMP}/${build}/src" + mkdir -p "$src" "${RUNNER_TEMP}/${build}/cargo-home" + cp -a . "$src" + (cd "$src" && CARGO_HOME="${RUNNER_TEMP}/${build}/cargo-home" \ + ./scripts/build-release.sh "${{ matrix.target }}") + done + + - name: Require identical sha256 + shell: bash + run: | + set -euo pipefail + a="${RUNNER_TEMP}/first/src/target/${{ matrix.target }}/release/NVRC" + b="${RUNNER_TEMP}/second-deliberately-longer-path/src/target/${{ matrix.target }}/release/NVRC" + sha256sum "$a" "$b" + if [ "$(sha256sum < "$a")" != "$(sha256sum < "$b")" ]; then + echo "::error::release binary is not reproducible across build locations" + # The offending path is usually visible in the strings diff. + diff <(strings "$a") <(strings "$b") | head -50 || true + exit 1 + fi From efca4191371659271bd5262d62088e768536304a Mon Sep 17 00:00:00 2001 From: Zvonko Kaiser Date: Tue, 7 Jul 2026 23:10:20 +0000 Subject: [PATCH 6/7] test: gate syscall-boundary tests under miri miri aborts at the first foreign operation it cannot interpret: process spawn and exec, fork, unix sockets, mount, O_NONBLOCK opens, raw syscall(SYS_getpid), the sudo re-exec in require_root, and hashing /proc/self/exe, which under miri is the interpreter binary. Each such test carries cfg_attr(miri, ignore) with its reason; the pure logic keeps running under full UB interpretation. #[serial] applies only outside miri: serial_test's lock map sits behind tagged pointers that defeat the leak checker, and miri runs single-threaded. Signed-off-by: Zvonko Kaiser Assisted-By: Claude Opus 4.8 (1M context) --- src/daemon.rs | 10 ++++++++ src/execute.rs | 7 ++++++ src/hash.rs | 12 +++++++++ src/init.rs | 9 +++++++ src/kata_agent.rs | 23 +++++++++++++++++- src/kernel_params.rs | 42 ++++++++++++++++++++++++++------ src/kmsg.rs | 44 ++++++++++++++++++++++++++++++--- src/modprobe.rs | 12 +++++++-- src/mount.rs | 11 +++++++++ src/net.rs | 16 ++++++++++++ src/nvrc.rs | 5 ++++ src/smi.rs | 4 +++ src/syslog.rs | 58 +++++++++++++++++++++++++++++++++++++++++++- src/test_utils.rs | 4 +++ src/toolkit.rs | 2 ++ 15 files changed, 244 insertions(+), 15 deletions(-) diff --git a/src/daemon.rs b/src/daemon.rs index 46cf5121..2aede562 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -195,6 +195,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_nv_fabricmanager_gpu_mode() { use tempfile::NamedTempFile; @@ -212,6 +213,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_persistenced_success() { let tmpdir = TempDir::new().unwrap(); let run_dir = tmpdir.path().join("nvidia-persistenced"); @@ -227,6 +229,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_persistenced_uvm_disabled() { let tmpdir = TempDir::new().unwrap(); let run_dir = tmpdir.path().join("nvidia-persistenced"); @@ -237,6 +240,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_hostengine_success() { let mut nvrc = NVRC::default(); nvrc.dcgm_enabled = Some(true); @@ -245,6 +249,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_dcgm_exporter_success() { let mut nvrc = NVRC::default(); nvrc.dcgm_enabled = Some(true); @@ -252,12 +257,14 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_fabricmanager_success() { let mut nvrc = NVRC::default(); nvrc.spawn_fabricmanager("/bin/true"); } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_fabricmanager_with_port_guid() { let mut nvrc = NVRC::default(); nvrc.port_guid = Some("0xdeadbeef".to_string()); @@ -266,6 +273,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_nvlsm_success() { let mut nvrc = NVRC::default(); nvrc.port_guid = Some("0xdeadbeef".to_string()); @@ -274,6 +282,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_nvlsm_skipped_without_guid() { let mut nvrc = NVRC::default(); // port_guid is None, should be a no-op @@ -281,6 +290,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_spawn_persistenced_binary_not_found() { use std::panic; diff --git a/src/execute.rs b/src/execute.rs index 3ad79c78..d04dda47 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -47,11 +47,13 @@ mod tests { // ==================== foreground tests ==================== #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_foreground_success() { foreground("/bin/true", &[]); } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_foreground_failure_exit_code() { // Command runs but exits non-zero - should panic let result = panic::catch_unwind(|| { @@ -61,6 +63,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_foreground_not_found() { // Command doesn't exist - should panic let result = panic::catch_unwind(|| { @@ -70,6 +73,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_foreground_with_args() { foreground("/bin/sh", &["-c", "exit 0"]); @@ -82,6 +86,7 @@ mod tests { // ==================== background tests ==================== #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_background_spawns() { let mut child = background("/bin/sleep", &["0.01"]); let status = child.wait().unwrap(); @@ -89,6 +94,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_background_not_found() { // Command doesn't exist - should panic let result = panic::catch_unwind(|| { @@ -98,6 +104,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_background_check_later() { let mut child = background("/bin/sh", &["-c", "exit 7"]); let status = child.wait().unwrap(); diff --git a/src/hash.rs b/src/hash.rs index 59564be0..54dcd0c4 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -74,6 +74,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "under miri /proc/self/exe is the interpreter binary; hashing it takes hours" + )] fn test_sha256_self_returns_64_hex_chars() { let digest = sha256().expect("hash self"); assert_eq!(digest.len(), 64); @@ -99,6 +103,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "under miri /proc/self/exe is the interpreter binary; hashing it takes hours" + )] fn test_self_exe_runs_to_completion() { self_exe(); } @@ -122,6 +130,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "under miri /proc/self/exe is the interpreter binary; hashing it takes hours" + )] fn test_boot_line_of_self_carries_cargo_version_and_real_digest() { let digest = sha256().expect("hash self"); let line = boot_line(&digest, GIT_REV); diff --git a/src/init.rs b/src/init.rs index 7b39f222..e990ecac 100644 --- a/src/init.rs +++ b/src/init.rs @@ -35,6 +35,7 @@ mod tests { use std::cell::Cell; #[test] + #[cfg_attr(miri, ignore = "raw syscall(SYS_getpid) is not shimmed by miri")] fn test_running_as_init_false_for_test_harness() { // The test runner is never PID 1, so the real syscall must report so. assert!(!running_as_init()); @@ -51,6 +52,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_as_pid1_exits_when_not_init() { let exited = Cell::new(false); as_pid1_with(false, || exited.set(true)); @@ -66,6 +71,10 @@ mod tests { // is instrumented too, and untaken lines count against the per-file gate. #[cfg(coverage)] #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_as_pid1_production_guard_exits_zero() { let pid = unsafe { libc::fork() }; if pid == 0 { diff --git a/src/kata_agent.rs b/src/kata_agent.rs index 1bc30b9d..4c9d16e9 100644 --- a/src/kata_agent.rs +++ b/src/kata_agent.rs @@ -98,6 +98,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_agent_setup() { require_root(); @@ -115,6 +119,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "exec_agent calls execvp, which miri cannot emulate")] fn test_exec_agent_not_found() { // exec_agent with nonexistent command panics (doesn't exec) let result = panic::catch_unwind(|| { @@ -124,6 +129,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_kata_agent_not_found() { require_root(); @@ -146,6 +155,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_syslog_loop_timeout() { // ~1s: two 500ms iterations; try_poll() is best-effort on /dev/log. let start = std::time::Instant::now(); @@ -163,7 +176,11 @@ mod tests { /// already bound by main(); this child does a fresh bind — on dev hosts /// with a host syslog daemon, reverting to `poll()` reproduces via EADDRINUSE. #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_syslog_loop_does_not_trigger_power_off_hook() { use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -205,6 +222,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_fork_agent_with_timeout() { require_root(); diff --git a/src/kernel_params.rs b/src/kernel_params.rs index 01e70fca..a1281ae6 100644 --- a/src/kernel_params.rs +++ b/src/kernel_params.rs @@ -123,7 +123,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_nvrc_log_debug() { require_root(); log_setup(); @@ -134,7 +138,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_process_kernel_params_nvrc_log_debug() { require_root(); log_setup(); @@ -149,7 +157,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_process_kernel_params_nvrc_log_info() { require_root(); log_setup(); @@ -164,7 +176,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_process_kernel_params_nvrc_log_0() { require_root(); log_setup(); @@ -175,7 +191,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_process_kernel_params_nvrc_log_none() { require_root(); log_setup(); @@ -186,7 +206,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_process_kernel_params_nvrc_log_trace() { require_root(); log_setup(); @@ -197,7 +221,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_process_kernel_params_nvrc_log_unknown() { require_root(); log_setup(); diff --git a/src/kmsg.rs b/src/kmsg.rs index eb23babc..d30e75a6 100644 --- a/src/kmsg.rs +++ b/src/kmsg.rs @@ -151,7 +151,7 @@ mod tests { } #[test] - #[serial] + #[cfg_attr(not(miri), serial)] fn test_kmsg_routes_to_dev_null_when_log_off() { // Default log level is Off, so kmsg() should open /dev/null log::set_max_level(log::LevelFilter::Off); @@ -159,7 +159,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_kmsg_routes_to_kmsg_when_debug() { require_root(); // When debug is enabled, kmsg() should open /dev/kmsg @@ -169,7 +173,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_kernlog_setup() { require_root(); @@ -213,6 +221,10 @@ mod tests { // === wait_for_marker tests === #[test] + #[cfg_attr( + miri, + ignore = "open_kmsg uses O_NONBLOCK, an open flag miri does not support" + )] fn test_wait_for_marker_finds_marker() { let mut tmp = NamedTempFile::new().unwrap(); writeln!(tmp, "some noise").unwrap(); @@ -228,6 +240,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "open_kmsg uses O_NONBLOCK, an open flag miri does not support" + )] fn test_wait_for_marker_finds_marker_at_end() { let mut tmp = NamedTempFile::new().unwrap(); writeln!(tmp, "line 1").unwrap(); @@ -243,6 +259,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "open_kmsg uses O_NONBLOCK, an open flag miri does not support" + )] fn test_wait_for_marker_no_marker_panics() { let mut tmp = NamedTempFile::new().unwrap(); writeln!(tmp, "no match here").unwrap(); @@ -259,6 +279,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "open_kmsg uses O_NONBLOCK, an open flag miri does not support" + )] fn test_wait_for_marker_empty_file_panics() { let tmp = NamedTempFile::new().unwrap(); @@ -273,6 +297,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "open_kmsg uses O_NONBLOCK, an open flag miri does not support" + )] fn test_wait_for_marker_nonexistent_file_panics() { let result = panic::catch_unwind(|| { wait_for_marker(&mut open_kmsg("/nonexistent/path"), "marker", 1); @@ -281,6 +309,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "open_kmsg uses O_NONBLOCK, an open flag miri does not support" + )] fn test_wait_for_marker_partial_match_not_enough() { let mut tmp = NamedTempFile::new().unwrap(); writeln!(tmp, "FM starting").unwrap(); @@ -299,7 +331,11 @@ mod tests { } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_wait_for_marker_on_dev_kmsg() { require_root(); diff --git a/src/modprobe.rs b/src/modprobe.rs index 2ae19d7f..49e1a58c 100644 --- a/src/modprobe.rs +++ b/src/modprobe.rs @@ -40,14 +40,22 @@ mod tests { // calls can race and cause spurious failures. #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_load_loop() { require_root(); load("loop"); } #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_load_nonexistent() { require_root(); let result = panic::catch_unwind(|| { diff --git a/src/mount.rs b/src/mount.rs index 56597eef..e00d08f3 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -109,6 +109,7 @@ mod tests { // === mount_optional tests === #[test] + #[cfg_attr(miri, ignore = "mount/mknod are foreign syscalls miri cannot emulate")] fn test_mount_optional_target_not_exists() { // When target path doesn't exist, should be no-op let filesystems = "nodev tmpfs\n"; @@ -122,6 +123,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "mount/mknod are foreign syscalls miri cannot emulate")] fn test_mount_optional_fs_not_available() { use tempfile::TempDir; @@ -141,6 +143,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_mount_optional_success() { use nix::mount::umount; use tempfile::TempDir; @@ -169,6 +175,7 @@ mod tests { // === Error path tests === #[test] + #[cfg_attr(miri, ignore = "mount/mknod are foreign syscalls miri cannot emulate")] fn test_mount_fails_nonexistent_target() { use std::panic; @@ -187,6 +194,10 @@ mod tests { // === setup_at() tests with temp directory === #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_setup_at_with_temp_root() { use nix::mount::umount; use tempfile::TempDir; diff --git a/src/net.rs b/src/net.rs index e29560a9..05b1ba9c 100644 --- a/src/net.rs +++ b/src/net.rs @@ -40,6 +40,10 @@ mod tests { use crate::test_utils::require_root; #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_loopback_up() { require_root(); // lo is already up on a normal system, calling again is idempotent @@ -47,6 +51,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_loopback_up_idempotent() { require_root(); loopback_up(); @@ -54,6 +62,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_loopback_up_verifies_flags() { require_root(); loopback_up(); @@ -82,6 +94,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_siocgifflags_invalid_interface() { require_root(); diff --git a/src/nvrc.rs b/src/nvrc.rs index 136442c7..caa80d3f 100644 --- a/src/nvrc.rs +++ b/src/nvrc.rs @@ -67,6 +67,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_track_daemon() { let mut nvrc = NVRC::default(); let child = Command::new("/bin/true").spawn().unwrap(); @@ -76,6 +77,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_health_checks_success() { let mut nvrc = NVRC::default(); // /bin/true exits with 0 @@ -86,6 +88,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_health_checks_failure() { let mut nvrc = NVRC::default(); // /bin/false exits with 1 @@ -99,6 +102,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_health_checks_still_running() { let mut nvrc = NVRC::default(); // sleep 1 will still be running when we check immediately @@ -114,6 +118,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "miri cannot emulate process spawn")] fn test_health_checks_multiple() { let mut nvrc = NVRC::default(); nvrc.track_daemon("d1", Command::new("/bin/true").spawn().unwrap()); diff --git a/src/smi.rs b/src/smi.rs index 3b8914f7..69c38160 100644 --- a/src/smi.rs +++ b/src/smi.rs @@ -82,6 +82,7 @@ mod tests { // When fields are Some, nvidia-smi is called (panics without NVIDIA hardware) #[test] + #[cfg_attr(miri, ignore = "spawns a process, which miri cannot emulate")] fn test_lmc_some_fails_without_nvidia_smi() { let mut nvrc = NVRC::default(); nvrc.nvidia_smi_lmc = Some(1000); @@ -92,6 +93,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "spawns a process, which miri cannot emulate")] fn test_lgc_some_fails_without_nvidia_smi() { let mut nvrc = NVRC::default(); nvrc.nvidia_smi_lgc = Some(1500); @@ -102,6 +104,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "spawns a process, which miri cannot emulate")] fn test_pl_some_fails_without_nvidia_smi() { let mut nvrc = NVRC::default(); nvrc.nvidia_smi_pl = Some(300); @@ -112,6 +115,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "spawns a process, which miri cannot emulate")] fn test_srs_some_fails_without_nvidia_smi() { let mut nvrc = NVRC::default(); nvrc.nvidia_smi_srs = Some("1".into()); diff --git a/src/syslog.rs b/src/syslog.rs index e3101b6d..41386af5 100644 --- a/src/syslog.rs +++ b/src/syslog.rs @@ -174,6 +174,10 @@ mod tests { // === bind tests === #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_bind_success() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -182,6 +186,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_bind_nonexistent_dir() { let path = Path::new("/nonexistent/dir/test.sock"); let err = bind(path).unwrap_err(); @@ -190,6 +198,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_bind_already_exists() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -202,6 +214,10 @@ mod tests { // === poll_socket tests === #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_socket_no_data() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -212,6 +228,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_socket_with_data() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -225,6 +245,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_socket_strips_priority() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -238,6 +262,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_socket_multiple_messages() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -260,6 +288,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_socket_trims_trailing_whitespace() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -275,6 +307,10 @@ mod tests { // === poll_at / poll_once tests === #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_once_no_data() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -285,6 +321,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_once_with_data() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("test.sock"); @@ -302,6 +342,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_at_custom_path() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("custom.sock"); @@ -312,6 +356,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_poll_dev_log() { use std::panic; // poll() tries to bind /dev/log - may panic if already bound or no permission @@ -320,6 +368,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "fork/socket syscalls are foreign functions miri cannot emulate" + )] fn test_try_poll_swallows_errors() { // /dev/log may be foreign (bind fails) or already ours; both must be // non-fatal for the best-effort drain. @@ -328,7 +380,11 @@ mod tests { // Serialized with the kmsg test that removes and recreates the same file. #[test] - #[serial] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] + #[cfg_attr(not(miri), serial)] fn test_forward_message_appends_to_syslog_file() { crate::test_utils::require_root(); // Nonce keeps the assertion honest against /run/syslog.log contents diff --git a/src/test_utils.rs b/src/test_utils.rs index 9efdeb9f..55bdfd51 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -75,6 +75,10 @@ mod tests { } #[test] + #[cfg_attr( + miri, + ignore = "root-gated: require_root re-execs the test binary via sudo, which miri cannot emulate" + )] fn test_require_root_when_actually_root() { // We're running as root for coverage, so this should succeed require_root(); diff --git a/src/toolkit.rs b/src/toolkit.rs index ea7de5a6..8f43906e 100644 --- a/src/toolkit.rs +++ b/src/toolkit.rs @@ -29,6 +29,7 @@ mod tests { use std::panic; #[test] + #[cfg_attr(miri, ignore = "spawns a process, which miri cannot emulate")] fn test_ctk_fails_without_binary() { let result = panic::catch_unwind(|| { ctk(&["--version"]); @@ -37,6 +38,7 @@ mod tests { } #[test] + #[cfg_attr(miri, ignore = "spawns a process, which miri cannot emulate")] fn test_nvidia_ctk_cdi_fails_without_binary() { let result = panic::catch_unwind(|| { nvidia_ctk_cdi(); From bcf756e9f07eba109543bf2e4c3d9b8b0550c747 Mon Sep 17 00:00:00 2001 From: Zvonko Kaiser Date: Tue, 7 Jul 2026 23:10:20 +0000 Subject: [PATCH 7/7] test: panic hook tests restore state and assert the shutdown path The hook installer test leaked the process-global hook, so every later caught panic wrote /dev/kmsg and called sync(); the power_off variant outlived its test and powered off the machine on the next caught panic. Both hook tests now restore the previous hook, and a new test asserts a panic reaches the shutdown action, which was previously covered only by accidental firings of the leaked hook. power_off itself stays uncovered: executing it powers off the test machine. Signed-off-by: Zvonko Kaiser Assisted-By: Claude Opus 4.8 (1M context) --- src/lockdown.rs | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/lockdown.rs b/src/lockdown.rs index ae1c5eca..b8cc9f3c 100644 --- a/src/lockdown.rs +++ b/src/lockdown.rs @@ -60,21 +60,6 @@ mod tests { use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; - #[test] - fn test_set_panic_hook_with_custom_action() { - let called = Arc::new(AtomicBool::new(false)); - let called_clone = called.clone(); - - // Install hook with test closure - set_panic_hook_with(move || { - called_clone.store(true, Ordering::SeqCst); - }); - - // The hook is installed - we can't trigger it without panicking, - // but we've exercised the code path - assert!(!called.load(Ordering::SeqCst)); // Not called yet - } - #[test] #[ignore] // Permanently disables module loading until reboot - run with --include-ignored on CI fn test_disable_modules_loading() { @@ -95,11 +80,34 @@ mod tests { let _: fn() = power_off; } + #[test] + #[cfg_attr(not(miri), serial_test::serial)] + #[cfg_attr(miri, ignore = "the hook calls sync(), which miri cannot emulate")] + fn test_panic_hook_invokes_shutdown_on_panic() { + let saved_hook = panic::take_hook(); + let called = Arc::new(AtomicBool::new(false)); + let called_clone = called.clone(); + set_panic_hook_with(move || called_clone.store(true, Ordering::SeqCst)); + + assert!(!called.load(Ordering::SeqCst)); // must not fire on install + + let result = panic::catch_unwind(|| panic!("boom")); + + assert!(result.is_err()); + assert!( + called.load(Ordering::SeqCst), + "panic must reach the shutdown action" + ); + panic::set_hook(saved_hook); + } + #[test] #[ignore] // Installs real power_off hook - run with --include-ignored on CI fn test_set_panic_hook() { - // Installs the real hook (with power_off) - just don't trigger it! + // Restore the previous hook: leaving power_off installed powers + // off the machine on the next caught panic. + let saved_hook = panic::take_hook(); set_panic_hook(); - // If we got here, the hook was installed successfully + panic::set_hook(saved_hook); } }