What Are Sanitizers

Sanitizers are compiler-instrumented runtime checks that detect memory and concurrency bugs. During compilation, the compiler injects additional code throughout your binary to monitor operations like memory accesses, allocations, and thread synchronization. This instrumentation catches bugs at runtime with precise diagnostics, including stack traces pointing to the exact source location.

The instrumentation adds overhead (typically 2-10x slowdown depending on the sanitizer), so sanitizers are used during development and testing rather than production.

How Instrumentation Works

When you compile with AddressSanitizer enabled, the compiler transforms memory operations. For example, a simple array access:

int arr[10];
arr[i] = 42;

Becomes conceptually:

int arr[10];
__asan_check_store(/* address */ &arr[i], /* size */ 4);
arr[i] = 42;

ASan maintains shadow memory — a compact representation mapping every 8 bytes of application memory to 1 byte of shadow memory. This shadow byte encodes whether the corresponding memory region is valid, freed, a redzone (padding around allocations), or partially addressable. Each memory access consults the shadow memory to verify validity.

LLVM Sanitizers

LLVM provides four primary sanitizers, enabled via -fsanitize= flags in Clang or via RUSTFLAGS in Rust:

AddressSanitizer (ASan) detects spatial and temporal memory errors: buffer overflows (stack, heap, and global), use-after-free, use-after-return, and memory leaks. It uses shadow memory plus redzones (poisoned memory regions) around allocations.

# Clang
clang -fsanitize=address program.c
 
# Rust (nightly)
RUSTFLAGS="-Zsanitizer=address" cargo build

MemorySanitizer (MSan) detects reads of uninitialized memory. It tracks whether each bit of memory has been initialized via shadow memory propagation. MSan is notably difficult to use because all code (including libraries) must be instrumented — any uninstrumented code causes false positives.

ThreadSanitizer (TSan) detects data races and some deadlocks. It intercepts synchronization primitives (mutexes, atomics) and memory accesses to build a happens-before graph, flagging accesses that aren’t properly ordered.

UndefinedBehaviorSanitizer (UBSan) catches undefined behavior: signed integer overflow, null pointer dereference, misaligned accesses, and invalid enum values. Unlike other sanitizers, UBSan has minimal overhead and can sometimes run in production.

Tip

ASan and MSan cannot be combined (they use conflicting shadow memory schemes). ASan + UBSan is a common combination.

Non-LLVM Sanitizers

GCC implements the same sanitizers with compatible flags (-fsanitize=address, etc.). The implementations share design principles but differ in internals. GCC’s sanitizers work on platforms where LLVM isn’t available.

Valgrind takes a different approach entirely — it’s a dynamic binary instrumentation framework that runs your unmodified binary inside a virtual machine. Valgrind’s Memcheck tool detects similar bugs to ASan but with 20-50x slowdown compared to ASan’s 2x. However, Valgrind requires no recompilation and works on any binary.

Dr. Memory is another dynamic instrumentation tool, similar to Valgrind but for Windows.

Intel Inspector provides commercial memory and threading analysis with IDE integration.

Warning

Valgrind and ASan detect overlapping but not identical bug classes. ASan catches stack buffer overflows that Valgrind misses; Valgrind catches some uninitialized reads that ASan doesn’t target.

Sanitizers and Rust

Safe Rust’s ownership system and bounds checking already prevent most bugs that sanitizers detect. Sanitizers remain valuable for:

Unsafe blocks where you’ve opted out of Rust’s guarantees:

unsafe {
    let ptr = vec.as_ptr();
    std::ptr::read(ptr.add(1000))  // ASan catches this overflow
}

FFI calls into C/C++ libraries where memory safety depends entirely on the foreign code.

Compiler bugs where safe Rust code is miscompiled — sanitizers have caught LLVM codegen issues affecting Rust.