agentleFS
po4ykapo4yka/rust-skills/skills/rust-sanitizers-miri/SKILL.md

rust-sanitizers-miri

Skill5 starsChanged 13 days agoBSD-3-ClauseView on GitHub

Use when running Miri or a sanitizer (ASan, ThreadSanitizer, MSan, HWASan) on Rust code, choosing between Stacked Borrows and Tree Borrows, reading a Miri or ASan report, stubbing FFI that Miri cannot execute, enabling Android MTE or Xcode sanitizers for a Rust library, running UBSan on a C dependency, or wiring these checks into CI. Not for writing or reviewing unsafe code itself; use `rust-unsafe`. Triggers on "miri", "MIRIFLAGS", "sanitizer", "-Zsanitizer", "TSan", "SEGV_MTESERR", "SEGV_MTEAERR".

The file

Show its rules and text

We split this skill into the rules it gives an agent

24 rules. About 0 are common, 10 are distinctive, and 14 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈10

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 14

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read references/miri-ub-patterns.md ("Coverage decision table", "Sanitizer comparison") when you decide which tool runs on each crate of a …

    line 27
  2. Very specific:

    Never silence that error with -Cunsafe-allow-abi-mismatch=sanitizer.

    line 110
  3. Very specific:

    Keep secrets out of any Miri job that writes a target/ cache a pull request can read.

    line 136
  4. Very specific:

    Keep the Miri test set small and deterministic.

    line 144
  5. Very specific:

    Read references/miri-ub-patterns.md when a Miri run fails and you need the shape of the defect and its fix.

    line 162
  6. Distinctive:

    Run the default model, Stacked Borrows (SB), first.

    line 173
  7. Distinctive:

    Add a Tree Borrows (TB) run as more evidence on a crate with hand-written raw-pointer code.

    line 174
  8. Distinctive:

    Treat a failure under either model as a finding.

    line 178
  9. Distinctive:

    Run Miri over the Rust logic while the foreign call is replaced or skipped.

    line 185
  10. Distinctive:

    If the real function returns a pointer that the caller dereferences, the stub returns a pointer into a real allocation.

    line 214
  11. Distinctive:

    If the real function stores a pointer, the stub stores it too, and the stub of each later call that uses it dereferences it.

    line 217
  12. Very specific:

    Do not let a stub hide the UB that you want to find.

    line 220
  13. Very specific:

    Keep the stub next to the real declaration, in the same module, so it cannot drift from the signature.

    line 224
  14. Very specific:

    Put a comment with the reason on each #[cfg_attr(miri, ignore)], and name the job that covers the path instead.

    line 231
  15. Distinctive:

    Every path that a stub, a skip or an exclusion removes from Miri needs an ASan run on the host, or a HWASan or MTE run on a device.

    line 242
  16. Very specific:

    Read references/miri-ub-patterns.md ("Box plus FFI aliasing") when you write these stubs for a registration API.

    line 258
  17. Very specific:

    Use Pin<Box<T>> only when T has a pinning invariant.

    line 270
  18. Very specific:

    Read references/miri-flags-and-ci.md when you need another flag, for example -Zmiri-seed=N to reproduce one failing seed.

    line 311
  19. Very specific:

    Read frame #0. It names the access, not always the defect.

    line 327
  20. Distinctive:

    Read the allocation and free stacks that ASan prints below the access.

    line 328
  21. Distinctive:

    If the path is pure Rust, write a Miri test for it.

    line 330
  22. Very specific:

    Read references/platform-sanitizers.md when you build for a device.

    line 340
  23. Very specific:

    Read references/miri-flags-and-ci.md when you write the CI jobs.

    line 346
  24. Distinctive:

    Treat a Miri failure as a build failure.

    line 349

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Rust Sanitizers and Miri

1. Select the tool

Select the tool from the bug class. Each tool finds its own class and misses the others.

Bug classToolDoes not find it
Aliasing breach, invalid value, provenance error in RustMiriASan, HWASan, MTE: they do not model Rust rules
Heap overflow, use-after-free, double free at runtimeASan, HWASan or MTEMiri, when the path reaches foreign code
Data race between threadsTSan, or Miri with -Zmiri-many-seedsASan
Read of uninitialized memoryMiri, or MSan with every object instrumentedASan
Integer overflow or null dereference in a C or C++ dependencyUBSan: clang -fsanitize=undefined on that dependencyMiri (does not run C); rustc (has no UBSan option)
Memory error inside a C or C++ dependencyASan, HWASan or MTEMiri

