Skip to content
rusthacks.dev
Tooling

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.

2 min read
Summarize in AI
Flat illustration for the post “Debugging Memory Errors in Unsafe Rust With Sanitizers”, composed around tooling.

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

rust
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

Shell
RUSTFLAGS="-Zsanitizer=address" \
  cargo +nightly run --target x86_64-unknown-linux-gnu
Text
==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>::drop

The 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:

Shell
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu

ThreadSanitizer

Shell
RUSTFLAGS="-Zsanitizer=thread" \
  cargo +nightly run -Zbuild-std --target x86_64-unknown-linux-gnu

Data 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

Shell
RUSTFLAGS="-Zsanitizer=memory" \
  cargo +nightly run -Zbuild-std --target x86_64-unknown-linux-gnu

Finds 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

SanitizersMiri
Runscompiled native codean interpreter
Speeda few times slowermuch slower
FFI and C codeyesmostly no
Aliasing rules (stacked or tree borrows)noyes
TargetsLinux, macOS (partial), Windows (partial)any, by interpretation
Findsreal memory errorsrule 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

YAML
- run: rustup toolchain install nightly --component rust-src
- run: RUSTFLAGS="-Zsanitizer=address" cargo +nightly test --target x86_64-unknown-linux-gnu

Keep 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).

Also inSystems
Taggedasan
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.