From f65eab9bbcbcbabde8690a8e9f0b10483490ade2 Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Wed, 7 Oct 2026 23:51:33 +0100 Subject: [PATCH 1/2] Run Miri once per architecture, on the hosted runners Miri interprets rather than executing. It reaches no hypervisor, reads no guest binary and asks nothing of a runner beyond the target it compiles for, yet it ran on every Linux build-and-test job: eight times over on X64 and twice on aarch64, for two answers, each on a pool the hypervisor jobs were queuing for. It runs once per architecture now, in a job of its own on the hosted runners. The build-and-test jobs shed a step that was a sizeable share of what was left between them and their timeout, and the hypervisor pools stop spending a slot on work that never asked for one. The shared setup is not used there. It reads /dev/kvm to report what the runner offers, which a hosted one need not have, and installs a C toolchain and spare Rust versions for builds Miri does not do. A toolchain, just and nextest are what it wants. `miri-tests` installs the pinned nightly it names rather than expecting the runner to carry it, which is what let it assume a provisioned image before. It still takes no arguments, which is what makes one job per architecture enough. Should it ever take a profile or a feature, that becomes another axis and the matrix has to grow with it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jorge Prendes --- .github/workflows/DailyArm64.yml | 12 +++- .github/workflows/ValidatePullRequest.yml | 20 +++++++ .github/workflows/dep_build_test.yml | 16 +---- .github/workflows/dep_miri.yml | 72 +++++++++++++++++++++++ Justfile | 8 ++- 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/dep_miri.yml diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index 0e72150c5..803292578 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -37,6 +37,14 @@ jobs: arch: arm64 config: ${{ matrix.config }} + # Miri on aarch64, which the build-and-test matrix no longer carries. + miri: + needs: build-guests + uses: ./.github/workflows/dep_miri.yml + secrets: inherit + with: + arch: arm64 + # Full aarch64 build-and-test. full_aarch64: "true" re-enables the # single-driver steps that are skipped on PRs. build-test: @@ -100,8 +108,8 @@ jobs: # `gh issue create` call will fail. notify-failure: runs-on: ubuntu-latest - needs: [build-guests, build-test, run-examples, fuzzing] - if: always() && (needs.build-guests.result == 'failure' || needs.build-test.result == 'failure' || needs.run-examples.result == 'failure' || needs.fuzzing.result == 'failure') + needs: [build-guests, miri, build-test, run-examples, fuzzing] + if: always() && (needs.build-guests.result == 'failure' || needs.miri.result == 'failure' || needs.build-test.result == 'failure' || needs.run-examples.result == 'failure' || needs.fuzzing.result == 'failure') permissions: # Job-level permissions replace (not merge with) the workflow-level grant, # so contents:read must be re-declared for actions/checkout to work. diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index b0d500e6d..f899894ec 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -205,6 +205,25 @@ jobs: config: ${{ matrix.config }} should_regen_goldens: ${{ needs.check-golden-label.outputs.should_regen_goldens }} + # Miri interprets rather than executing, so it reaches no hypervisor and only + # the architecture it compiles for changes what it covers. One job each is + # what the build-and-test matrix used to repeat ten times over. + miri: + needs: [docs-pr, update-guest-locks] + # Required because update-guest-locks is skipped on non-dependabot PRs, + # and a skipped dependency transitively skips all downstream jobs. + # See: https://github.com/actions/runner/issues/2205 + if: ${{ !cancelled() && !failure() }} + strategy: + fail-fast: false + matrix: + arch: [X64, arm64] + uses: ./.github/workflows/dep_miri.yml + secrets: inherit + with: + docs_only: ${{ needs.docs-pr.outputs.docs-only }} + arch: ${{ matrix.arch }} + # Run examples - needs guest artifacts, runs in parallel with build-test run-examples: needs: @@ -347,6 +366,7 @@ jobs: - update-guest-locks - build-guests - build-baseline-guests + - miri - code-checks - check-golden-label - build-test diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index 88a36b6d7..37c59796f 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -29,8 +29,8 @@ on: full_aarch64: description: >- When "true", also run the single-driver tests on aarch64. PRs leave - this "false" so the aarch64 runner builds and runs Miri and the default - Rust tests; the daily schedule (DailyArm64.yml) sets it "true". This + this "false" so the aarch64 runner builds and runs the default Rust + tests; the daily schedule (DailyArm64.yml) sets it "true". This input only affects aarch64: it does not change X64, where those tests already run on Linux and are skipped on Windows regardless. required: false @@ -134,18 +134,6 @@ jobs: - name: Build run: just build ${{ inputs.config }} - - name: Install cargo-nextest - if: runner.os == 'Linux' - uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 - with: - tool: cargo-nextest - - - name: Run Miri tests - # Linux on both architectures. arch/aarch64 carries no tests of its own - # yet, so aarch64 covers a subset until some land. - if: runner.os == 'Linux' - run: env -u RUSTC_WRAPPER just miri-tests - - name: Run Rust tests run: | # with default features diff --git a/.github/workflows/dep_miri.yml b/.github/workflows/dep_miri.yml new file mode 100644 index 000000000..4e9a428fd --- /dev/null +++ b/.github/workflows/dep_miri.yml @@ -0,0 +1,72 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +# Reusable workflow to run the Miri tests for a single architecture. +# +# Miri interprets rather than executing, so it never reaches a hypervisor and +# nothing it does depends on which one the runner offers. It runs on the hosted +# runners for that reason. The only axis its results depend on is the target it +# compiles for, so one job per architecture covers what the whole +# build-and-test matrix used to repeat. +# +# `just miri-tests` takes no arguments. Should it ever take a profile or a +# feature, that becomes another axis and this matrix has to grow with it. + +name: Miri + +on: + workflow_call: + inputs: + docs_only: + description: Skip building if docs only + required: false + type: string + default: "false" + arch: + description: CPU architecture for the run, X64 or arm64 (passed from caller matrix) + required: true + type: string + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + miri: + if: ${{ inputs.docs_only == 'false' }} + timeout-minutes: ${{ fromJSON(vars.MIRI_JOB_TIMEOUT || '30') }} + # Miri asks nothing of a runner beyond its architecture, so it runs on the + # hosted ones and leaves the hypervisor pools to the jobs that need them. + runs-on: ${{ inputs.arch == 'X64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # The shared setup reads /dev/kvm to report what the runner offers, which + # a hosted one need not have. Miri wants a toolchain and two tools, so it + # asks for those rather than everything a hypervisor job needs. + - uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067 # master + with: + toolchain: "1.94" + + - uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 + with: + tool: just,cargo-nextest + + - name: Rust cache + uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + with: + shared-key: "${{ inputs.arch }}-Linux-miri" + cache-on-failure: "true" + # Only save on main as caches are not shared across branches. + save-if: ${{ github.ref == 'refs/heads/main' }} + + # Miri reads no guest binaries: it covers hyperlight-common and the + # shared memory tests, neither of which loads one. + - name: Run Miri tests + run: env -u RUSTC_WRAPPER just miri-tests diff --git a/Justfile b/Justfile index 37d07862a..0ccfb4c2f 100644 --- a/Justfile +++ b/Justfile @@ -344,10 +344,14 @@ miri-jobs := env('MIRI_JOBS', num_cpus()) ensure-cargo-nextest: {{ if os() == "windows" { "if (-not (cargo nextest --version 2>$null)) { cargo install --locked cargo-nextest }" } else { "cargo nextest --version >/dev/null 2>&1 || cargo install --locked cargo-nextest" } }} -miri-tests: (ensure-cargo-nextest) +# The pinned nightly and Miri, which a runner carries only if its image does +ensure-nightly-miri: + rustup toolchain list | grep -q '^{{nightly-toolchain}}' || rustup toolchain install {{nightly-toolchain}} + rustup +{{nightly-toolchain}} component list | grep -q 'miri.*installed' || rustup component add miri --toolchain {{nightly-toolchain}} + +miri-tests: (ensure-cargo-nextest) (ensure-nightly-miri) @# A Miri interpreter is single-threaded, so libtest's --test-threads cannot @# use more than one core. Nextest spawns one Miri process per test. - rustup +{{nightly-toolchain}} component list | grep -q "miri.*installed" || rustup component add miri --toolchain {{nightly-toolchain}} # We can add more as needed cargo +{{nightly-toolchain}} miri nextest run -p hyperlight-common -F trace_guest -j {{miri-jobs}} cargo +{{nightly-toolchain}} miri nextest run -p hyperlight-host --lib -E 'test(/mem::shared_mem::tests/)' -j {{miri-jobs}} From 58ffc74cf7ebdfd132aa7769ba6cb9b3567c5940 Mon Sep 17 00:00:00 2001 From: Jorge Prendes Date: Thu, 8 Oct 2026 00:03:39 +0100 Subject: [PATCH 2/2] Run Miri once everywhere it ran, not only on pull requests The nightly musl matrix ran Miri in all eight of its entries, on the same pools the pull request matrix was freed from. Its `TARGET_TRIPLE` never reaches the recipe, which builds for the host with plain cargo, and the toolchain it pins is the same one every other job uses, so the eight runs covered what one does. The aarch64 daily run waited on the guest builds before starting. Miri reads no guest binary, so it now waits for nothing and reports even when they fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Jorge Prendes --- .github/workflows/DailyArm64.yml | 5 +++-- .github/workflows/RustNightly.yml | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index 803292578..952c6416e 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -37,9 +37,10 @@ jobs: arch: arm64 config: ${{ matrix.config }} - # Miri on aarch64, which the build-and-test matrix no longer carries. + # Miri on aarch64, which the build-and-test matrix no longer carries. It + # reads no guest binaries, so it waits for nothing and reports even when the + # guests fail to build. miri: - needs: build-guests uses: ./.github/workflows/dep_miri.yml secrets: inherit with: diff --git a/.github/workflows/RustNightly.yml b/.github/workflows/RustNightly.yml index 8c142d893..82bf61e92 100644 --- a/.github/workflows/RustNightly.yml +++ b/.github/workflows/RustNightly.yml @@ -98,14 +98,6 @@ jobs: - name: Build run: just build ${{ matrix.config }} - - name: Install cargo-nextest - uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21 - with: - tool: cargo-nextest - - - name: Run Miri tests - run: env -u RUSTC_WRAPPER just miri-tests - - name: Run Rust tests run: | # with default features @@ -140,10 +132,18 @@ jobs: RUST_LOG: debug run: just test-rust-tracing ${{ matrix.config }} + # Miri asks nothing of a runner beyond its architecture, so it runs once on a + # hosted one rather than in every entry of the musl matrix. + miri: + uses: ./.github/workflows/dep_miri.yml + secrets: inherit + with: + arch: X64 + notify-failure: runs-on: ubuntu-latest - needs: musl - if: always() && needs.musl.result == 'failure' + needs: [musl, miri] + if: always() && (needs.musl.result == 'failure' || needs.miri.result == 'failure') permissions: issues: write steps: