On this page
Valgrind and Sanitizers in Practice
Coverage: Valgrind (memcheck/helgrind/cachegrind/massif) → ASAN/UBSAN/TSAN → Comparison and Selection → Practical Debugging Cases Applicable to: Linux user-space, C/C++
Valgrind: Dynamic Binary Translation
Valgrind is essentially a JIT compiler—it translates the program's machine code into an intermediate representation (VEX IR), instruments it for analysis on the IR, and then translates it back to machine code for execution. There is no need to recompile the target program, but the performance overhead is significant.
# Memory leaks + out-of-bounds detection:
# Trace leak origins:
# Output interpretation:
# ==12345== Invalid read of size 4 ← Out-of-bounds / use-after-free
# ==12345== at 0x4005A4: main (test.c:5) ← Location where it occurred
# ==12345== Address 0x5204040 is 0 bytes after a block of size 40 alloc'd
# ==12345== at 0x4C2E80F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
# ==12345== by 0x40058F: main (test.c:3) ← Allocation location
Valgrind Tool Suite
# memcheck (default): Memory errors
# Detects: UAF, double free, out-of-bounds, uninitialized values, memory leaks
# helgrind: Data races
# cachegrind: CPU cache simulation
# massif: Heap profiling
Valgrind Limitations
Overhead:
memcheck: ~20-30x slower
helgrind: ~10-20x slower
cachegrind: ~20-50x slower
massif: ~10x slower
Not supported:
- Statically linked programs (ld.so and libc must be replaceable)
- Some uncommon system calls
- Some instruction set extensions (partial support for AVX-512)
Sanitizers: Compile-time Instrumentation
Sanitizers insert check code at compile time → detect errors at runtime. They are much faster than Valgrind but require recompilation.
# ASAN: Heap overflow, UAF, stack overflow, global overflow
# → On error: prints exact location + allocation location + deallocation location (colorful!)
# UBSAN: Undefined behavior
# Detects: integer overflow, shift out-of-bounds, null dereference, type alignment errors
# TSAN: Data races (64-bit only)
# → Reports: "data race on variable x between thread T1 and T2"
# LSAN: Memory leaks (included in ASAN)
ASAN_OPTIONS=detect_leaks=1
# MSAN: Uninitialized memory reads (Clang only)
ASAN Principles
ASAN uses shadow memory:
1 byte of shadow memory per 8 bytes of application memory
Shadow values: 0=fully addressable, 1-7=partially valid, negative=unaddressable
Every memory access: compiler inserts shadow checks
→ If shadow value says unaddressable → ASAN report
→ "Red zones" placed around allocations → Out-of-bounds detection
→ "Quarantine" placed after deallocation → UAF detection
Memory overhead: ~2x (shadow memory + red zones + quarantine)
Valgrind vs Sanitizers
| Valgrind | Sanitizers | |
|---|---|---|
| Requires recompilation | No (any binary) | Yes (-fsanitize=) |
| Speed | 20-50x slower | 1.5-5x slower |
| Memory overhead | No extra memory | 2x (ASAN) |
| Detection accuracy | High (binary translation sees everything) | High (compile-time instrumentation) |
| Multi-thread detection | helgrind | TSAN |
| Use cases | Black-box without source code | With source code, integrated into CI |
Practical Cases
# Case 1: Heap overflow → ASAN
# test.c:4: buf[100] = 0; // buf is char[100]
# ==ERROR: AddressSanitizer: heap-buffer-overflow
# WRITE of size 1 at 0x602000000064 thread T0
# #0 0x4007b3 in main test.c:4
# Case 2: Data race → TSAN
# WARNING: ThreadSanitizer: data race
# Write of size 4 at 0x7b0400000000 by thread T2:
# #0 counter_thread test.c:8
# Previous write of size 4 at 0x7b0400000000 by thread T1:
# #0 counter_thread test.c:8
# Case 3: Uninitialized read → Valgrind memcheck
# Conditional jump or move depends on uninitialised value(s)
References
- Documentation: https://github.com/google/sanitizers, https://valgrind.org/docs/manual
- LWN: "Address Sanitizer", "Thread Sanitizer"
- Tools: heaptrack (heap profiler, alternative to massif)
Keywords: Valgrind, memcheck, helgrind, massif, ASAN, UBSAN, TSAN, MSAN, shadow memory, data race