Miri knows the Rust rules but cannot execute foreign code. Sanitizers execute foreign code but do not know the Rust aliasing model. For a crate that has unsafe code and FFI, run both.

Read references/miri-ub-patterns.md ("Coverage decision table", "Sanitizer comparison") when you decide which tool runs on each crate of a workspace, or when you need overhead figures.

A green run does not prove the absence of undefined behaviour (UB). Every tool checks only the paths that the tests execute. A clean Miri run says nothing about a stubbed or skipped path. A clean sanitizer run says nothing about uninstrumented code or about the Rust aliasing rules.

2. Review gates

Apply these gates before you approve a change that adds or edits unsafe.

  • A test exercises the new unsafe path.
  • cargo +nightly miri test --locked passes on that test, or the test has #[cfg_attr(miri, ignore)] and a comment that gives the reason.
  • The strict-provenance job passes, or the code exposes provenance on purpose and runs in the separate job (section 9).
  • Raw-pointer code also passes under -Zmiri-tree-borrows.
  • Byte-parsing code passes under -Zmiri-symbolic-alignment-check.
  • Each new #[cfg(miri)] stub keeps the signature, the return domain and any stored pointer of the real function.
  • An ASan, HWASan or MTE run covers every path that Miri skips.
  • New concurrency is covered by TSan, by Miri with -Zmiri-many-seeds, or by loom.

3. Failure triage

Miri messages below were measured on nightly 2026-05-15. Match on the quoted fragment. Tags and allocation ids change with the program, the toolchain, the seed and the flags.

SymptomProbable causeNext action
Miri: unsupported operation: can't call foreign functionThe test reaches code that Miri cannot interpretApply section 7
Miri: not available when isolation is enabledThe test reads a host resource, for example SystemTime::now or a fileAdd -Zmiri-disable-isolation to that job, or inject the value in the test
Miri: allocN has been freed, so this pointer is danglingA pointer kept across a reallocation or a dropRe-derive the pointer after each operation that can reallocate
Miri: in-bounds pointer arithmetic failedadd or offset past the end of the allocationCheck the length before the arithmetic
Miri: constructing invalid value ... expected a valid enum tag or expected a booleanA transmute or read produced a value that no variant or bool usesDecode with TryFrom and return an error
Miri: accessing memory based on pointer with alignment ... is required, only on some seedsA misaligned read through a raw pointerUse read_unaligned or from_le_bytes; add -Zmiri-symbolic-alignment-check
Miri: memory is uninitialized or encountered uninitialized memoryA read of MaybeUninit before every byte was writtenWrite every byte first, or use a zero-filled buffer
Miri: does not exist in the borrow stack (SB) or is forbidden (TB)An aliasing violationSection 6, section 8, and references/miri-ub-patterns.md
Miri: integer-to-pointer casts and `ptr::with_exposed_provenance` are not supportedThe code rebuilds a pointer from an integer under -Zmiri-strict-provenanceKeep the original pointer, or use with_addr or map_addr. If exposure is required, move the test to the separate job
Miri: dangling pointer (it has no provenance)A pointer made from an integer that was never exposedKeep the original pointer, or use with_addr
Miri: memory leaked: allocNAn allocation that no static can reach: Box::leak, mem::forget, or a pointer held only by a thread still running at exit or by foreign stateStore an intended process-lifetime value in a static (OnceLock or LazyLock), which Miri does not report, or add -Zmiri-ignore-leaks to that job only
Miri passes, ASan failsThe defect is in foreign code that Miri stubbed or skippedDebug with ASan and read the allocation stack
ASan passes, Miri failsAn aliasing or provenance breach that did not corrupt memory on this runFix it. It is UB, and the optimizer can act on it later
MSan reports uninitialized reads in a dependencyNot every object was built with MSanRebuild every dependency with MSan, or use Miri
TSan reports a race inside an atomics-based structureAn ordering is too weak, or the code synchronizes with a fence that TSan does not modelConfirm with Miri -Zmiri-many-seeds or loom before you change an ordering; see the memory-model skill
mixing `-Zsanitizer` will cause an ABI mismatch in crateA crate or std was built without the sanitizerAdd -Zbuild-std; for doctests, set RUSTDOCFLAGS to the RUSTFLAGS value
Doctests fail to link with undefined __asan_* symbolsRUSTDOCFLAGS lacks the -Zsanitizer flagSet RUSTDOCFLAGS to the RUSTFLAGS value
rustc aborts while it expands a proc macro under a sanitizer--target is missing, so the proc macro was instrumentedPass --target "$HOST"

