Rust and Miri: Beyond the Compiler for Memory Safety

In Rust, the compiler is your first line of defense. Ownership, borrowing, and lifetimes prevent most memory errors at compile time. But some errors can still slip through the cracks, especially when using unsafe or raw pointers. This is where Miri comes in.

The Rust Compiler Is Not Always Enough

Rust is renowned for its memory safety, and rightly so. The type system, ownership rules, and borrow checker form a remarkably effective safety net. However, as soon as you use the unsafe keyword, you enter territory where the compiler can no longer verify everything.

unsafe blocks allow operations that the compiler cannot prove to be safe: dereferencing raw pointers, calling C functions through FFI, direct memory manipulation. These operations are sometimes essential in systems code or low-level libraries, but they open the door to Undefined Behavior (UB).

The problem: rustc will compile your code without complaint, even if undefined behavior lurks within.

Miri: An Interpreter for Tracking Undefined Behavior

Miri is a Rust interpreter that executes an internal compiler representation called MIR (Mid-level Intermediate Representation). This is the same representation that the compiler uses for the borrow checker and optimizations.

Instead of running your program natively, Miri simulates it instruction by instruction, checking at each step that memory rules are respected. It can detect UB that the compiler cannot verify statically:

A Concrete Example

Consider this Rust code:

let s = String::from("hello");
let r: *const String = &s;
unsafe {
    drop(s);           // Rust does not see that r becomes dangling
    println!("{}", &*r); // UB: use-after-free
}

Here, we create a String, take a raw pointer to it, then destroy it with drop. The pointer r becomes a dangling pointer. When we try to dereference it, we access freed memory.

When to Use Miri

Miri is particularly useful in the following situations:

To use it, simply install the component via rustup and run:

cargo miri run
# or for tests
cargo miri test

Limitations of Miri

Miri is not a universal solution. It cannot execute real system calls or FFI code calling into C. Its execution is also significantly slower than native execution, since it simulates each instruction. It is therefore complementary to standard tests, not a replacement.

Conclusion

Even in a memory-safe language like Rust, dynamic analysis with Miri is essential whenever unsafe is used. The compiler guarantees the safety of safe code, but Miri extends that guarantee to areas where the compiler cannot reach. For any serious project or library that touches unsafe code, Miri represents an indispensable verification layer that catches bugs before they reach production.