Miri: Finding the Undefined Behavior Hiding in Your Unsafe Rust
You've written some unsafe code, the compiler accepted it, and your tests pass. But does that actually mean it's correct? Rust's safety guarantees rest on a contract you're responsible for upholding whenever you reach for unsafe, and violating that contract can produce bugs that only show up on a different compiler version, a different platform, or a different day. Miri is a tool built to catch exactly those violations before they bite you.
What It Does
Miri is an Undefined Behavior detection tool for Rust. It runs binaries and test suites of cargo projects as a platform-independent interpreter, checking for unsafe code that fails to uphold its safety requirements. Instead of relying on your machine's actual hardware and runtime behavior, Miri interprets your program and watches for the kinds of mistakes that Rust's type system can't catch on its own.
The list of things it detects is concrete: out-of-bounds memory accesses and use-after-free, invalid use of uninitialized data, violations of intrinsic preconditions (like reaching an unreachable_unchecked or calling copy_nonoverlapping with overlapping ranges), misaligned memory accesses and references, and violations of basic type invariants such as a bool that isn't 0 or 1. It also catches data races and emulates some weak memory effects, meaning atomic reads can return outdated values. On top of that, Miri reports memory leaks when allocated memory is still around at the end of execution and isn't reachable from a global static.
There are also two experimental aliasing checks: Stacked Borrows, which governs aliasing for reference types, and Tree Borrows, an optional alternative. Since Miri is an interpreter, it can also emulate programs on other targets, which is handy for confirming that byte-level data manipulation works correctly on both little-endian and big-endian systems.
Why It's Cool
-
It fills a gap the compiler can't. The Rust compiler checks your types, but it can't verify that your
unsafeblocks actually honor their contracts. Miri runs your code and checks the invariants at runtime, which is where these bugs actually live. -
It's found real bugs. The README notes that Miri has already discovered many real-world bugs, and the maintainers keep a list. That's not a theoretical selling point—this tool has earned its place in the ecosystem.
-
Deterministic by default. Miri isolates your program from the host system, replacing entropy sources, environment variables, and clocks with deterministic "fake" implementations. That means the same run gives you the same result, which makes debugging far less maddening. You can opt out with
MIRIFLAGS="-Zmiri-disable-isolation"if you need real system APIs. -
It's honest about its limits. The README is refreshingly upfront: Miri doesn't catch every violation of the Rust specification, partly because there's no single specification to check against. It uses its own approximation of what counts as Undefined Behavior. It also warns that if your program depends on unspecified layout details, it might run fine in Miri and still break elsewhere—though
-Zrandomize-layoutcan help surface some of those cases. -
It knows it can't explore everything. Program execution is non-deterministic when it depends on things like where allocations land or how threads interleave. Miri tests one of many possible executions, so it can miss bugs that only appear in a different one. Running with different
-Zmiri-seedvalues helps somewhat, but the README is clear that it won't explore all possible executions. -
One important caveat: the fake RNG APIs mean Miri is not suited for cryptographic use. Don't generate keys with it.
How to Try It
Miri lives in the Rust language organization, so the usual route is through rustup. Since it needs a nightly toolchain and a component that tracks the compiler, the general shape of getting started looks like this:
- Install or update the nightly toolchain with the Miri component:
rustup +nightly component add miri - Run Miri against your project's tests:
cargo +nightly miri test - If you need real system APIs instead of the deterministic fakes, disable isolation:
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test - To hunt for layout-dependent bugs, try:
MIRIFLAGS="-Zrandomize-layout" cargo +nightly miri test
The exact commands and any additional flags are best confirmed against the repository, since Miri tracks the compiler and details can shift between versions. Head to github.com/rust-lang/miri for the current setup instructions and the full documentation.
If Miri finds a bug in someone else's crate, the README asks that you report it—they'll add it to the list of bugs Miri has caught.
Final Thoughts
Miri isn't a silver bullet, and it doesn't pretend to be. It's a targeted tool for a specific, genuinely hard problem: verifying that your unsafe code does what you think it does. If you write unsafe Rust, maintain a crate with unsafe internals, or just want more confidence that your memory manipulation is sound, it's worth adding to your test workflow. It won't catch everything, and it can't promise anything about future compiler versions, but it'll catch a lot more than your test suite will on its own.
Follow @githubprojects for more developer tools and open source projects.