In this lesson, you will learn how to use the Miri interpreter to detect subtle race conditions and undefined behavior in your concurrent Rust code.
When you write parallel algorithms—especially those involving UnsafeCell or custom synchronization—it is incredibly easy to introduce data races that pass standard unit tests but cause non-deterministic crashes in production. Miri is a tool that runs your code in an isolated, instrumented environment to catch these errors before they ever hit the CPU.
How Miri Works
Miri works by executing your code as an interpreter rather than compiling it to machine code. Because Miri "sees" every memory access, it can track the state of every byte. If two threads attempt to access the same memory location where at least one access is a write, and there is no proper synchronization between them, Miri will stop execution and report a data race.
Detecting a Data Race
Consider this naive attempt at shared state that lacks proper synchronization:
use std::thread;
fn main() {
let mut data = 0;
// SAFETY: This is a data race waiting to happen!
let ptr = &mut data as *mut i32;
unsafe {
thread::spawn(move || {
*ptr += 1;
});
}
}
If you run this with cargo miri run, Miri will immediately flag the memory violation:
error: Undefined Behavior: data race detected between ...
Running Miri
To use Miri, you first install it via rustup:
rustup component add miri
Then, execute your test suite or binary:
cargo miri test
Miri doesn’t just check for data races; it also validates that your unsafe code adheres to Rust’s strict aliasing rules. If you perform an illegal pointer dereference or violate the rules of Send or Sync while implementing custom synchronization, Miri will identify the exact line where your safety guarantees were broken. Because it explores different execution orderings, it acts as a "fuzzer" for your concurrent logic.
Key Takeaway: Miri turns non-deterministic concurrency bugs into deterministic errors by verifying memory safety and identifying data races during execution.