4. Run a sanitizer on the host

-Zsanitizer is unstable as of Rust 1.98.1, so every sanitizer run needs nightly.

rustup toolchain install nightly --component rust-src
HOST="$(rustc +nightly -vV | sed -n 's/^host: //p')"

# AddressSanitizer (Linux, macOS)
RUSTFLAGS="-Zsanitizer=address" RUSTDOCFLAGS="-Zsanitizer=address" \
    cargo +nightly test --locked -Zbuild-std --target "$HOST"

# ThreadSanitizer (Linux, macOS)
RUSTFLAGS="-Zsanitizer=thread" RUSTDOCFLAGS="-Zsanitizer=thread" \
    cargo +nightly test --locked -Zbuild-std --target "$HOST"

# MemorySanitizer (Linux; every linked object must be instrumented)
RUSTFLAGS="-Zsanitizer=memory -Zsanitizer-memory-track-origins" \
RUSTDOCFLAGS="-Zsanitizer=memory -Zsanitizer-memory-track-origins" \
    cargo +nightly test --locked -Zbuild-std --target "$HOST"

Rules (build failures and ASan defaults measured on nightly 2026-05-15, aarch64-apple-darwin):

  • Pass -Zbuild-std. The prebuilt standard library is not instrumented. Without the rebuild, TSan stops with mixing `-Zsanitizer` will cause an ABI mismatch in crate, ASan leaves std unchecked, and MSan reports false positives.
  • Never silence that error with -Cunsafe-allow-abi-mismatch=sanitizer. The mixed build misses errors in the uninstrumented crates and can report false positives. Fix the flags so every crate is instrumented.
  • Pass --target, also for the host. Then Cargo keeps RUSTFLAGS off build scripts and proc macros. An instrumented proc macro can abort rustc.
  • Set RUSTDOCFLAGS to the same -Zsanitizer value, or every doctest fails to build (section 3).
  • On macOS, set ASAN_OPTIONS=detect_stack_use_after_return=1:detect_leaks=1. Without it, ASan misses stack-use-after-return and does not check leaks. Linux enables both by default, so an intentional leak fails a Linux ASan run. Set detect_leaks=0 only for that job.
  • Build every C and C++ dependency with clang -fsanitize=memory for an MSan run. Otherwise do not trust an MSan report.
  • Expect TSan false positives on std::sync::atomic::fence and on synchronization in inline assembly. TSan does not model either one.

5. Run Miri

rustup +nightly component add miri
cargo +nightly miri test --locked                  # whole suite
cargo +nightly miri test --locked <test_filter>    # one test

Nightly and cache rules:

  • Keep secrets out of any Miri job that writes a target/ cache a pull request can read. Miri nightlies before 2026-09-22 wrote every environment variable into target/. If such a job ran with secrets, clear the cache and rotate the secrets (Rust blog, 2026-09-21).
  • Pin a nightly date if a Miri regression blocks the pipeline. Pin 2026-09-22 or later. Miri diagnostics change with the nightly.

Miri interprets every operation, so it runs much slower than a native test. Keep the Miri test set small and deterministic. Gate a large-input or long-running test with #[cfg_attr(miri, ignore)], or shrink its input under cfg(miri).

Miri also reports data races and leaks at exit. It reports an invalid value where the code produces it, at the transmute or read, not at a later use. It emulates some weak-memory effects, so an atomic load can return an outdated value, but that emulation is not complete.

Miri cannot execute a foreign function that has no Miri shim. This includes every C or C++ entry point behind a -sys crate, JNI calls, generated UniFFI scaffolding, and inline assembly. Miri shims a subset of the platform API, including files (with -Zmiri-disable-isolation), threads, and epoll and eventfd on Linux targets. System API support varies between targets. If a shim is missing, try --target x86_64-unknown-linux-gnu before you stub. -Zmiri-native-lib calls real native code, but it is experimental and unsound. A run through it is not evidence for the FFI path.

