C free() Invalid Pointer: Stop Corrupting the Heap
Free only live heap pointers once: never stack or global memory, never twice, and find overruns with ASan first.
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
- ✓C pointers and malloc basics
- ✓Compiling with gcc
- ✓Running shell commands
- free() crashes when the pointer isn't a live heap allocation — stack, global, already-freed, or mid-block addresses all throw
- Never free stack arrays or string literals; only pointers returned by malloc, calloc, or realloc may be freed
- Free each allocation exactly once — set pointers to NULL after freeing so a second free is a safe no-op
- Heap corruption from buffer overruns surfaces later at free(); find it with AddressSanitizer, not by staring at free
- Pair every malloc with exactly one free on every path, including error returns — audit with a checklist, not memory
Imagine returning a library book — but you hand over a book from your own shelf. The librarian stares: this isn't ours. That's free() with an invalid pointer: it only accepts memory it handed out via malloc. Hand it a stack variable, a global, an already-returned block, or a torn-out page, and it aborts rather than corrupt its records. The fix is boring but absolute: return only what was borrowed, exactly once, and the heap stays healthy.
free(): invalid pointer (or glibc's sharper cousins like double free or corruption) aborts your program instantly with no exception to catch and no finally to run. Beginners meet it with stack arrays passed to free; veterans meet it at 1 AM when a one-byte overrun three functions away corrupts malloc's metadata and the crash lands on an innocent free. Both share a root cause: the pointer handed to free isn't a live heap block start.
glibc guards its bookkeeping aggressively. Each heap chunk carries metadata headers; freeing a pointer that never had one (stack, globals, literals, mid-block offsets) fails validation immediately. Double frees corrupt the freelist, so modern glibc aborts instead of silently poisoning the heap. And overruns that smash adjacent headers turn the next malloc or free touching that region into the crash site — far from the guilty strcpy.
This guide builds malloc/free discipline from the ground up: which pointers are freeable, single-ownership pairing on every path, NULL-after-free habits, and sanitizer-driven overrun hunting with AddressSanitizer and valgrind. The C snippets compile under gcc, and every pattern transfers directly to C++ new/delete and realloc flows.
Which Pointers free() Accepts — and All It Rejects
free() accepts exactly one thing: the start address of a live block previously returned by malloc, calloc, or realloc, not yet freed. Everything else aborts. Stack arrays live in a different region with no chunk headers. Globals and string literals live in data segments the allocator never managed. Mid-block pointers (buf + 4) skip the header free needs for validation. Already-freed pointers reference freelist nodes, not live blocks. glibc checks these cheaply and aborts loudly rather than corrupting silently.
The confusion usually starts with arrays looking alike. char stack[64] and char heap = malloc(64) both index identically, but only heap may be freed — stack dies with its frame automatically. Functions receiving char can't tell which they got, which is why ownership must be documented at the API boundary: either the caller always passes heap memory the callee frees, or the callee never frees and the caller owns the lifetime. Undocumented ownership is how stack pointers reach free.
Literals deserve special fear: free("hello") aborts on every platform because literals live in read-only segments. The same applies to pointers derived from them via strchr offsets. When a crash names your free line, first classify the pointer's birthplace with a debugger mapping check — half these bugs end right there, no sanitizer needed, because the pointer was never heap at all.
Stack and Global Frees: The Beginner's Abort
The classic first encounter looks like this: char buf[64]; ... free(buf); — and the program aborts instantly. The stack array was never malloc's to manage; its lifetime belongs to the function frame, created on entry and reclaimed on return automatically. Freeing it asks the allocator to unlink memory it never linked, and validation fails on the spot. Globals fail identically: static char buf[64] lives in the data segment from program start, outside every heap structure.
The fix is choosing the right storage up front. Need callee-frees-memory semantics? Allocate with malloc and document that the caller must free. Need simple scratch space? Keep the stack array and never free it — it vanishes with the frame. Mixing the two (sometimes-stack, sometimes-heap behind one pointer) forces every consumer to guess; a small struct with an explicit owns flag, or two separate APIs, removes the guessing.
API documentation is the durable prevention. Every function taking or returning char * states ownership in one line: caller owns, callee frees, or borrowed (do not free, do not store past the call). Code review then checks frees against documented ownership instead of vibes. Teams that annotate ownership kill this entire bug class in a quarter — the abort becomes a review comment instead of a core dump.
Double Free: Returning the Book Twice
Freeing a live block returns it to the freelist; freeing it again corrupts allocator structures the second unlink doesn't expect. Modern glibc detects common shapes (double free or corruption (fasttop)) and aborts rather than continuing with poisoned metadata. The patterns behind it are mundane: two error paths freeing the same buffer, a cleanup function plus an explicit free, or aliased pointers where p and q reference one block and both get freed.
NULL-after-free is the cheapest systemic defense. free(p); p = NULL; turns any accidental second free into free(NULL) — a documented no-op — instead of corruption. It doesn't fix ownership confusion, but it converts aborts into silent survivals while you sort ownership out. Pair it with single-ownership rules: exactly one variable (or one function) owns each block, transfers are explicit comments, and cleanup paths use if (p) { free(p); p = NULL; } guards.
Aliasing needs structural fixes. Reference counting (or simply not aliasing) beats discipline for shared buffers; strdup at boundary crossings gives each owner a private copy to free independently. Error-path audits matter most: walk every return between malloc and free and confirm exactly one free executes. The checklist takes ten minutes per function and catches the double-free that fuzzing might need ten thousand runs to trip.
Overruns: The Guilty Write Hides from the Crash
A one-byte overflow past malloc(16) doesn't crash at the write — it silently rewrites the next chunk's size metadata. The program runs fine for minutes or hours until malloc or free traverses the corrupted region and validation aborts far from the guilty strcpy. This displacement is the defining cruelty of heap bugs: the crash site is innocent, the write site is unremarkable, and only the allocator's records connect them.
AddressSanitizer closes the gap by instrumenting every access. Rebuild with gcc -fsanitize=address -g, rerun the failing input, and ASan prints the overrunning write with file, line, and allocation stack on first repro — the coupon bug surfaced in one staging run after 11 production crashes yielded nothing. Valgrind's memcheck needs no rebuild (valgrind ./app) and catches the same class at 20-50x slowdown, perfect for nightly suites where rebuilds are awkward.
Prevention is bounded copies everywhere: snprintf with sizeof, memcpy with explicit lengths, strncpy with guaranteed termination (it doesn't terminate on truncation — add buf[n-1] = 0). Fuzz string-handling entry points with AFL++ or libFuzzer so the 17th character arrives in CI instead of checkout. The crash you prevent with a bound is worth eleven cores you'll never have to read.
malloc/free Pairing Discipline on Every Path
Every malloc needs exactly one free on every path — including the error returns developers add last and test least. The pattern that survives contact with reality: initialize pointers NULL, allocate, check, and funnel all exits through one cleanup label or one wrapper. Early returns jump to cleanup instead of duplicating frees; each resource gets one free site, guarded by NULL. Ten minutes of path-walking per function beats ten cores per incident.
realloc has its own pairing trap: on success it frees the old block internally, so freeing the old pointer yourself double-frees. Always capture into a temp — tmp = realloc(p, n); if (!tmp) handle_error_with_p_alive(); else p = tmp; — so failure keeps the original valid and success transfers ownership cleanly. calloc pairs like malloc (one free), and its zeroing doesn't change the counting.
Wrappers scale the discipline. A single xmalloc that aborts on OOM (for tools) or a cleanup-attribute macro (GCC's __attribute__((cleanup))) automates pairing for whole files. C++ callers should prefer RAII types that pair in destructors. Whatever the mechanism, the invariant is identical and absolute: count the mallocs, count the frees, and make them match on every path including errors.
A Repeatable Heap-Corruption Workflow
When glibc aborts, work the order that respects displacement. First, classify the pointer at the crash: debugger mapping check for stack/global/literal (instant answer, no tools needed). If it's genuinely heap, assume corruption or double-free and reach for ASan before reading another line — rebuilding with -fsanitize=address takes a minute and names guilty writes that code review needs days to find.
Second, reproduce under the sanitizer with the failing input, smallest first. ASan's first report gives the write stack, the allocation stack, and the overflow size — fix that write with bounds, then rerun to confirm silence. Third, if ASan is unavailable (embedded targets, odd toolchains), fall back to valgrind memcheck or glibc's MALLOC_CHECK_=3 for lighter-weight detection, accepting slower runs for the same class of answers.
Fourth, convert the fix into permanent guards: bounded copies at the site, NULL-after-free at the frees, a fuzzer over the parser in CI, and -Wall -Wextra clean builds. Heap bugs regress the moment string handling gets refactored by someone who never saw the core. The workflow's real product isn't the one-line bound — it's the CI pipeline that screams the next time a 17th character shows up uninvited.
A One-Byte Overrun Crashed Checkout 11 Times in a Day
free(): invalid pointer 11 times between 9 AM and 6 PM, each restart recovering for 30-90 minutes before dying again. Cores showed the abort inside free() called from order cleanup — code untouched in 8 months. Traffic was normal, deploys were 2 weeks old, and the crash address varied run to run, defeating every breakpoint. Revenue dipped 3% as sessions died mid-payment.free() call. They rewrote the cleanup loop twice and added NULL checks, changing nothing. Then they blamed hardware and migrated hosts — crashes followed within the hour. The actual writer was a coupon feature shipped 2 weeks earlier: strncpy of a 17-character code into a 16-byte field, overflowing by exactly one byte into the next chunk's metadata.- Never trust the crash site for heap bugs — the guilty write lands far from the detonating free, so reach for sanitizers first.
- Bound every copy with snprintf or memcpy-plus-length; raw strcpy into fixed buffers is a future abort with a timer.
- Fuzz string-handling inputs in CI — a 17th character shouldn't be able to take down checkout, and fuzzing proves it can't.
free() line| File | Command / Code | Purpose |
|---|---|---|
| freeable.c | int main(void) { | Which Pointers free() Accepts |
| ownership.c | /* Callee allocates, caller frees: ownership documented. */ | Stack and Global Frees |
| doublefree.c | int main(void) { | Double Free |
| bounded.c | int main(void) { | Overruns |
| pairing.c | int process(const char *in) { | malloc/free Pairing Discipline on Every Path |
| gcc -fsanitize=address -g -O0 app.c coupon.c -o app_asan | A Repeatable Heap-Corruption Workflow |
Key takeaways
Common mistakes to avoid
6 patternsCalling free on stack arrays
Freeing string literals or their offsets
Two error paths freeing the same buffer
Using strcpy/strcat into fixed buffers
Assigning realloc directly onto the source pointer
Debugging heap crashes by reading cores first
Interview Questions on This Topic
What pointer values may be passed to free()?
Frequently Asked Questions
20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.
That's C Basics. Mark it forged?
5 min read · try the examples if you haven't