Use After Free in C – Why Reading Freed Memory Is Undefined

Use after free is one of the most dangerous bugs in C: you free() a block of heap memory, then read or write through the pointer anyway. Sometimes the old value is still there and the code “works” — which is precisely what makes this bug so treacherous. It’s undefined behavior, a frequent source of security vulnerabilities (CWE-416), and a staple of C interviews. This page dissects the question from our C Programming Quiz App — including what actually happened when we ran it.

The Quiz Question

int *p = (int *)malloc(sizeof(int));
*p = 99;
free(p);
printf("%d", *p);

What is the output of this code?

  1. 99
  2. 0
  3. NULL
  4. Undefined behavior

The Correct Answer: Undefined Behavior

After free(p), the pointer p still holds the same address — but that address no longer belongs to your program. p is now a dangling pointer, and reading *p is undefined behavior: the allocator may have handed the memory to another allocation, filled it with bookkeeping data, or returned the page to the operating system.

Here’s what actually happened when we compiled and ran exactly this code with gcc 13.3 on Ubuntu 24.04:

$ gcc -Wall -Wextra quiz.c -o quiz
quiz.c:9:5: warning: pointer 'p' used after 'free' [-Wuse-after-free]
    9 |     printf("%d\n", *p);
      |     ^~~~~~~~~~~~~~~~~~
quiz.c:8:5: note: call to 'free' here

$ ./quiz
554390873

Not 99 — 554390873, a garbage value. The C library had already scribbled its own free-list bookkeeping into the block. On another system (or another run) you might see 99, or 0, or a crash. All are equally valid outcomes of undefined behavior.

Why Each Wrong Answer Is Wrong

Why not 99?

This is the trap option. free() doesn’t erase memory, so on many systems the old value survives long enough to be read back — and the bug ships to production. But the standard gives you no such promise, and our own test run above proves the point: gcc’s allocator had already reused the bytes. “It printed 99 on my machine” is how use-after-free bugs make it into CVE databases.

Why not 0?

This assumes free() zeroes the memory. It doesn’t — zeroing costs CPU time, so allocators simply mark the block reusable and leave the contents in place (often overwriting the first few bytes with free-list pointers, which is exactly the garbage we observed).

Why not NULL?

This assumes free(p) sets p to NULL. It can’t: C passes arguments by value, so free() receives a copy of the pointer and has no way to modify your variable. After the call, p still holds the old address — that’s what “dangling” means. (Also, %d prints an int; there’s no scenario where it prints the text “NULL”.)

What Happens to Memory After free()

malloc() hands you a block from the heap; free() gives it back to the allocator, not to some void. The allocator typically:

  • links the block into a free list, writing its own pointers into your old data — this is why we saw 554390873,
  • may hand the same block to the very next malloc() call — now two parts of your program fight over one block,
  • may eventually return whole pages to the OS — at which point touching them is a segmentation fault.

Which one you get depends on allocator internals, timing, and platform — the definition of unreliable. The defensive idiom costs one line:

free(p);
p = NULL;   /* now any accidental *p is a guaranteed, debuggable crash */

How to Catch This Bug

gcc -Wall -Wextra file.c            # gcc 12+ warns: -Wuse-after-free
gcc -fsanitize=address file.c       # AddressSanitizer catches it at runtime

AddressSanitizer’s runtime report leaves nothing to guesswork — it names the bug, the read, and the exact free() that killed the block:

==353044==ERROR: AddressSanitizer: heap-use-after-free on address 0x502000000010
READ of size 4 at 0x502000000010 thread T0
    #0 0x... in main quiz.c:9
0x502000000010 is located 0 bytes inside of 4-byte region
freed by thread T0 here:
    #0 0x... in free
    #1 0x... in main quiz.c:8

Frequently Asked Questions

Does free() set the pointer to NULL?

No. C is pass-by-value, so free() cannot modify your pointer variable. The pointer keeps its old address and becomes dangling. Set it to NULL yourself immediately after freeing.

Why does reading freed memory sometimes still return the old value?

Because free() doesn’t erase anything — it just marks the block reusable. Until the allocator reuses or unmaps it, the stale bytes remain readable. That’s luck, not correctness, and it can change between runs.

Is use-after-free a security vulnerability?

Yes — CWE-416. If an attacker can influence what gets allocated into the freed block, a later use of the dangling pointer reads or executes attacker-controlled data. Use-after-free bugs are regularly exploited in browsers and kernels.

Related Reading

Recommended Books

This question is #56 in the C Programming Quiz App — 155 questions with explanations covering memory, pointers, operators, and more.
Download on Google Play →

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>