Miri is a Rust interpreter that finds undefined behavior in your code.

Let’s see Miri in action. Imagine this simple Rust program:

fn main() {
    let mut data = [0u8; 4];
    let ptr = data.as_mut_ptr();
    let invalid_ptr = unsafe { ptr.add(5) }; // Pointer arithmetic out of bounds
    unsafe {
        *invalid_ptr = 1; // Dereferencing an invalid pointer
    }
}

If you compile and run this with rustc and cargo, it might crash, or worse, it might seem to work, but with subtle, unpredictable consequences. This is undefined behavior. Miri is designed to catch this.

To run this with Miri, you first need to install it:

cargo install miri

Then, you can run your program under Miri like this:

cargo miri run

Miri will execute the code step-by-step, simulating the execution and checking for violations of Rust’s memory safety rules. In this case, it will detect the out-of-bounds pointer access.

error: Undefined Behavior: pointer arithmetic resulted in an out-of-bounds pointer
 --> src/main.rs:5:29
5 |     let invalid_ptr = unsafe { ptr.add(5) }; // Pointer arithmetic out of bounds
  |                             ^^^^^^^^^^^^^^^
  = help: pointer to `[u8; 4]` at `0x55f11f302000` was offset by 5 bytes, which is out of bounds for the allocation of size 4 bytes

This output tells you exactly what went wrong: pointer arithmetic (specifically ptr.add(5)) went beyond the allocated memory for data.

The Mental Model: Miri as a Strict Memory Auditor

Miri doesn’t just compile your code; it runs it in a simulated environment. Think of it as a hyper-vigilant auditor for your program’s memory interactions. It tracks every byte of memory your program touches, where it came from, and how it’s being accessed.

Here’s what Miri is checking for:

  • Out-of-Bounds Accesses: Reading or writing memory outside the bounds of an allocated object (like an array or Vec). This includes pointer arithmetic that goes too far.
  • Use-After-Free: Accessing memory that has already been deallocated.
  • Double Free: Attempting to deallocate memory that has already been freed.
  • Data Races: Concurrent access to shared mutable data where at least one access is a write, without proper synchronization.
  • Invalid Dereferences: Dereferencing null pointers or pointers that don’t point to valid memory.
  • Uninitialized Reads: Reading from memory that hasn’t been initialized with a valid value.

Miri achieves this by instrumenting your code. When you compile with cargo miri test or cargo miri run, Miri uses a custom Cargo configuration to build your code with specific compiler flags. It essentially wraps your code in a shim that intercepts memory operations and validates them against its internal state.

The core of Miri is its interpreter, which understands Rust’s abstract machine. It doesn’t just execute machine code; it executes a higher-level representation (MIR) of your program, allowing it to perform checks that are difficult or impossible at the machine code level.

Common Pitfalls Miri Uncovers

  1. Pointer Arithmetic Gone Wild: As seen in the example, ptr.add(N) where N exceeds the size of the pointed-to object is a prime offender.

    • Diagnosis: Miri’s error message will clearly state "pointer arithmetic resulted in an out-of-bounds pointer."
    • Fix: Ensure your pointer arithmetic stays within the bounds of the allocated object. For slices and Vecs, use methods like get() or get_mut() which return Option and handle bounds checking gracefully. If you must use raw pointers, carefully calculate the end of your buffer. For example, if ptr points to the start of a [u8; 4], ptr.add(3) is the last valid byte, and ptr.add(4) points just past the end, which is valid for forming a one-past-the-end pointer, but ptr.add(5) is definitely out.
  2. unsafe Block Mismanagement: Any unsafe block is a potential source of UB. Miri will scrutinize them.

    • Diagnosis: Miri will point to the specific unsafe block or operation that caused the UB.
    • Fix: Carefully review the invariants you are upholding within unsafe blocks. Ensure that all requirements of the unsafe function or operation (e.g., pointer validity, aliasing rules) are met before the operation. This often involves adding runtime checks or static assertions.
  3. FFI Calls with Invalid Data: When interacting with C libraries (Foreign Function Interface), passing invalid pointers or data can lead to UB.

    • Diagnosis: Miri might report UB originating from within the FFI call, or it might manifest as UB in your Rust code after the FFI call returns, if the FFI corrupted memory.
    • Fix: Always validate data before passing it to FFI. Ensure that raw pointers passed to C are valid and that the C code adheres to its contract regarding memory management. Use tools like bindgen to generate safe wrappers.
  4. Misinterpreting *mut T and *const T Aliasing Rules: Rust has strict rules about having multiple mutable references or a mutable reference and an immutable reference to the same data simultaneously. Raw pointers can bypass these checks, leading to UB.

    • Diagnosis: Miri will often report UB related to "accessing data through a pointer that does not meet the aliasing requirements."
    • Fix: Ensure that when you are using raw pointers, you do not create situations where multiple mutable pointers or a mutable and immutable pointer alias the same memory location unless you are performing atomic operations or using synchronization primitives.
  5. Integer to Pointer Casts: Casting arbitrary integers to pointers can create invalid pointers.

    • Diagnosis: Miri will report "dereferencing a pointer that was created from a non-pointer integer value."
    • Fix: Only cast integers to pointers if you are certain the integer represents a valid memory address that is currently accessible and properly aligned. This is rarely necessary in safe Rust.
  6. Slice Indexing with get_unchecked: While get_unchecked can offer performance gains, it bypasses bounds checks. If used incorrectly, it’s a direct route to UB.

    • Diagnosis: Miri will flag out-of-bounds accesses, similar to raw pointer issues.
    • Fix: Use get_unchecked only when you have statically proven (or can prove via runtime logic) that the index is within bounds. Prefer get() and get_mut() for safety.

When Miri reports an error, it’s a critical signal. The fix isn’t always obvious, but Miri’s detailed output, including the exact location and the nature of the UB, is your best tool for debugging.

The next error you’ll likely encounter after fixing memory safety issues is related to uninitialized memory.

Want structured learning?

Take the full Cargo course →