Memory Testing in C/C++: Valgrind, AddressSanitizer, and Memory Leak Detection
Memory bugs in C/C++ are notoriously hard to reproduce — use-after-free, buffer overflows, and memory leaks often cause failures far from their origin. Valgrind and AddressSanitizer (ASan) catch these at the point of occurrence with precise diagnostics. This guide shows you how to use both tools and integrate them into CI.
Two Approaches to Memory Testing
Valgrind (Memcheck)
- Runs your program in an instrumented virtual machine
- No recompilation required
- Slower (5-20x slower)
- Catches every memory error regardless of code path coverage
- Works on any binary, even without debug symbols
AddressSanitizer (ASan)
- Compiler instrumentation (compile with
-fsanitize=address) - Fast (1.5-3x slower than uninstrumented code)
- Requires recompilation
- Catches errors only on executed code paths
- Better error messages with source locations
Rule of thumb: Use ASan in CI (fast, CI-friendly). Use Valgrind for deep investigations of subtle bugs or legacy binaries you can't recompile.
AddressSanitizer (ASan)
Setup
# GCC or Clang — add flags at compile time
clang++ -fsanitize=address -fno-omit-frame-pointer -g -O1 myapp.cpp -o myapp
gcc -fsanitize=address -fno-omit-frame-pointer -g -O1 myapp.cpp -o myappIn CMake:
# CMakeLists.txt
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer -g)
add_link_options(-fsanitize=address)
endif()Build with ASan:
cmake -B build -DENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/myapp # or ./build/testsWhat ASan Catches
Heap Buffer Overflow
// Bug: writing past end of allocation
void heap_overflow_example() {
int* arr = new int[5];
arr[5] = 42; // one past the end!
delete[] arr;
}ASan output:
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001c
WRITE of size 4 at 0x60200000001c thread T0
#0 0x401234 in heap_overflow_example example.cpp:4
0x60200000001c is located 0 bytes to the right of 20-byte region
[0x602000000008,0x60200000001c)
allocated by thread T0 here:
#0 0x7f1a2b3c4d5e in operator new[](unsigned long)
#1 0x401220 in heap_overflow_example example.cpp:3Use After Free
void use_after_free_example() {
int* p = new int(42);
delete p;
*p = 100; // use after free!
}ASan output:
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
WRITE of size 4 at 0x602000000010 thread T0
#0 0x401234 in use_after_free_example example.cpp:4
0x602000000010 was freed by thread T0 here:
#0 0x7f1a2b3c4d5e in operator delete(void*)
#1 0x401228 in use_after_free_example example.cpp:3Stack Buffer Overflow
void stack_overflow_example() {
char buf[10];
strcpy(buf, "hello world"); // 11 bytes into 10-byte buffer
}Memory Leak
ASan reports leaks at program exit by default:
void memory_leak_example() {
int* p = new int[100];
// forgot delete[]
}==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 400 byte(s) in 1 object(s) allocated from:
#0 0x7f1a2b3c4d5e in operator new[](unsigned long)
#1 0x401234 in memory_leak_example example.cpp:2ASan Configuration
# Disable leak detection (useful when testing subsystems, not whole programs)
ASAN_OPTIONS=detect_leaks=0 ./myapp
<span class="hljs-comment"># More verbose output
ASAN_OPTIONS=verbosity=1 ./myapp
<span class="hljs-comment"># Halt on first error vs. continue collecting
ASAN_OPTIONS=halt_on_error=0 ./myapp
<span class="hljs-comment"># Full configuration
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:print_stats=1 ./myappMemorySanitizer (MSan)
Catches reads of uninitialized memory — a different class of bug from ASan:
clang++ -fsanitize=memory -fno-omit-frame-pointer -g myapp.cpp -o myappvoid uninitialized_read_example() {
int x;
if (x > 0) { // read of uninitialized x
do_something();
}
}MSan output:
==12345==WARNING: MemorySanitizer: use-of-uninitialized-value
#0 0x401234 in uninitialized_read_example example.cpp:3Note: MSan requires all linked libraries to be compiled with MSan, which means you typically need to rebuild the standard library. Practically, use it for code you control; accept false negatives from system libraries.
UndefinedBehaviorSanitizer (UBSan)
Catches C++ undefined behavior: signed integer overflow, null pointer dereference, misaligned access, etc.
clang++ -fsanitize=undefined -g myapp.cpp -o myapp
# Or combine:
clang++ -fsanitize=address,undefined -g myapp.cpp -o myappvoid ubsan_examples() {
// Signed integer overflow (UB in C++)
int x = INT_MAX;
int overflow = x + 1; // UB!
// Null pointer dereference
int* p = nullptr;
int val = *p; // UB!
// Signed shift past bitwidth
int shifted = 1 << 32; // UB on 32-bit int!
// Array index out of bounds on static arrays
int arr[5];
int oob = arr[10]; // UB!
}Valgrind Memcheck
Basic Usage
# Install
<span class="hljs-built_in">sudo apt install valgrind <span class="hljs-comment"># Ubuntu/Debian
brew install valgrind <span class="hljs-comment"># macOS (limited support)
<span class="hljs-comment"># Run your program under Valgrind
valgrind --tool=memcheck ./myapp
<span class="hljs-comment"># Full options for maximum detection
valgrind \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=<span class="hljs-built_in">yes \
--verbose \
--error-exitcode=1 \
./myappWhat Valgrind Catches
Invalid Memory Read
char* buf = malloc(5);
char c = buf[10]; // invalid read!
free(buf);Invalid read of size 1
at 0x40123: main (example.c:3)
Address 0x5204055 is 5 bytes after a block of size 5 alloc'd
at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
at 0x401229: main (example.c:2)Memory Leak
LEAK SUMMARY:
definitely lost: 100 bytes in 1 blocks
indirectly lost: 0 bytes in 0 blocks
possibly lost: 0 bytes in 0 blocks
still reachable: 0 bytes in 1 blocks- Definitely lost — leaked, no pointer to it remains
- Still reachable — pointer exists at exit, but was never freed (sometimes intentional)
- Possibly lost — pointer arithmetic may be involved
Double Free
char* p = malloc(10);
free(p);
free(p); // double free!Invalid free() / delete / delete[] / realloc()
at 0x4C30D3B: free (vg_replace_malloc.c:540)
at 0x401238: main (example.c:4)
Address 0x5204050 is 0 bytes inside a block of size 10 free'd
at 0x4C30D3B: free (vg_replace_malloc.c:540)
at 0x40122F: main (example.c:3)Running Tests Under Valgrind
With gtest:
valgrind --leak-check=full --error-exitcode=1 ./testsWith CTest:
# Add a Valgrind test target
find_program(VALGRIND valgrind)
if(VALGRIND)
add_test(NAME valgrind_tests
COMMAND ${VALGRIND}
--leak-check=full
--error-exitcode=1
$<TARGET_FILE:unit_tests>
)
set_tests_properties(valgrind_tests PROPERTIES LABELS "memcheck")
endif()Suppression Files
Valgrind reports errors in system libraries that you can't fix. Suppress them:
# valgrind.supp — suppress known false positives
{
openssl_false_positive
Memcheck:Leak
fun:malloc
obj:*/libssl.so*
}
{
glibc_pthread_leak
Memcheck:Leak
fun:calloc
fun:_dl_allocate_tls
}valgrind --suppressions=valgrind.supp ./testsASan with Google Test
# CMakeLists.txt
add_executable(tests tests/test_*.cpp)
target_link_libraries(tests PRIVATE mylib GTest::gtest_main)
# Add ASan build configuration
target_compile_options(tests PRIVATE
$<$<CONFIG:Debug>:-fsanitize=address,undefined -fno-omit-frame-pointer -g>
)
target_link_options(tests PRIVATE
$<$<CONFIG:Debug>:-fsanitize=address,undefined>
)cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 ./build/testsCI Integration
GitHub Actions with ASan
name: Memory Safety Tests
on: [push, pull_request]
jobs:
asan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install clang
run: sudo apt install -y clang
- name: Configure with ASan
run: |
cmake -B build \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_BUILD_TYPE=Debug \
-DENABLE_ASAN=ON
- name: Build
run: cmake --build build --parallel
- name: Run tests with ASan
env:
ASAN_OPTIONS: detect_leaks=1:halt_on_error=1
run: cd build && ctest --output-on-failure
valgrind:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install valgrind
run: sudo apt install -y valgrind cmake
- name: Configure
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
- name: Build
run: cmake --build build --parallel
- name: Run under Valgrind
run: |
valgrind \
--leak-check=full \
--error-exitcode=1 \
--suppressions=valgrind.supp \
./build/testsPractical Workflow
Development
- Always compile debug builds with ASan — catch bugs immediately during development
- Run tests with ASan enabled — your test suite doubles as a memory checker
# Add to your development Makefile or script
build-test:
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON
cmake --build build --parallel
ASAN_OPTIONS=detect_leaks=1 ./build/tests
<span class="hljs-comment"># Quick check: production build without ASan
build-release:
cmake -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-release --parallelCI
| Stage | Tool | Speed | Purpose |
|---|---|---|---|
| Every commit | ASan | Fast | Catch regressions immediately |
| Every commit | UBSan | Fast | Undefined behavior detection |
| Nightly | Valgrind | Slow | Deep leak analysis |
| Pre-release | Valgrind + MSan | Very slow | Comprehensive check |
Common False Positives and Workarounds
ASan and Clang's libc++
Sometimes ASan reports leaks in system libraries:
LSAN_OPTIONS=suppressions=lsan.supp ./testsValgrind and OpenSSL
OpenSSL has known "still reachable" memory at exit. Use suppressions (see above).
Preloaded Libraries
# If ASan conflicts with LD_PRELOAD
ASAN_OPTIONS=verify_asan_link_order=0 ./testsInterpreting Results Quickly
| Error type | First suspect |
|---|---|
| Heap buffer overflow | Array index calculation, off-by-one |
| Stack buffer overflow | sprintf/strcpy with unbounded input |
| Use after free | Premature delete, dangling reference |
| Uninitialized read | Missing constructor initialization |
| Memory leak | Missing delete/free, exception before cleanup |
| Double free | Shared ownership without shared_ptr |
The fastest fix is usually not "where ASan reports the error" but "where the memory was allocated." Both Valgrind and ASan show the allocation site in the trace.
Next Steps
- Enable ASan now in your CMakeLists.txt Debug configuration — low effort, high payoff
- Add the CI step — 5 minutes of setup, catches an entire class of bugs
- Combine with gtest — run the Google Test tutorial tests under ASan
- Try UBSan too —
-fsanitize=address,undefinedcatches both classes in one build - Add to Catch2 — see the Catch2 guide for framework integration