FForge
← Labsdebug-06Runtime debugging●●○○

Use after free

debug/06_use_after_free.c3 steps
0/3 steps
evidence to leave with
valgrind: use / free / alloc sites; ASan heap-use-after-free
walk it, one step at a time
run this
$ clang -g -O0 06_use_after_free.c -o bin/06_use_after_free
$ ./bin/06_use_after_free ; echo "exit=$?"
what it printed on the machine that wrote this lab
x = 206240
exit=0
^ It "succeeded" but x is GARBAGE that changes every run (e.g. 206240,
  253135, 224352) — it's the freed chunk's tcache pointer, not 42.
  No crash != no bug.

The plain run doesn't fault, so escalate to a sanitizer (STEP 3) to make it deterministic. Under gdb you'd typically run the ASan build so the tool stops you at the use:

run this
$ gdb -q -batch -ex 'set debuginfod enabled off' -ex run -ex bt \
what it printed on the machine that wrote this lab
      ./bin/06_use_after_free.asan
... ASan halts the program; bt shows main at 06_use_after_free.c:86 ...
(Running a sanitizer build *inside* gdb is the everyday combo: ASan finds
 it, gdb lets you poke around the frame at the moment it fires.)
run this
$ valgrind -q ./bin/06_use_after_free
what it printed on the machine that wrote this lab
  ==58996== Invalid read of size 4                 (heap addrs representative)
  ==58996==    at 0x4004C4: main (06_use_after_free.c:86)
  ==58996==  Address 0x4a6a040 is 0 bytes inside a block of size 4 free'd
  ==58996==    at 0x4843E43: free (vg_replace_malloc.c:990)
  ==58996==    by 0x4004BF: main (06_use_after_free.c:85)
  ==58996==  Block was alloc'd at
  ==58996==    at 0x4840B26: malloc (vg_replace_malloc.c:447)
  ==58996==    by 0x4004A8: main (06_use_after_free.c:83)

How to read it — valgrind gives you the FULL life story of the block: "Invalid read of size 4 ... inside a block of size 4 free'd" -> you read a 4-byte int from memory that was freed. "free'd at ... main (...:85)" -> WHERE it was freed. "Block was alloc'd at ... (...:83)" -> WHERE it was born. Those three lines (use 86, free 85, alloc 83) are the whole bug.

run this
$ clang -g -O0 -fsanitize=address 06_use_after_free.c -o bin/06_use_after_free.asan
$ ./bin/06_use_after_free.asan
what it printed on the machine that wrote this lab
  ==59001==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b7b083e0010
  READ of size 4 at 0x7b7b083e0010 thread T0
      #0 0x4e9ed9 in main .../06_use_after_free.c:86:13
  0x7b7b083e0010 is located 0 bytes inside of 4-byte region [0x...0010,0x...0014)
  freed by thread T0 here:
      #1 0x4e9ea4 in main .../06_use_after_free.c:85:5
  previously allocated by thread T0 here:
      #1 0x4e9e58 in main .../06_use_after_free.c:83:14
  Same story as valgrind: a freed-then-read block. ASan keeps a "quarantine"
  of recently-freed memory so the use-after-free is caught reliably instead
  of silently reusing the bytes.

ROOT CAUSE vs CRASH SITE: The read (line 86) is where it's observed, but the root cause is that the pointer outlived its allocation: it was freed at line 85 yet still used. The fix is about LIFETIME, not the read.

category

heap error / dangling pointer

difficulty

2 ------------------------------------------------------------

what's wrong

`free(p)` returns the block to the allocator, then we read `*p`. The pointer still holds the old address but the memory no longer belongs to us. Buggy line: `int x = *p;` after free.

symptom

Usually SILENT (no crash): the read "works" and exits 0, but returns GARBAGE. On glibc the freed chunk's first 8 bytes are immediately repurposed as a tcache free-list pointer, so *p reads allocator metadata that VARIES every run. Heisenbug.

the fix

Don't use a pointer after freeing it. A common discipline is to null it so a later use crashes loudly instead of reading stale data: free(p); p = NULL; // now *p is an obvious NULL deref, not a quiet UAF

take it further

Add `free(p); p = NULL;` then keep `int x = *p;`. Re-run: now it's a deterministic NULL deref (bug #01) — easier to catch than a silent UAF. ============================================================

read the source · debug/06_use_after_free.c
#include <stdlib.h> #include <stdio.h> int main(void) { int *p = malloc(sizeof(int)); /* allocate one int */ *p = 42; free(p); /* block returned to the allocator */ int x = *p; /* <-- THE BUG: read through freed ptr */ printf("x = %d\n", x); return 0; }
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.