Skip to content

Miri

Miri #217

Workflow file for this run

# Miri: Rust undefined behavior detector
# Runs tests under the Miri interpreter to catch UB, invalid memory access,
# and unsafe code issues that normal test runs cannot detect.
#
# **Not on pull requests**, since 2026-08-22. It is the longest job in this repo by a wide margin —
# a median of 9 minutes against `ci.yml`'s 1 and `coverage.yml`'s 4 — and running it on the PR *and*
# on the merge meant paying that twice for every change. It has never failed: 92 successes and 6
# runs cancelled by the rule below, across the last hundred. So it moves to where one run per change
# is enough, and stops being the thing a PR waits on.
#
# A **path filter** was the first proposal and was measured rather than adopted. Skipping Miri for
# changes that cannot affect it does not work in this crate: filtering to the obviously-unsafe
# modules would have to exclude `dbgeng.rs` (23 tests Miri executes) and `pool/**` (81, decoding raw
# kernel structures out of byte slices), which is the most UB-prone code here; and a filter that
# keeps everything Miri's run could depend on — `src/**`, `Cargo.*`, `build.rs` — would have skipped
# 2 of the last 30 merged pull requests. Nearly every change here touches `src/`.
#
# What this costs: a UB regression is found on `main` rather than on the branch. That is a small
# difference in a repo where `main` is not protected and the author merges their own work, and
# `workflow_dispatch` runs it against a branch when a change is one Miri would have something to say
# about.
name: Miri
on:
push:
branches: [ "main" ]
# The toolchain is the other thing that can turn this red, and it moves on its own: Miri ships
# with nightly, and a nightly that tightens a rule finds the same code newly wrong. A weekly run
# separates "the code changed" from "the checker changed", which a push-only schedule cannot.
schedule:
- cron: "0 6 * * 1"
workflow_dispatch:
concurrency:
# A superseded push should cancel its predecessor: two merges landing minutes apart otherwise
# leave the first still checking code nobody has any more. The rule was written for pull
# requests, where five pushes to one PR meant five concurrent Miri runs — most of an hour of
# runner time nobody would read, back when a run took ~28 minutes — and it outlives them because
# the shape is the same and the reasoning never depended on the duration.
#
# Manual dispatches and scheduled runs are deliberately excluded. Both share `github.ref` with
# the branch's own run — `main`, in the scheduled case — so a shared group would have them cancel
# each other: a Monday run would kill the merge that happened to land beside it, or be killed by
# it, and a repeat dispatch would kill the run someone had just asked for. Keying them by run id
# gives each one a group of its own: never cancelled, never cancelling.
group: miri-${{ github.ref }}-${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') && github.run_id || 'auto' }}
cancel-in-progress: true
permissions:
contents: read
jobs:
miri:
name: Miri
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- uses: microsoft/setup-msbuild@v3
with:
vs-architecture: x64
- name: Install nightly Rust with Miri
run: |
rustup toolchain install nightly --component miri,rust-src
rustup override set nightly
# After the toolchain install, never before: rust-cache derives its key from the active
# rustc, so caching earlier would key everything on stable and restore the wrong artifacts.
# A nightly bump changes the key and rebuilds, which is what we want — the Miri sysroot in
# `cache-directories` is toolchain-specific and must never outlive the toolchain that
# produced it. Within one nightly this turns `cargo miri setup` into a no-op.
- uses: Swatinem/rust-cache@v2
with:
key: miri
cache-directories: "~/AppData/Local/rust-lang/miri/cache"
- uses: taiki-e/install-action@nextest
- name: Setup Miri sysroot
run: cargo miri setup
# nextest, not `cargo miri test`, because libtest's threads all share one interpreter —
# Miri interleaves them rather than running them in parallel, so a multi-core runner buys
# nothing. nextest puts each test in its own process, which is real parallelism: 27.5s
# against 43.7s locally. The cost is ~1.3s of sysroot load per test process, so this is a
# win only while the suite is short; revisit if a single test starts to dominate again.
- name: Run Miri
env:
MIRIFLAGS: "-Zmiri-disable-isolation -Zmiri-ignore-leaks"
run: cargo miri nextest run
# nextest cannot run doctests, and ci.yml is on nextest too — without this step the crate's
# doctests would run nowhere in CI. It is ~0.3s on top of an already-built tree.
- name: Run Miri doctests
env:
MIRIFLAGS: "-Zmiri-disable-isolation -Zmiri-ignore-leaks"
run: cargo miri test --doc