Dynamic Analysis
The Rust programming language does not prevent you from writing invalid code, it
just makes it a lot harder. The default state is that code is subject to the
borrow checker, which ensures memory safety. However, sometimes you need to
write code that bypasses these safety guarantees and places the burden of
ensuring correctness on you: unsafe code.
A typical Rust program contains minimal unsafe code. Most crates avoid it, and
when they do use it, it tends to be in small, contained sections. Rust doesn’t
eliminate the ability to shoot yourself in the foot; it just forces you to be
intentional about it. In languages like C or C++, effectively all code is
implicitly unsafe, without the clear boundaries Rust provides.
Sometimes, you would like to check if the unsafe code you have written is in
fact valid. This can be challenging because what you’re trying to catch is
undefined behavior. For example, reading one byte past an array’s bounds
wouldn’t necessarily cause your program to crash; you might simply read garbage
data.
One solution is to use dynamic analysis, where your program runs in a special environment (instrumented or emulated) and a higher-level tool validates every action your program takes. If your program triggers any undefined behavior, you receive an error and a description of what went wrong:
- Read uninitialized memory
- Read past memory allocation/stack
- Write past memory allocation/stack
- Free memory that is already freed (double free)
- Forget to free memory (memory leak)
These tools can be enabled when running unit tests to monitor your code’s behavior and provide diagnostic errors when it performs invalid operations. Triggering undefined behavior is dangerous because your program may break when switching compilers or when running on different hardware. For example, x86 CPUs permit unaligned memory reads, but other platforms might not, so code that relies on this behavior will fail on those platforms.
Due to Rust’s built-in safety guarantees, most Rust code doesn’t contain significant amounts of undefined behavior, making these tools less frequently needed than in languages like C or C++.
There is one tool particularly well-suited for detecting invalid operations in Rust code: Miri.
Miri
Miri is a tool that lets you find undefined behaviour in Rust programs. It works as an interpreter for Rust’s mid-level intermediate representation (MIR), which the compiler uses internally. Similar to Valgrind, Miri works by interpreting code rather than executing it directly. The advantage of Miri over Valgrind is that MIR retains rich semantic information, resulting in more precise diagnostic messages. However, like Valgrind, it significantly slows down your program’s execution.
You can install and use Miri with the following commands:
rustup +nightly component add miri
cargo +nightly miri test
Miri can detect numerous issues such as:
- Invalid memory accesses
- Use of uninitialized memory
- Data races
- Violations of Rust’s stacked borrows model
- Leaking of memory marked as
MayLeak
Miri is particularly valuable for testing unsafe code, as it can catch subtle
issues that might not manifest in normal testing environments. It is also useful
for testing code that interfaces with external libraries through FFI, as this is
a common source of unsafety.
Miri has some limitations that are worth knowing about. For example, Miri runs as a single-threaded interpreter (it simulates threads sequentially, like a multi-threaded OS on a single-core CPU), so it cannot detect bugs that depend on specific thread interleavings — but it can and does detect data races. SIMD support is limited, with only a subset of intrinsics implemented. Miri also cannot access platform-specific APIs, FFI, or networking.
cargo-careful
cargo-careful, by the same author
as Miri (Ralf Jung), is a lighter-weight tool that adds extra runtime checks to
your code without the overhead of a full interpreter. It works by enabling
additional debug assertions in the standard library and your code, catching
issues like uninitialized memory usage, misaligned memory accesses, and integer
overflow.
cargo install cargo-careful
cargo +nightly careful test
The key advantage over Miri is speed: cargo-careful runs your tests at near
normal speed, making it practical to include in regular test runs or CI. The
tradeoff is that it catches fewer issues — it cannot detect aliasing violations
or data races the way Miri can. Think of it as a middle ground between normal
testing and a full Miri run.
Valgrind
Valgrind lets you run your program in an emulated way, where all memory access is monitored. It has a relatively faithful emulation of the x86 architecture, it even incorporates features such as a model of how CPU caches work so you can check how good the memory locality of your program is.
Due to the emulation, there is some overhead. It can also report how many instructions your program took to run, which is more useful for microbenchmarks than time, because it is stable between machines (but not architectures).
There is a cargo-valgrind tool that you can use to run your Rust unit tests with valgrind. It will parse the output of valgrind and output them in a human-readable format.
LLVM Sanitizers
LLVM sanitizers (AddressSanitizer, ThreadSanitizer, UndefinedBehaviorSanitizer, LeakSanitizer) are compile-time instrumentation tools. Unlike Valgrind, which emulates execution, sanitizers insert checks directly into your binary during compilation. This gives them access to richer metadata (type information, allocation context) and lets them detect certain issues that Valgrind cannot, at the cost of requiring a recompilation with the appropriate flags.
All sanitizers currently require a nightly toolchain because they use the
unstable -Z sanitizer flag.
Address Sanitizer (ASan)
AddressSanitizer is designed to detect memory errors such as:
- Use-after-free
- Heap/stack/global buffer overflow
- Stack-use-after-return
- Double-free, invalid free
You can use ASan with Rust by passing the sanitizer flag:
RUSTFLAGS="-Z sanitizer=address" cargo +nightly test
ASan typically introduces a 2-3x runtime overhead but runs significantly faster than Valgrind while providing comparable detection capabilities.
Memory Sanitizer (MSan)
MemorySanitizer detects uses of uninitialized memory, which can cause subtle bugs that are hard to track down. Unlike ASan, MSan focuses specifically on detecting reads from uninitialized memory.
RUSTFLAGS="-Z sanitizer=memory" cargo +nightly test
MSan is particularly valuable for code that manually manages memory or interfaces with C libraries where memory initialization might be incomplete.
Undefined Behaviour Sanitizer (UBSan)
UndefinedBehaviorSanitizer detects various types of undefined behavior at runtime, including:
- Integer overflow
- Invalid bit shifts
- Misaligned pointers
- Null pointer dereferences
- Unreachable code execution
RUSTFLAGS="-Z sanitizer=undefined" cargo +nightly test
UBSan has relatively low performance overhead (typically 20-50%) and can detect issues that other sanitizers might miss.
Thread Sanitizer (TSan)
ThreadSanitizer detects data races in multithreaded code. This is particularly
valuable in Rust when using unsafe to implement concurrent data structures or
when interfacing with external threading libraries.
RUSTFLAGS="-Z sanitizer=thread" cargo +nightly test
TSan has higher overhead (5-15x) but excels at identifying race conditions that might occur only sporadically during normal testing.
Reading
Data-driven performance optimization with Rust and Miri (archived) by Keaton Brandt
Keaton shows you how you can use Miri to get detailed profiling information from Rust programs, visualize them in Chrome developer tools and use this information to optimize your program’s execution time.
Unsafe Rust and Miri by Ralf Jung
In this talk, Ralf explains key concepts around writing unsafe code, such as what “undefined behaviour” and “unsoundness” mean, and explains how to write unsafe code in a systematic way that reduces the chance of getting it wrong.
C++ Safety, in context (archived) by Herb Sutter
In this article, Herb Sutter discusses the safety issues C++ has. While this is not directly relevant to Rust, he does make a good point about the fact that there is good tooling to catch a lot of issues (sanitizers, for example) and that they should be more widely used, even by projects that use languages that are safer by design, such as Rust. While some consider C++ to be defective, with the right tooling a majority of issues can be caught.
The Soundness Pledge (archived) by Ralph Levien
Ralph talks about the use of unsafe in Rust. Many developers consider using
it to be bad style, but he argues that it is not unsafe that is a problem, it
is unsound code that is a problem. As a community, we should strive to
eliminate unsound code. This includes using tools like Miri to ensure
soundness.
Rust and Valgrind by Nicholas Nethercote
Nicholas explains why you should use Valgrind with Rust, and what kinds of issues it can detect.