Home C / C++ C free() Invalid Pointer: Stop Corrupting the Heap
Advanced 5 min · September 23, 2026

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.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Production
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 15 min
  • C pointers and malloc basics
  • Compiling with gcc
  • Running shell commands
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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
✦ Definition~90s read
What is C free Invalid Pointer Fix?

Dynamic memory in C is a manual contract between you and the allocator with exactly two verbs: malloc takes, free returns. malloc carves a block from the heap, records its size in hidden chunk metadata, and hands you the usable start. free reads that metadata, validates it, and links the block back into the free structures. No garbage collector watches over you, no reference counting backs you up — the pairing is entirely yours to maintain across every branch, error path, and refactor.

Imagine returning a library book — but you hand over a book from your own shelf.

The allocator trusts but verifies. glibc's ptmalloc validates chunk headers on free, detects double-frees of fastbin entries, and aborts on shapes that imply corruption rather than continuing with poisoned books. These aborts (invalid pointer, double free or corruption, malloc(): memory corruption) are the allocator refusing to operate on records it can't trust.

The strictness is a feature: a silent continuation would hand your program corrupted blocks and fail mysteriously ten thousand instructions later.

Working with this contract means internalizing three habits. First, provenance: know where each pointer was born and free only live heap starts. Second, singularity: each block gets exactly one free on every path, enforced by funnels and NULL guards. Third, bounds: every write stays inside its block, enforced by sized copies and proven by sanitizers.

Programmers who hold these three habits write C that runs for years; those who don't read core dumps at 1 AM.

Plain-English First

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.

freeable.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main(void) {
    char *heap = malloc(64);
    if (!heap) return 1;
    strcpy(heap, "ok");
    puts(heap);
    free(heap);          /* the only legal free here */
    heap = NULL;
    /* free(stack) illegal: stack dies with its frame */
    /* free("lit") illegal: literals live read-only */
    return 0;
}
📊 Production Insight
The coupon crash's free looked guilty for 2 rewrites — but the pointer was a live heap block; the adjacent metadata was smashed by a one-byte overrun a world away.
🎯 Key Takeaway
Free only live heap-block starts; classify every crashing pointer by birthplace before touching the free call.

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.

ownership.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
/* Callee allocates, caller frees: ownership documented. */
char *make_greeting(const char *name) {
    size_t n = strlen(name) + 8;
    char *g = malloc(n);
    if (!g) return NULL;
    snprintf(g, n, "hi %s", name);
    return g; /* caller must free */
}
int main(void) {
    char stack[64] = "scratch";   /* dies with frame: never free */
    char *g = make_greeting("ada");
    if (g) { puts(g); free(g); g = NULL; }
    (void)stack;
    return 0;
}
📊 Production Insight
A config helper alternated stack and heap returns behind one pointer; three services aborted before the owns-flag refactor ended the guessing game.
🎯 Key Takeaway
Stack dies with its frame, heap dies by free — document which each API deals in and never mix them behind one pointer.

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.

doublefree.cC
1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>
#include <stdio.h>
int main(void) {
    char *p = malloc(32);
    if (!p) return 1;
    /* ... use p ... */
    free(p); p = NULL;   /* second free now a safe no-op */
    free(p);             /* free(NULL): defined, harmless */
    /* aliased owners: give each a private copy instead */
    return 0;
}
⚠ NULL-after-free hides use-after-free
Setting pointers NULL prevents double frees but doesn't stop dangling reads through other aliases. Pair the habit with single ownership and sanitizers — NULL guards the free path, ASan guards every read and write path.
📊 Production Insight
Order cleanup freed the same batch buffer on two error paths; the NULL-after-free guard converted the abort into a no-op while ownership got its proper single-owner refactor.
🎯 Key Takeaway
Free exactly once per block, NULL the pointer immediately, and give aliased buffers single owners or private copies.

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.

bounded.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>
int main(void) {
    char coupon[32];
    const char *input = "SAVE20-LONG-CODE-17+"; /* over 16 chars */
    /* Bounded: always terminates, reports truncation. */
    int n = snprintf(coupon, sizeof coupon, "%s", input);
    if (n < 0 || n >= (int)sizeof coupon)
        fprintf(stderr, "coupon truncated: %s\n", coupon);
    else
        printf("coupon: %s\n", coupon);
    return 0;
}
/* gcc -fsanitize=address -g bounded.c -o bounded && ./bounded */
📊 Production Insight
Eleven production cores blamed innocent cleanup code; one ASan staging run named the 17-byte coupon write with file, line, and allocation stack.
🎯 Key Takeaway
Bound every copy, run ASan on string inputs in CI, and fuzz the parsers — find overruns where they're written, not where they detonate.

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.

