412 lines
11 KiB
Markdown
412 lines
11 KiB
Markdown
|
|
# 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 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 |
|
||
|
|
|
||
|
|
### 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.
|