Read references/miri-ub-patterns.md when a Miri run fails and you need the shape of the defect and its fix. It shows each UB class with the message Miri prints.

6. Aliasing model policy

cargo +nightly miri test --locked                                  # Stacked Borrows (default)
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test --locked  # Tree Borrows
  • Run the default model, Stacked Borrows (SB), first. The SB run is the gate.
  • Add a Tree Borrows (TB) run as more evidence on a crate with hand-written raw-pointer code. Never make TB the only gate. The Miri README calls TB "even more experimental than Stacked Borrows", and code it accepts today "might be declared UB in the future".
  • Treat a failure under either model as a finding.
  • Fix an SB failure even when TB passes. TB accepts some patterns that SB rejects, for example a &mut reborrow that is written after a read through its parent raw pointer.

7. Stub an FFI dependency that Miri cannot execute

Run Miri over the Rust logic while the foreign call is replaced or skipped. Select the strategy from the shape of the dependency.

FFI situationStrategy
extern "C" block that you declare in your own crateGate the block with #[cfg(not(miri))]. Add a #[cfg(miri)] stub with the same signature.
Inline asm! or global_asm!Add a pure-Rust fallback behind #[cfg(miri)]. Keep both paths under one test.
Third-party crate that links C or C++You cannot stub it. Exclude the crate from the Miri run, or gate the tests that reach it.
Generated FFI scaffolding, for example a UniFFI or JNI binding layerDo not stub the generated code. Gate the tests that cross the boundary.
Test that needs a live host runtime, for example a JVM, a GPU driver or a databaseGate the test with #[cfg_attr(miri, ignore)]. Do not stub.
Platform syscall through libc with a simple return valueStub it behind #[cfg(miri)] and return the success value.

Stub a foreign function that you declare

#[cfg(not(miri))]
unsafe extern "C" {
    fn platform_specific_call(fd: i32) -> i32;
}

#[cfg(miri)]
unsafe fn platform_specific_call(_fd: i32) -> i32 {
    0 // Deterministic success value for the interpreter.
}

The stub must keep the signature, the safety contract and the return domain of the real function:

  • If the real function returns a pointer that the caller dereferences, the stub returns a pointer into a real allocation. A null stub moves the defect instead of removing it.
  • If the real function stores a pointer, the stub stores it too, and the stub of each later call that uses it dereferences it. Otherwise Miri cannot see an alias against the stored pointer (section 8).
  • Do not let a stub hide the UB that you want to find. A stub that always returns 0 for a function whose error path frees a buffer removes the test you need.

Keep the stub next to the real declaration, in the same module, so it cannot drift from the signature. Use the built-in cfg(miri), not a miri Cargo feature: Miri sets the cfg itself, and a feature can be enabled by accident in a normal build.

Skip a test or exclude a crate

Put a comment with the reason on each #[cfg_attr(miri, ignore)], and name the job that covers the path instead.

# Select the crates that Miri can interpret.
cargo +nightly miri test --locked -p my-core -p my-parser

# Or run the workspace and exclude the crates that reach foreign code.
cargo +nightly miri test --locked --workspace --exclude my-ffi

Every path that a stub, a skip or an exclusion removes from Miri needs an ASan run on the host, or a HWASan or MTE run on a device. A cargo +nightly careful test run without -Zcareful-sanitizer is not a substitute: it adds std debug assertions, not memory-error detection.

8. A Box and a pointer that foreign code keeps

A Box<T> asserts unique access while it is live. If foreign code stores a *mut T taken from the Box, and Rust then writes through the Box, the stored pointer is invalid. A later foreign access (read or write) through it is UB.

Miri sees this only when Miri-executed code uses the stored pointer after the Box access. Make the #[cfg(miri)] stubs keep the pointer and read through it on a later call, as the foreign side does. Then both SB and TB report the read. With a stub that ignores the pointer, neither model reports anything. Read references/miri-ub-patterns.md ("Box plus FFI aliasing") when you write these stubs for a registration API. It has the full stubs and test.

