Back to Oh My Openagent

Miri, Sanitizers, Loom, and Fuzzing — The UB Detection Arsenal

packages/omo-codex/plugin/skills/programming/references/rust-ub/miri-sanitizers-loom.md

4.5.1211.4 KB
Original Source

Miri, Sanitizers, Loom, and Fuzzing — The UB Detection Arsenal

Miri is the primary weapon. Everything else is supplementary for the gaps Miri cannot reach.


Miri — The First and Last Line of Defense

What Miri Is

Miri is an interpreter for Rust's MIR (Mid-level IR). It executes your test suite inside a virtual machine that tracks every byte of memory for validity, provenance, alignment, initialization, and aliasing. It is deterministic — same inputs, same result — and it can find UB that no amount of testing on real hardware will ever trigger.

Why Miri Is Non-Negotiable

  • Detects 12 of 14 UB categories (see ub-taxonomy.md).
  • Catches aliasing violations that compile and run correctly on every platform today but are UB that future compiler optimizations will exploit.
  • Catches data races under a configurable scheduling model.
  • Catches provenance violations that are impossible to observe on real hardware.
  • Zero false positives — if Miri says it is UB, it is UB. Period.

Installation

bash
rustup install nightly
rustup component add miri rust-src --toolchain nightly

Verify:

bash
cargo +nightly miri --version

Running Miri

Default run (Stacked Borrows, standard checks):

bash
cargo +nightly miri test

With nextest (recommended for projects already using nextest):

bash
cargo +nightly miri nextest run

Specific test:

bash
cargo +nightly miri test -- test_name

Run a binary:

bash
cargo +nightly miri run

MIRIFLAGS — The Dial-Up Knobs

These flags are set via the MIRIFLAGS environment variable. The agent should use ALL of the strictness flags during a UB audit.

Aliasing Model

bash
# Default: Stacked Borrows (strict)
cargo +nightly miri test

# Tree Borrows (newer, more permissive — use as a second pass)
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test

Protocol: Run Stacked Borrows first. If it fails, fix it. Then run Tree Borrows to confirm. Code that passes Stacked Borrows is sound under both models.

Strict Provenance

bash
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test

Catches ptr as usize as *const T roundtrips where provenance is lost. Should be ON for every audit.

Symbolic Alignment Checks

bash
MIRIFLAGS="-Zmiri-symbolic-alignment-check" cargo +nightly miri test

Catches alignment UB that happens to be aligned on your machine but is not guaranteed by the type system.

Data Race Detection Tuning

bash
# Increase preemption rate to stress-test race conditions
MIRIFLAGS="-Zmiri-preemption-rate=0.5" cargo +nightly miri test

# Disable preemption (sequential scheduling — fewer races found but deterministic)
MIRIFLAGS="-Zmiri-preemption-rate=0" cargo +nightly miri test

The Full Paranoia Sweep (Use This for Audits)

bash
MIRIFLAGS="\
  -Zmiri-strict-provenance \
  -Zmiri-symbolic-alignment-check \
  -Zmiri-preemption-rate=0.1 \
  -Zmiri-backtrace=full \
  -Zmiri-disable-isolation" \
cargo +nightly miri test

Then a second pass with Tree Borrows:

bash
MIRIFLAGS="\
  -Zmiri-tree-borrows \
  -Zmiri-strict-provenance \
  -Zmiri-symbolic-alignment-check \
  -Zmiri-preemption-rate=0.1 \
  -Zmiri-backtrace=full \
  -Zmiri-disable-isolation" \
cargo +nightly miri test

Isolation and I/O

Miri runs in isolation by default — no file I/O, no network, no system calls. If your tests need the filesystem:

bash
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test

Use sparingly — isolation is a feature, not a limitation. Tests that need I/O should have a separate #[cfg(not(miri))] path.

Miri Limitations

Cannot doWorkaround
Execute FFI / C codeASAN, MSAN, Valgrind
Run I/O-heavy tests (default)-Zmiri-disable-isolation or #[cfg(not(miri))]
Exhaustive interleaving explorationloom
Find performance bugscriterion, flamegraph
Run inline assemblyskip with #[cfg(not(miri))]
Test OS-specific behaviorreal hardware + sanitizers

Miri in CI

yaml
# GitHub Actions example
miri:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: dtolnay/rust-toolchain@nightly
      with:
        components: miri, rust-src
    - name: Miri test (Stacked Borrows + strict provenance)
      run: |
        MIRIFLAGS="-Zmiri-strict-provenance -Zmiri-symbolic-alignment-check -Zmiri-backtrace=full" \
        cargo +nightly miri test
    - name: Miri test (Tree Borrows)
      run: |
        MIRIFLAGS="-Zmiri-tree-borrows -Zmiri-strict-provenance -Zmiri-symbolic-alignment-check -Zmiri-backtrace=full" \
        cargo +nightly miri test

Miri-Incompatible Test Gating

rust
#[test]
#[cfg_attr(miri, ignore)]  // Miri cannot run this (FFI, I/O, inline asm)
fn test_requires_real_hardware() {
    // ...
}

// Or conditionally compile the test body:
#[test]
fn test_with_miri_fallback() {
    #[cfg(miri)]
    {
        // Simplified version that avoids FFI
    }
    #[cfg(not(miri))]
    {
        // Full version with FFI
    }
}

Sanitizers — Where Miri Cannot Reach

Sanitizers are compiler instrumentation passes. They run your actual binary on real hardware with extra checks injected. Use them for FFI, I/O-heavy code, and integration tests.

AddressSanitizer (ASAN)

Detects: use-after-free, buffer overflow, stack-use-after-return, double-free, memory leaks.

bash
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

On macOS:

bash
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target aarch64-apple-darwin

