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:
valgrind --leak-check=full ./prog arg1 arg2

# Trace leak origins:
valgrind --leak-check=full --track-origins=yes ./prog

# 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
valgrind --tool=helgrind ./prog

# cachegrind: CPU cache simulation
valgrind --tool=cachegrind ./prog
cg_annotate cachegrind.out.<pid>  # Source-level miss rate

# massif: Heap profiling
valgrind --tool=massif ./prog
ms_print massif.out.<pid>  # Timeline: who allocated how much memory

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
gcc -fsanitize=address -g -O1 -o prog prog.c
./prog
# → On error: prints exact location + allocation location + deallocation location (colorful!)

# UBSAN: Undefined behavior
gcc -fsanitize=undefined -o prog prog.c
# Detects: integer overflow, shift out-of-bounds, null dereference, type alignment errors

# TSAN: Data races (64-bit only)
gcc -fsanitize=thread -g -O1 -o prog prog.c
# → Reports: "data race on variable x between thread T1 and T2"

# LSAN: Memory leaks (included in ASAN)
ASAN_OPTIONS=detect_leaks=1 ./prog

# MSAN: Uninitialized memory reads (Clang only)
clang -fsanitize=memory -g -o prog prog.c

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

ValgrindSanitizers
Requires recompilationNo (any binary)Yes (-fsanitize=)
Speed20-50x slower1.5-5x slower
Memory overheadNo extra memory2x (ASAN)
Detection accuracyHigh (binary translation sees everything)High (compile-time instrumentation)
Multi-thread detectionhelgrindTSAN
Use casesBlack-box without source codeWith source code, integrated into CI

Practical Cases

# Case 1: Heap overflow → ASAN
#   test.c:4: buf[100] = 0;  // buf is char[100]
gcc -fsanitize=address -g -o test test.c
./test
# ==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
gcc -fsanitize=thread -g -o test test.c
./test
# 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
valgrind ./prog
# 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