Correct patterns:

  • Transfer ownership with Box::into_raw and never use the original Box again. Access the value only through the raw pointer. Recover it with Box::from_raw exactly once, after the foreign side unregisters it and no foreign call or callback can still use the pointer. An unregister call can return while a callback on a foreign thread still runs.
  • For a synchronous foreign borrow, do not access or reborrow the value until the call returns.
  • Use Pin<Box<T>> only when T has a pinning invariant. It keeps the address stable. It does not permit aliasing or concurrent foreign access.

9. Miri jobs and flag hazards

# Gate: default model and strict provenance.
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test --locked

# Byte parsers and raw-pointer crates: symbolic alignment, then Tree Borrows.
MIRIFLAGS="-Zmiri-strict-provenance -Zmiri-symbolic-alignment-check" \
    cargo +nightly miri test --locked -p my-parser
MIRIFLAGS="-Zmiri-tree-borrows -Zmiri-strict-provenance" \
    cargo +nightly miri test --locked -p my-parser

# Concurrency: many schedules for the threaded tests.
MIRIFLAGS="-Zmiri-many-seeds=0..16" cargo +nightly miri test --locked <test_filter>

# Endianness: interpret on a big-endian target.
cargo +nightly miri test --locked -p my-parser --target s390x-unknown-linux-gnu

Flag hazards:

  • A misaligned read passes default Miri on some seeds and fails on others. Add -Zmiri-symbolic-alignment-check to make the failure certain. It gives false positives when code aligns a pointer with manual integer arithmetic; align_to is fine in both modes.
  • -Zmiri-strict-provenance rejects every deliberate exposure, including with_exposed_provenance. Run exposing code in a separate job without the flag. No cfg reports the flag, so cfg_attr(miri, ignore) cannot select these tests. Keep them out of the gate with -- --skip <name> or by crate (--exclude). -Zmiri-permissive-provenance only silences the warning. Miri can miss bugs on exposed pointers either way.
  • Miri reports 1 CPU by default, so cargo miri test runs one test at a time and does not detect a race between two tests on a shared resource (measured on nightly 2026-05-15). Add -Zmiri-num-cpus=4 or -- --test-threads=4 for that job. RUST_TEST_THREADS does not reach the program under isolation. cargo miri nextest run runs each test in its own process and never detects such a race.

Read references/miri-flags-and-ci.md when you need another flag, for example -Zmiri-seed=N to reproduce one failing seed. It has the full MIRIFLAGS table.

10. Read an ASan report

ASan errorLikely Rust cause
heap-buffer-overflowunsafe slice or pointer access past the end of a buffer
heap-use-after-freeRaw pointer kept across a Vec or String reallocation, or used after drop()
stack-use-after-returnRaw pointer to a local that escaped its function
double-freeBox::from_raw called twice on one pointer, or ownership passed to FFI and also dropped in Rust
alloc-dealloc-mismatchC++ new memory freed with free, or the reverse, across the FFI boundary

Triage order:

  1. Read frame #0. It names the access, not always the defect.
  2. Read the allocation and free stacks that ASan prints below the access.
  3. Find the unsafe block on the path between them. That block owns the bug.
  4. If the path is pure Rust, write a Miri test for it. Miri names aliasing and provenance defects more exactly than ASan.

11. On-device sanitizers: Android and iOS

On Android arm64, use HWASan or MTE. Xcode instruments only the code that Xcode compiles. A prebuilt Rust static library gets no ASan or TSan instrumentation from the scheme setting. Run a host sanitizer build for the Rust code itself.

Read references/platform-sanitizers.md when you build for a device. It has the platform requirements, the HWASan build and wrap.sh, the MTE manifest modes and stack-MTE rebuild, the tombstone check, and the Xcode commands.

12. CI integration

Read references/miri-flags-and-ci.md when you write the CI jobs. It has a GitHub Actions example and the job layout rules.

Treat a Miri failure as a build failure. Miri reports UB, not style.

Related skills

  • rust-unsafe: unsafe patterns and the review checklist for unsafe
  • memory-model: atomics, memory ordering, loom
  • rust-debugging: symbolication and addr2line for sanitizer and tombstone frames
  • rust-test-tools: property tests and fuzzing that feed these tools
  • rust-jni, uniffi-boundary: FFI layers that Miri cannot execute
  • rust-android-build: Android target setup for the device builds

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.