Debugging Memory Errors in Unsafe Rust With Sanitizers
Find memory errors in unsafe Rust with AddressSanitizer, ThreadSanitizer and MemorySanitizer on nightly, and Miri for interpretation. Flags, build-std, output and limits.

Safe Rust cannot corrupt memory, so a segfault, a heap corruption report from the allocator or a data race in a Rust program points at unsafe code, a C library or a compiler bug. Sanitizers find these at run time. They are unstable features, so they need a nightly toolchain. Everything here was checked against the Rust sanitizer documentation for the Rust 1.90 era; flag names move, so use rustc +nightly -Z help if one is rejected.
A bug to find
fn main() {
let v = vec![1u8, 2, 3];
let p = v.as_ptr();
drop(v);
let x = unsafe { *p };
println!("{x}");
}This reads freed memory. It compiles, usually runs and may print garbage. That is a use after free.
AddressSanitizer
RUSTFLAGS="-Zsanitizer=address" \
cargo +nightly run --target x86_64-unknown-linux-gnu==1234==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
READ of size 1 at 0x602000000010 thread T0
#0 0x... in app::main src/main.rs:5:22
freed by thread T0 here:
#0 ... in alloc::vec::Vec<T>::dropThe report names the access, the line, and where the memory was freed. ASan also catches buffer overflows on the stack, heap and globals, double frees, and leaks at exit (LeakSanitizer, on Linux). Replace x86_64-unknown-linux-gnu with your host triple; on macOS (aarch64-apple-darwin) ASan works but leak detection is limited.
For tests:
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnuThreadSanitizer
RUSTFLAGS="-Zsanitizer=thread" \
cargo +nightly run -Zbuild-std --target x86_64-unknown-linux-gnuData races (unsynchronized concurrent access with at least one write) are reported with both stacks. -Zbuild-std (with the rust-src component installed) rebuilds the standard library with instrumentation; without it, TSan misses synchronization inside std and reports false races. This is the tool for hand-written lock-free code and unsafe impl Send.
MemorySanitizer
RUSTFLAGS="-Zsanitizer=memory" \
cargo +nightly run -Zbuild-std --target x86_64-unknown-linux-gnuFinds reads of uninitialized memory, such as MaybeUninit misuse. It requires every linked library to be instrumented, so it is practical only for pure Rust or fully built-from-source dependencies.
Sanitizers versus Miri
| Sanitizers | Miri | |
|---|---|---|
| Runs | compiled native code | an interpreter |
| Speed | a few times slower | much slower |
| FFI and C code | yes | mostly no |
| Aliasing rules (stacked or tree borrows) | no | yes |
| Targets | Linux, macOS (partial), Windows (partial) | any, by interpretation |
| Finds | real memory errors | rule violations, UB that happens not to crash |
Miri finds undefined behavior that never visibly misbehaves, such as an invalid reference that still points at valid memory (Miri). Sanitizers find actual corruption in programs Miri cannot run. Run both on unsafe crates (undefined behavior cases).
In CI
- run: rustup toolchain install nightly --component rust-src
- run: RUSTFLAGS="-Zsanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnuKeep the job separate and allow it a longer timeout. Combine with fuzzing for the best coverage of unsafe code (cargo fuzz). Safety documentation for each unsafe block remains the main defense (safety comments).


