Skip to content
rusthacks.dev
Systems

Using Miri to Catch Undefined Behavior in Unsafe Rust

Miri interprets your tests and reports undefined behavior in unsafe code: out of bounds access, use after free, bad aliasing and uninitialized reads. Here is how to run and read it.

2 min read
Summarize in AI
Flat illustration for the post “Using Miri to Catch Undefined Behavior in Unsafe Rust”, composed around unsafe.

You wrote an unsafe block, the tests pass and you are unsure. Passing tests mean little here, because undefined behavior often looks like working code. Miri runs the same tests in an interpreter that tracks every pointer and every byte, and stops at the first rule violation. It is the cheapest way to turn "I think this is sound" into "this particular run found nothing wrong". It is the checking step in unsafe Rust explained.

Rust nightly, Miri bundled with the component.

Install and run

Shell
rustup +nightly component add miri
cargo +nightly miri test

The first run builds a special sysroot, which takes a minute. After that, each test is interpreted.

A bug it finds

rust
pub fn dangling() -> i32 {
    let v = vec![1, 2, 3];
    let p = v.as_ptr();
    drop(v);
    unsafe { *p }
}

#[test]
fn reads_freed_memory() {
    dangling();
}

Native cargo test usually passes, because the freed memory still holds the old bytes. Miri stops:

Text
error: Undefined Behavior: memory access failed: alloc1234 has been freed, so this pointer is dangling
  --> src/lib.rs:5:14
   |
5  |     unsafe { *p }
   |              ^^ memory access failed: alloc1234 has been freed, so this pointer is dangling

The report shows where the allocation was made and where it was freed, which usually answers the question without a debugger.

Aliasing violations

rust
let mut x = 1;
let p = &mut x as *mut i32;
let r = &mut x;
unsafe { *p = 2 };
*r = 3;

Miri's Stacked Borrows model reports that the pointer p was invalidated when r was created from x, so the write through p is not allowed. This class of bug is nearly invisible without a tool, since no hardware check exists for it. Whether a given pattern is allowed is still being refined in the language, and Tree Borrows is an alternative model: run both with MIRIFLAGS="-Zmiri-tree-borrows" when unsure. See aliasing in undefined behavior cases.

Useful flags

Set them through MIRIFLAGS:

FlagPurpose
-Zmiri-many-seeds=0..32run with 32 scheduler seeds to explore thread interleavings
-Zmiri-strict-provenancecatch int-to-pointer casts that lose provenance
-Zmiri-disable-isolationallow access to the real clock, environment and files
-Zmiri-ignore-leaksdo not fail on leaked memory

What it cannot do

  • Run FFI calls into C, or inline assembly.
  • Check paths your tests do not reach.
  • Run quickly: a test that loops a million times will take minutes.
  • Find logic errors, deadlocks (it can report them) or performance problems.

Combine it with sanitizers on a real binary for FFI heavy code, and with fuzzing to explore more inputs.

In CI

A separate job with a pinned nightly date keeps results stable. Restrict it to the unsafe modules, with cargo miri test --lib unsafe_module. The tool is documented in the Miri README.

Taggedmiri
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.