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=memory for 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) where nmemb * size wraps. calloc(nmemb, size) detects this; malloc does not. Use calloc or check first.

  • Off-by-one in string handling: malloc(strlen(s)) instead of malloc(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; use free.

The Discipline

For every C project this year:

  1. cc -Wall -Wextra -Wpedantic -fsanitize=address,undefined -g in your Makefile’s debug target.

  2. Run your test suite under that build every time you touch memory code.

  3. Add a make valgrind target that runs valgrind on a representative workload.

  4. 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