packages/omo-codex/plugin/skills/programming/references/rust-ub/miri-sanitizers-loom.md
Miri is the primary weapon. Everything else is supplementary for the gaps Miri cannot reach.
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.
ub-taxonomy.md).rustup install nightly
rustup component add miri rust-src --toolchain nightly
Verify:
cargo +nightly miri --version
Default run (Stacked Borrows, standard checks):
cargo +nightly miri test
With nextest (recommended for projects already using nextest):
cargo +nightly miri nextest run
Specific test:
cargo +nightly miri test -- test_name
Run a binary:
cargo +nightly miri run
These flags are set via the MIRIFLAGS environment variable. The agent should use ALL of the strictness flags during a UB audit.
# 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.
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.
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.
# 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
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:
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
Miri runs in isolation by default — no file I/O, no network, no system calls. If your tests need the filesystem:
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.
| Cannot do | Workaround |
|---|---|
| Execute FFI / C code | ASAN, MSAN, Valgrind |
| Run I/O-heavy tests (default) | -Zmiri-disable-isolation or #[cfg(not(miri))] |
| Exhaustive interleaving exploration | loom |
| Find performance bugs | criterion, flamegraph |
| Run inline assembly | skip with #[cfg(not(miri))] |
| Test OS-specific behavior | real hardware + sanitizers |
# 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
#[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 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.
Detects: use-after-free, buffer overflow, stack-use-after-return, double-free, memory leaks.
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
On macOS:
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target aarch64-apple-darwin
Detects: data races on non-atomic accesses across threads.
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.
Detects: reads of uninitialized memory.
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.
Detects: integer overflow, misaligned access, null dereference, and other C/C++-style UB at the LLVM level.
RUSTFLAGS="-Zsanitizer=undefined" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu
-Zbuild-std (rebuilds the standard library with instrumentation).Loom explores all possible thread interleavings of a bounded concurrent program. It is mandatory for lock-free and wait-free primitives.
unsafe code involving atomics with ordering weaker than SeqCst.Relaxed, Acquire, or Release ordering.Mutex/RwLock from std or parking_lot — the locks are sound, your usage is the question, and Miri + TSAN cover that.tokio::test + Miri instead).[dev-dependencies]
loom = "0.7"
#[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);
});
}
}
#[cfg(loom)]
use loom::sync::atomic::{AtomicUsize, Ordering};
#[cfg(not(loom))]
use std::sync::atomic::{AtomicUsize, Ordering};
# 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 and Miri solve different problems:
Run BOTH on lock-free code:
# 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
Fuzzing generates random inputs to maximize code coverage and find crashes, panics, and UB.
cargo install cargo-fuzz
cargo fuzz init
// 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);
});
# 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
When the fuzzer finds a crashing input:
cargo fuzz tmin.# After adding the input as a test case:
cargo +nightly miri test -- test_fuzz_regression_001
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.
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
unsafecode that has not passed Miri.