Diagnosing Heap Corruption in C with AddressSanitizer: Symptoms, Checks, and Fixes
Intermittent crashes that move when you add a printf are heap corruption, not logic bugs. This guide shows how to catch the real culprit with AddressSanitizer in GCC and Clang: ordered checks, fixes matched to each report type, and when to escalate to Valgrind, UBSan, or TSan.
30 Apr 2026, 00:59 UTC

The recognizable condition
Your C program crashes intermittently, glibc aborts with messages like double free or corruption or heap-buffer-overflow, and — the telltale sign — the crash moves or disappears when you add an unrelated printf or reorder a declaration. That is heap corruption: some earlier write damaged allocator metadata or another allocation, and the crash site is a victim, not the culprit. Debugging the crash location directly wastes time because the actual bug happened earlier, somewhere else.
AddressSanitizer (ASan), built into both GCC and Clang, solves exactly this problem. It instruments every allocation and access so the first invalid operation is reported at the moment it happens, with stack traces for the bad access, the allocation, and the deallocation. This guide walks through recognizing the bug class, running ordered checks, and matching fixes to findings.
Common causes and what ASan calls them
| Bug | Typical symptom | ASan report name |
|---|---|---|
Use-after-free: writing or reading through a pointer after free() | Data changes "by itself"; crash in unrelated code | heap-use-after-free |
| Heap buffer overflow: off-by-one or bad size arithmetic past the malloc'd region | Corrupted neighboring objects; glibc aborts | heap-buffer-overflow |
| Double-free: freeing the same pointer twice | Immediate glibc abort, sometimes only under load | attempting double-free |
Mismatched allocator: e.g. free() on memory from a custom arena | Abort inside free() | alloc-dealloc-mismatch or bad-free |
| Uninitialized heap read feeding a size or index | Nondeterministic behavior across runs | Not caught by ASan — use MemorySanitizer or Valgrind |
Check 1: Rebuild with ASan and reproduce
Rebuild the project (or at minimum the failing module and everything it links) with instrumentation. Run this in your normal build environment; you need permission to recompile, nothing more:
gcc -fsanitize=address -g -O1 -fno-omit-frame-pointer -o myapp myapp.c
./myapp # run the failing workloadThe same flags work with Clang (clang -fsanitize=address ...). -g gives line numbers, -O1 keeps the build fast while preserving reasonable stack traces, and -fno-omit-frame-pointer improves unwinding. When the bug triggers, ASan prints the error class, the exact address and access size, and stack traces showing where the block was allocated and freed. That allocation/free pair is usually enough to identify the ownership mistake.
Do not ship ASan builds or benchmark them: instrumentation roughly doubles memory use and slows execution noticeably. It is a diagnostic build only.
Check 2: Tame noisy or timing-sensitive output
If the program logs many errors or the bug is a race against cleanup code, control the runtime through the ASAN_OPTIONS environment variable:
ASAN_OPTIONS=halt_on_error=1:abort_on_error=1:detect_leaks=0 ./myapphalt_on_error=1 stops at the first error (the first one is the real one; later reports are usually fallout). detect_leaks=0 silences leak reports when you are chasing corruption, not leaks — re-enable it later. If stack traces show raw addresses instead of function names, ensure llvm-symbolizer is on your PATH (Clang) or pipe addresses through addr2line -e myapp (GCC). Exact option names and defaults differ between toolchains and versions, so confirm against your compiler's documentation.
Check 3: When ASan cannot see the bug
ASan only instruments allocations that go through its intercepted malloc/free. It is blind to corruption inside custom allocators, mmap'd memory pools, and shared or DMA buffers. For those cases:
- Valgrind Memcheck — slower, but works on uninstrumented binaries and catches uninitialized reads ASan misses.
- Guard pages — place the suspect allocation at the end of an
mmap'd region followed by anmprotect(PROT_NONE)page, so any overflow faults at the exact instruction. - Electric-fence-style allocators — same guard-page idea packaged as a malloc replacement.
Fixes tied to findings
Match the repair to the ASan report, then re-run under ASan to confirm:
- heap-use-after-free: fix ownership. Give each allocation a single owner responsible for freeing it, and set pointers to
NULLimmediately afterfree()so dangling uses fail loudly instead of silently. - heap-buffer-overflow: fix the size arithmetic. Allocate with
malloc(n * sizeof *p), checknfor overflow, and replace unbounded copies withsnprintformemcpyusing a computed bound — never a guessed constant. - double-free: centralize cleanup in one function (often a
destroy_*paired with the constructor) and NULL the pointer after freeing;free(NULL)is a safe no-op. - alloc-dealloc-mismatch: free memory with the same allocator family that produced it; document the requirement in the function's header comment.
A minimal confirmation test
To verify your toolchain setup before trusting it on the real bug, compile this deliberately broken program:
#include <stdlib.h>
#include <string.h>
int main(void) {
char *buf = malloc(16);
free(buf);
strcpy(buf, "dangling"); /* use-after-free */
return 0;
}Compile with gcc -fsanitize=address -g uaf.c and run it. A working setup reports heap-use-after-free with both the free and the invalid-write stacks. Repeat with clang to compare report wording — both should detect it. After fixing your real bug, re-run the full workload with ASAN_OPTIONS=detect_leaks=1 and confirm zero errors and no new leaks before closing the issue.
Escalation criteria
Escalate beyond ASan when any of these hold:
- Corruption persists after every ASan finding is fixed — suspect a custom allocator or memory ASan cannot instrument; move to Valgrind or guard pages.
- The bug only reproduces at
-O2/-O3— suspect strict-aliasing or alignment violations; add-fsanitize=undefinedand review type-punning casts. - Multiple threads touch the corrupted structure — rebuild with
-fsanitize=thread(TSan) to find the race; ASan and TSan cannot be combined in one binary, so test separately.
Limitations to keep in mind: ASan misses most stack and global overflows unless those objects are instrumented, it cannot see inside custom allocators, and flag names and defaults vary across GCC/Clang versions — verify against your toolchain's docs before scripting around them.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.