The Seven Memory Bugs You’ll Meet in Production¶
Every memory bug in C is a variation of one of seven canonical failure modes. When you can name the bug from the symptom, and reproduce it in five lines, you can fix it — not just guess-patch it. This file walks each one: what it is, a minimal repro, what ASan says, what valgrind says, and the fix.
Setup: Your Two Best Friends¶
Before every bug, know your two tools:
AddressSanitizer (ASan) — compile with
-fsanitize=address -g -O1. Instruments every load/store; typically 2x slowdown, 3x memory. Detects most heap bugs with clear stack traces. Enabled by both GCC and Clang.valgrind memcheck — no recompile needed. Runs the program under a synthetic CPU; ~20x slowdown. Slower but sometimes catches things ASan misses (uninitialized reads especially, though ASan has a companion
-fsanitize=memoryfor that).
Run both on every project this year. If ASan says clean and valgrind says clean, you’ve earned the right to be confident.
Bug 1: Heap Buffer Overflow¶
What: Write past the end of a heap allocation.
Minimal repro:
int *p = malloc(4 * sizeof(int));
p[4] = 42; // valid indices are 0..3
free(p);
ASan output:
==<phone_number_or_numberic_id_or_random_id_28>==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x<phone_number_or_numberic_id_or_random_id_29>
WRITE of size 4 at 0x<phone_number_or_numberic_id_or_random_id_29> thread T0
#0 0x... in main foo.c:4
0x<phone_number_or_numberic_id_or_random_id_29> is located 0 bytes to the right of 16-byte region
valgrind: Invalid write of size 4 ... Address 0x... is 0 bytes after a block of size 16 alloc'd.
Fix: Bounds-check every index. Prefer size_t for indices. Track buffer size alongside pointer. In the wild this bug hides in sprintf/strcpy/memcpy with a wrong length — use snprintf and memcpy with explicit sizeof *dst * n.
Bug 2: Use-After-Free (UAF)¶
What: Access memory through a pointer whose block has been freed.
Minimal repro:
int *p = malloc(sizeof *p);
*p = 7;
free(p);
printf("%d\n", *p); // UAF
ASan output: heap-use-after-free on address 0x..., plus stack traces of both the free and the use. This is why ASan is magical: it tells you when the block was freed.
Fix: Set pointers to NULL after free. Never free inside a function that also stores the pointer somewhere the caller can still reach. Ownership discipline (previous file) prevents almost all of these. In a shared-pointer context, use refcounting.
UAF is particularly nasty because heap allocators reuse freed memory. Your read might return the original bytes, or it might return whatever a later allocation put there. Nondeterministic bugs love UAF.
Bug 3: Double-Free¶
What: Call free twice on the same pointer.
Minimal repro:
int *p = malloc(sizeof *p);
free(p);
free(p);
glibc output (without ASan): free(): double free detected in tcache 2 and abort. Modern glibc catches most cases; older versions silently corrupted the heap and crashed later.
ASan output: attempting double-free on 0x..., with both stacks.
Fix: free(p); p = NULL; — free(NULL) is defined as a no-op. Or: single-ownership discipline so only one code path ever calls free on a given pointer.
Bug 4: Memory Leak¶
What: Allocate and never free. Doesn’t crash immediately — just makes your RSS grow forever.
Minimal repro:
for (;;) { int *p = malloc(8126311); (void)p; }
ASan output (with ASAN_OPTIONS=detect_leaks=1, default on Linux): at program exit, prints a summary of every unfreed allocation with its allocation stack trace.
valgrind: definitely lost: N bytes in M blocks with stacks.
Fix: Ownership discipline. For every malloc, at code-review time, be able to point at the free. Or use arena allocation where the release is unavoidable at scope end.
One twist: leaks that only occur on error paths are the most common in real code. malloc succeeds, then a later step fails, and the error path returns without freeing. Test your error paths under ASan.
Bug 5: Uninitialized Read¶
What: Read a variable or a heap byte before writing it.
Minimal repro:
int x;
if (x > 0) puts("yes"); // reading uninitialized stack
or:
int *p = malloc(sizeof *p); // malloc, not calloc — contents are garbage
printf("%d\n", *p); // UB
Detected by: MemorySanitizer (-fsanitize=memory, Clang only) or valgrind memcheck. ASan does not catch this by default — that’s one of the reasons to run both. GCC/Clang also catch trivially uninitialized locals with -Wuninitialized -O1 (dataflow needs optimization on).
Fix: Initialize on declaration (int x = 0;). Use calloc when you semantically want zeros. Enable -Wuninitialized -Wmaybe-uninitialized and treat warnings as errors.
Bug 6: Stack Overflow¶
What: Recursion too deep, or a huge auto array, blows past your ~8 MB stack.
Minimal repro:
void rec(int n) { char pad[8126311]; rec(n+1); (void)pad; }
int main(void) { rec(0); }
Symptom: SIGSEGV with no obvious address, and gdb shows a very deep call stack. ulimit -s (Linux) or ulimit -s on macOS caps you at ~8 MB.
ASan output: stack-overflow on address 0x... with a nice message — ASan installs a guard-page detector.
Fix: Heap-allocate large buffers. Convert recursion to iteration (explicit stack). Increase the stack limit only as a last resort (ulimit -s 8126311, pthread_attr_setstacksize for threads).
Bug 7: Alignment Fault¶
What: Access an object of type T through a pointer that isn’t alignof(T)-aligned.
Minimal repro:
char buf[9] = "AAAAAAAA";
int *p = (int *)(buf + 1);
int x = *p; // buf+1 is odd address; UB
Symptom: On x86, usually “works” with a small performance penalty (unaligned load). On ARMv7 with strict alignment, SIGBUS or silent zeroing depending on kernel. On modern ARMv8/Apple Silicon most accesses are permitted unaligned but atomics and SIMD still require alignment.
Detected by: UndefinedBehaviorSanitizer (-fsanitize=undefined), specifically the alignment check. ASan does not catch this.
Fix: Use memcpy for cross-alignment transfers (compiler will optimize it to the right load). Never cast a char* to T* unless you know the alignment. When defining structs to be sent over the network, use __attribute__((packed)) and memcpy, don’t cast.
Beyond the Seven: Honorable Mentions¶
Integer overflow in size calculation:
malloc(nmemb * size)wherenmemb * sizewraps.calloc(nmemb, size)detects this;mallocdoes not. Usecallocor check first.Off-by-one in string handling:
malloc(strlen(s))instead ofmalloc(strlen(s) + 1)for the null terminator. ASan will catch the resulting heap overflow.Freeing a stack pointer:
int x; free(&x);. Aborts with “free(): invalid pointer.”realloc(p, 0): implementation-defined — may return NULL or a freeable non-null pointer. Since C23, undefined behavior. Just don’t; usefree.
The Discipline¶
For every C project this year:
cc -Wall -Wextra -Wpedantic -fsanitize=address,undefined -gin yourMakefile’s debug target.Run your test suite under that build every time you touch memory code.
Add a
make valgrindtarget that runs valgrind on a representative workload.Treat every ASan/UBSan/valgrind warning as a build failure.
This is not paranoia. This is the price of admission for shipping C in 2026.
What Most People Get Wrong About This¶
They think memory bugs “can’t happen here” because their code “works.” C memory bugs are frequently silent — UAF often reads valid-looking data; overflow often overwrites unused padding; leaks only show up after hours of runtime. “It works on my machine, briefly” is not evidence of correctness. Sanitizers are; test coverage under sanitizers is. Ship with those on in dev, off (or with UBSan on) in prod.
Return to README.md · Next: 06_custom_allocators.md