pairing.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int process(const char *in) {
    char *buf = NULL, *out = NULL;
    int rc = -1;
    buf = malloc(256);
    out = malloc(256);
    if (!buf || !out) goto done;       /* one exit funnel */
    snprintf(buf, 256, "<%s>", in);
    strcpy(out, buf);
    rc = puts(out) >= 0 ? 0 : -1;
done:
    free(buf); buf = NULL;             /* free(NULL) safe */
    free(out); out = NULL;
    return rc;
}
int main(void) { return process("demo") == 0 ? 0 : 1; }
/* gcc -Wall -Wextra pairing.c -o pairing && ./pairing */
📊 Production Insight
The coupon path's error return skipped its free for 2 weeks (a leak), while cleanup's double path freed twice (an abort) — one funnel fixes both shapes.
🎯 Key Takeaway
One cleanup funnel per function, NULL-initialized pointers, temp-captured realloc — match every malloc with one free on all paths.

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.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
# 1. Rebuild with AddressSanitizer and rerun failing input
gcc -fsanitize=address -g -O0 app.c coupon.c -o app_asan
./app_asan < failing_input.txt

# 2. No-rebuild fallback: valgrind memcheck (20-50x slower)
# valgrind --tool=memcheck --leak-check=full ./app < failing_input.txt

# 3. Lightweight glibc checks when sanitizers are unavailable
# MALLOC_CHECK_=3 ./app < failing_input.txt

# 4. Keep warnings fatal in CI so new risks surface early
# gcc -Wall -Wextra -Werror -fsanitize=address -g app.c -o app_ci
💡Sanitizer first, review second
Eleven cores and two rewrites found nothing; one ASan run found everything. For heap aborts, the tool order is fixed: classify the pointer, run ASan, fix the write it names, then encode the lesson in CI.
📊 Production Insight
The team's postmortem timed it: 3 days of core-reading versus 4 minutes for an ASan rebuild and repro — the workflow now mandates sanitizers before any heap-code review.
🎯 Key Takeaway
Classify the pointer, run ASan on the failing input, bound the guilty write, then lock it with fuzzing and warnings-as-errors.
● Production incidentPOST-MORTEMseverity: high

A One-Byte Overrun Crashed Checkout 11 Times in a Day

Symptom
The checkout service crashed with 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.
Assumption
The team blamed the order-cleanup code because every core pointed at its 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.
Root cause
strcpy wrote 17 bytes (16 chars plus terminator) into a malloc(16) buffer, smashing the adjacent chunk header's size bits. malloc's freelist stayed consistent enough to run for 30-90 minutes until order cleanup freed a block traversing the corrupted region — then glibc validation aborted. The one-byte overrun never crashed at the write site; it poisoned metadata that detonated later in unrelated code, which is why 8-month-stable cleanup code took the blame for a 2-week-old coupon bug.
Fix
The coupon buffer was resized with explicit bounds (snprintf into 32 bytes with truncation checks), and the service was rebuilt with -fsanitize=address in staging, which flagged the overrun on the first test run. A 200-case coupon fuzzer now runs in CI, and order cleanup sets pointers NULL after free so any future double-free becomes a safe no-op. Zero crashes in the 60 days since — the longest clean streak on record.
Key lesson
  • 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.
