diff --git a/.github/workflows/DailyArm64.yml b/.github/workflows/DailyArm64.yml index 0e72150c5..952c6416e 100644 --- a/.github/workflows/DailyArm64.yml +++ b/.github/workflows/DailyArm64.yml @@ -37,6 +37,15 @@ jobs: arch: arm64 config: ${{ matrix.config }} + # 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: + 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 +109,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/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: 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}}