ThreadSanitizer (TSAN)

Detects: data races on non-atomic accesses across threads.

bash
RUSTFLAGS="-Zsanitizer=thread" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

When to use over Miri: Integration tests involving real threads + real I/O + FFI. Miri's data-race detector is superior for pure-Rust code.

MemorySanitizer (MSAN)

Detects: reads of uninitialized memory.

bash
RUSTFLAGS="-Zsanitizer=memory -Zsanitizer-memory-track-origins" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

When to use over Miri: FFI code where C/C++ may return uninitialized memory into Rust.

UndefinedBehaviorSanitizer (UBSAN)

Detects: integer overflow, misaligned access, null dereference, and other C/C++-style UB at the LLVM level.

bash
RUSTFLAGS="-Zsanitizer=undefined" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

Sanitizer Limitations

  • Require nightly + -Zbuild-std (rebuilds the standard library with instrumentation).
  • MSAN requires ALL dependencies (including C libs) to be instrumented — practically hard.
  • Cannot catch aliasing violations (that is Miri's domain).
  • Significant runtime overhead (2-15x slower).
  • Linux has the best support; macOS works for ASAN; Windows support is minimal.

Loom — Exhaustive Concurrency Testing

Loom explores all possible thread interleavings of a bounded concurrent program. It is mandatory for lock-free and wait-free primitives.

When to Use Loom

  • Any unsafe code involving atomics with ordering weaker than SeqCst.
  • Custom lock implementations.
  • Lock-free queues, stacks, or other concurrent data structures.
  • Any code where you chose Relaxed, Acquire, or Release ordering.

When NOT to Use Loom

  • Code using only Mutex/RwLock from std or parking_lot — the locks are sound, your usage is the question, and Miri + TSAN cover that.
  • Async code (loom does not model async runtimes — use tokio::test + Miri instead).

Setup

toml
[dev-dependencies]
loom = "0.7"

Loom Test Pattern

rust
#[cfg(loom)]
mod loom_tests {
    use loom::sync::atomic::{AtomicUsize, Ordering};
    use loom::sync::Arc;
    use loom::thread;

    #[test]
    fn concurrent_increment_is_sound() {
        loom::model(|| {
            let counter = Arc::new(AtomicUsize::new(0));

            let threads: Vec<_> = (0..2).map(|_| {
                let c = counter.clone();
                thread::spawn(move || {
                    c.fetch_add(1, Ordering::SeqCst);
                })
            }).collect();

            for t in threads {
                t.join().unwrap();
            }

            assert_eq!(counter.load(Ordering::SeqCst), 2);
        });
    }
}

Conditional Compilation for Loom

rust
#[cfg(loom)]
use loom::sync::atomic::{AtomicUsize, Ordering};
#[cfg(not(loom))]
use std::sync::atomic::{AtomicUsize, Ordering};

Running Loom Tests

bash
# Loom tests only (use cfg flag)
RUSTFLAGS="--cfg loom" cargo test --lib -- loom_tests

# With release optimizations (loom is slow)
RUSTFLAGS="--cfg loom" cargo test --lib --release -- loom_tests

Loom + Miri Interaction

Loom and Miri solve different problems:

  • Miri checks a single execution for UB (aliasing, validity, provenance).
  • Loom checks all interleavings for correctness (ordering, atomicity).

Run BOTH on lock-free code:

bash
# Step 1: loom for interleaving correctness
RUSTFLAGS="--cfg loom" cargo test --lib --release -- loom_tests

# Step 2: Miri for UB in each path
cargo +nightly miri test -- concurrent_tests

Cargo-Fuzz — Property-Based UB Hunting

Fuzzing generates random inputs to maximize code coverage and find crashes, panics, and UB.

Setup

bash
cargo install cargo-fuzz
cargo fuzz init

Fuzz Target

rust
// fuzz/fuzz_targets/parse_input.rs
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    // Your parsing/deserialization/processing code here.
    // If it panics or triggers UB, the fuzzer catches it.
    let _ = my_crate::parse(data);
});

Running

bash
# Run until interrupted
cargo +nightly fuzz run parse_input

# Run with ASAN (catches memory bugs in unsafe code)
cargo +nightly fuzz run parse_input -- -rss_limit_mb=4096

# Minimize a crashing input
cargo +nightly fuzz tmin parse_input artifacts/parse_input/crash-xxxxx

Fuzz + Miri Pipeline

When the fuzzer finds a crashing input:

  1. Minimize it with cargo fuzz tmin.
  2. Add it as a regression test.
  3. Run the regression test under Miri to classify whether it is a panic (safe) or UB (must fix).
bash
# After adding the input as a test case:
cargo +nightly miri test -- test_fuzz_regression_001

Tool Selection Decision Tree

Start
  │
  ├── Is it pure Rust (no FFI, no I/O)?
  │     YES → Miri (full paranoia flags)
  │     │     └── Also: loom (if atomics/lock-free)
  │     │     └── Also: proptest (if parsing/serialization)
  │     │     └── Also: cargo-fuzz (if untrusted input)
  │     │
  │     NO → Does it involve FFI?
  │           YES → ASAN + MSAN on integration tests
  │           │     └── Miri on the Rust-side handling
  │           │     └── cbindgen in CI for layout verification
  │           │
  │           NO → Is it I/O-heavy?
  │                 YES → TSAN for thread safety
  │                 │     └── Miri with -Zmiri-disable-isolation where possible
  │                 │
  │                 NO → Miri (full paranoia flags)
  │
  └── Always: Miri is the default. Other tools supplement.

The One Rule

When in doubt, run Miri. If Miri cannot run it, write a version it can run, and test that under Miri. Then test the real version under sanitizers. Never ship unsafe code that has not passed Miri.