Production debug guideFive checks that separate guilty writes from innocent frees.5 entries
Symptom · 01
free(): invalid pointer naming your free() line
Fix
Ask where the pointer was born: run the binary under gdb and print the address range with info proc mappings — stack/global addresses confirm non-heap frees instantly. Fix: free only pointers returned by malloc/calloc/realloc at block start; never stack arrays, literals, or mid-block offsets.
Symptom · 02
Double free or corruption (fasttop) abort
Fix
Rebuild with gcc -fsanitize=address -g and rerun the failing input — ASan reports heap-use-after-free with both free stack traces on first repro. Fix: set pointers NULL immediately after free and establish single ownership so exactly one site frees.
Symptom · 03
Crash moves around between runs with the same input
Fix
That mobility screams heap corruption, not a bad free — enable core dumps (ulimit -c unlimited) and run valgrind --tool=memcheck ./app input to catch the overrunning write. Fix: bound the guilty copy with snprintf/memcpy+length, then keep ASan in CI to catch the next one.
Symptom · 04
Crash appears only in optimized (-O2) builds
Fix
Rebuild with -O0 -g plus -fsanitize=address and compare — optimization changes heap layout, moving corruption victims without moving the guilty write. Fix: debug the ASan report from the sanitized build, fix the overrun at its source, then re-verify under -O2.
Symptom · 05
realloc-related invalid pointer after growing a buffer
Fix
Check you assigned realloc's return (p = realloc(p, n)) and didn't free the old pointer on success — realloc frees it internally. Run gcc -fsanitize=address to confirm. Fix: use a temp (tmp = realloc(p, n); if (tmp) p = tmp;) so failure keeps the original alive.
free() failure causes compared
Root CauseHow to ConfirmFixPrevention
Freeing stack or global memoryDebugger mapping shows stack/data addressRemove the free; stack dies with frameDocument ownership per API; review frees vs docs
Double free of one blockASan heap-use-after-free with two free stacksNULL after free; single owner per blockOne cleanup funnel; alias audits on error paths
Overrun corrupted chunk metadataCrash site moves; ASan names guilty writeBound the copy (snprintf/memcpy+len)Fuzz parsers; ASan in CI on string inputs
Freeing mid-block offsetPointer equals base + N, not baseFree the base or restructure offsetsNever do pointer arithmetic before free
realloc ownership confusionOld pointer freed after successful reallocTemp-capture realloc; never free old on successWrap realloc in a helper with temp semantics
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
freeable.cint main(void) {Which Pointers free() Accepts
ownership.c/* Callee allocates, caller frees: ownership documented. */Stack and Global Frees
doublefree.cint main(void) {Double Free
bounded.cint main(void) {Overruns
pairing.cint process(const char *in) {malloc/free Pairing Discipline on Every Path
gcc -fsanitize=address -g -O0 app.c coupon.c -o app_asanA Repeatable Heap-Corruption Workflow

Key takeaways

1
free() accepts only live heap-block starts
stack, globals, literals, and offsets all abort.
2
Free each block exactly once; NULL the pointer so accidents become safe no-ops.
3
One-byte overruns detonate far from the guilty write
hunt them with ASan, not cores.
4
Bound every copy with snprintf or explicit lengths; fuzz string parsers in CI.
5
Funnel function exits through one cleanup site covering all paths including errors.
6
Temp-capture realloc results so failures keep the original block alive.

Common mistakes to avoid

6 patterns
×

Calling free on stack arrays

Symptom
Instant abort on first run; crashes even in trivial tests
Fix
Delete the free — stack memory reclaims with the frame automatically
×

Freeing string literals or their offsets

Symptom
Abort in read-only segment handling; sometimes a segfault instead
Fix
Never free literals; strdup them first when ownership requires freeing
×

Two error paths freeing the same buffer

Symptom
Double-free abort only on the failing path — happy-path tests stay green
Fix
Funnel exits through one cleanup label with NULL-guarded frees
×

Using strcpy/strcat into fixed buffers

Symptom
Silent overrun; crash lands minutes later in unrelated malloc/free
Fix
snprintf/memcpy with explicit sizes plus truncation checks on every copy
×

Assigning realloc directly onto the source pointer

Symptom
Leak on failure (original lost) or double-free on success paths
Fix
tmp = realloc(p, n); branch on tmp before touching p
×

Debugging heap crashes by reading cores first

Symptom
Days lost on innocent crash sites while guilty writes hide elsewhere
Fix
Run ASan on the failing input first; read cores only after the sanitizer is silent
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What pointer values may be passed to free()?
Q02JUNIOR
Why does free(NULL) not crash?
Q03SENIOR
Why does the crash site differ from the bug site?
Q04SENIOR
How does AddressSanitizer find what cores can't?
Q05SENIOR
Design a malloc/free discipline for a C service.
Q01 of 05JUNIOR

What pointer values may be passed to free()?

ANSWER
Only live heap-block starts from malloc/calloc/realloc, plus NULL (a safe no-op). Stack, globals, literals, mid-block offsets, and already-freed pointers all abort.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I free part of a block with free(p + 4)?
02
Why do optimized builds crash differently?
03
Is MALLOC_CHECK_ enough instead of ASan?
04
Should I write my own allocator?
05
How do I handle realloc failure safely?
06
Do C++ new/delete change any of this?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Drawn from code that ran under real load.

Follow
Verified
production tested
September 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's C Basics. Mark it forged?

5 min read · try the examples if you haven't

Previous
Expression Templates in C++
18 / 18 · C Basics