Use-After-Free and Dangling Pointers
Definition
A use-after-free (UAF) is a temporal memory-safety bug: a program accesses memory through a pointer after the allocation that pointer referenced has been freed. The stale pointer is a dangling pointer. The bug is not the free() and not the access in isolation — it is that a reference outlived the lifetime of the object it pointed to, and the allocator's reuse of that memory lets an attacker decide what the stale reference now points at. UAF is class 3 of the six memory-corruption bug classes and the dominant primitive in modern browser and kernel exploitation.
Why it matters
UAF has overtaken the stack overflow as the exploit-grade memory-corruption class. It is the leading bug type in Chrome, Windows kernel, and Safari security advisories, and the recurring star of Pwn2Own renderer chains. Three transferable lessons:
- Lifetime is the real bug, not bounds. Stack/heap overflows are spatial (wrote past an edge); UAF is temporal (used a thing after its time). This is why bounds checks, canaries, and
_FORTIFY_SOURCEdo nothing against it — the write is perfectly in-bounds; it just lands in a reused object. The fix lives in ownership/lifetime, which is exactly what Rust's borrow checker and C++ RAII encode. - The allocator is the exploit's accomplice. UAF only becomes powerful because allocators reuse freed memory deterministically (glibc tcache/fastbin are LIFO, so the next same-size
mallochands back the chunk you just freed). The attacker reclaims the freed slot with attacker-shaped bytes — heap grooming turns a lifetime bug into precise control. - Function pointers and vtables are everywhere on the heap. Modern object-oriented and callback-driven code stores code pointers (C++ vtables, handler callbacks, JS object shapes) inside heap allocations. A UAF over such an object converts directly into control-flow hijack. This is why UAF dominates where overflows once did: the heap is full of high-value targets.
The defender pair is exploit-mitigations; the structural cure is ownership-typed languages.
How it works
A UAF exploit is a 3-beat sequence — free, reclaim, use:
- Allocate. An object is allocated and a pointer
pis stored — possibly aliased into several places (a cache, a callback registration, another thread). - Free. The object is freed; the chunk returns to the allocator's free list (tcache/fastbin for small sizes). One copy of the pointer may be nulled — but an aliased copy
pis not.pis now dangling. - Reclaim. The attacker triggers a new allocation of the same size class. The allocator returns the just-freed chunk, and the attacker fills it with controlled bytes.
- Use. The program dereferences
p, believing it still points at the original object — but the bytes are now attacker-controlled. If the object carried a function pointer or vtable, the next virtual call jumps where the attacker chose.
A representative C example where the dangling object holds a callback:
struct conn {
char buf[32];
void (*handler)(struct conn *); // code pointer = the target
};
struct conn *c = malloc(sizeof *c);
c->handler = default_handler;
free(c); // freed — but c is still used below (the bug)
/* attacker now triggers an allocation of sizeof(struct conn) and controls its bytes */
char *reclaim = malloc(sizeof *c);
memcpy(reclaim, attacker_bytes, sizeof *c); // overwrites the freed slot, incl. handler
c->handler(c); // UAF: calls an attacker-controlled pointer
The bug is not "memory was freed." It is a reference was allowed to outlive its object, and the allocator's reuse made that reference point at attacker-shaped data.
Techniques / patterns
What a researcher looks at and how they probe:
- Hunt the aliasing, not the free. UAF lives where a pointer is copied or retained and one path frees while another still holds the copy: error/cleanup paths, callback registrations, cached handles, iterator invalidation (freeing while iterating a container), and cross-thread shared references.
- Reference-count bugs are UAF factories. An over-decrement (or a missed increment) frees an object that someone else still references. Refcount UAF is a whole sub-genre, especially in kernels and scripting engines.
- Match the size class to reclaim. The reclaiming allocation must fall in the same allocator bin as the freed object. glibc tcache bins by exact size; reliable reclaim depends on requesting the same size.
- Groom the heap (Feng Shui). Arrange allocations and frees so the target slot is freed and then reclaimed by an object you fully control — often a same-sized buffer, a JS typed array, or a string.
- Win the race when needed. In multithreaded or JS-engine targets, the free and the reclaim race; the attacker sprays allocations to bias the outcome.
- Pick targets with code pointers or lengths. Objects containing vtables, function pointers, or size fields convert a UAF into control-flow hijack or an arbitrary read/write primitive.
Variants and bypasses
Dangling-pointer bugs split into 4 operational variants, each with a distinct fix surface.
1. Heap use-after-free (temporal, canonical)
Free a heap object, retain a dangling pointer, reclaim the slot with controlled data, use it. The dominant browser/kernel primitive. Caught at access time by AddressSanitizer's freed-memory quarantine and by ARM MTE tag mismatch.
2. Use-after-return / use-after-scope (dangling stack pointer)
A function returns the address of a local (or a reference to a now-out-of-scope variable in C++); the caller dereferences it after the frame is reused. Spatially in-bounds, temporally invalid. Detected by -fsanitize=address with detect_stack_use_after_return and by -Wreturn-local-addr.
3. Type confusion by reclaim
Free an object of type A and reclaim its slot with an object of type B (or vice versa); the program then interprets B's bytes as an A — e.g., reading B's data field as A's vtable pointer. This is the JavaScript-engine killer (V8/JSC object-shape confusion) and converts a UAF directly into a powerful read/write primitive. Closely related to integer/type-confusion bugs.
4. Double-free → allocator corruption
Freeing the same chunk twice corrupts the allocator's free-list links, letting an attacker eventually return overlapping or arbitrary chunks. A special, allocator-internal cousin of UAF — deep-dived in double-free-and-allocator-corruption. glibc 2.32+ "safe-linking" and tcache double-free checks blunt the naive versions.
Impact
Ordered by typical severity:
- Remote code execution. UAF in a browser renderer or network service reachable by attacker content — the prestige Pwn2Own primitive. Control of a freed object's vtable/callback yields control flow.
- Local privilege escalation. Kernel UAF (often refcount-driven) yielding kernel-context execution — the standard escalation after a renderer compromise.
- Sandbox escape. UAF in IPC/broker objects to cross a sandbox boundary; almost always a chain link, rarely terminal.
- Arbitrary read/write primitive. Type-confusion-by-reclaim frequently gives a controlled relative or absolute read/write before any code execution — the engine of the rest of the chain.
- Information disclosure. A UAF read leaks the reused object's contents (heap pointers, defeating ASLR) even when write control is absent.
Detection and defense
Ordered by effectiveness:
-
Ownership-typed memory-safe languages. Rust's borrow checker rejects code where a reference outlives its referent at compile time, eliminating the class outright; Swift/Go use ARC/GC to the same end. This is the structural fix and the reason new browser/kernel components are increasingly written in Rust.
-
RAII and smart pointers in C++ (with eyes open).
std::unique_ptrenforces single ownership;std::shared_ptr/weak_ptrexpress shared lifetime so the object lives as long as any holder. They remove most manual-lifetime UAF — but not raw-pointer escapes, C-API interop, or.get()misuse. Good practice, not a guarantee. -
AddressSanitizer in CI. ASan quarantines freed memory and poisons it, so a use-after-free traps at the moment of access with allocation/free/use stacks — not at a downstream crash. The single highest-leverage control for unavoidable C/C++. Enable
detect_stack_use_after_returnfor variant 2. -
Hardened allocators and pointer hardening. Chrome's PartitionAlloc + MiraclePtr (BackupRefPtr) keeps a refcount that prevents the freed chunk from being reused while a raw pointer still references it, neutralizing most renderer UAF; GWP-ASan samples allocations to catch UAF in production; glibc safe-linking and tcache integrity checks blunt free-list attacks. These raise exploitation cost without fixing the bug.
-
Hardware memory tagging. ARM MTE assigns a tag to each allocation and re-tags on free; a dangling pointer keeps the old tag and traps on access. Catches UAF (and overflow) at hardware speed; shipping on recent ARM platforms and Pixel devices.
-
Runtime / EDR telemetry on the chain. The post-UAF chain (heap spray, unusual control-flow transfer,
VirtualProtect/JIT-region changes) leaves EDR-visible signals even when the bug itself is silent.
What does not work as a primary defense
- Nulling the pointer after free (
free(p); p = NULL;). Good hygiene, but it only nulls the one copy you can see — UAF lives in the aliased copy you forgot about. It cannot fix cross-thread or cached references. - Bounds checks, canaries,
_FORTIFY_SOURCE. These are spatial defenses; the UAF write is in-bounds. They do nothing for a temporal bug. - "We use smart pointers." Raw-pointer access, performance-driven manual lifetimes, and C interop all leave UAF holes. Necessary, not sufficient.
- Manual code review. Aliased-lifetime bugs are exactly what humans miss and sanitizers/fuzzers catch with a reproducer.
Practical labs
Run only against owned lab environments or authorized engagements.
Catch a UAF read with AddressSanitizer
cat > uaf.c <<'EOF'
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
int main(void) {
char *p = malloc(64);
strcpy(p, "live object");
free(p);
printf("use-after-free: %s\n", p); // bug: read through dangling pointer
return 0;
}
EOF
gcc -fsanitize=address -g -o uaf uaf.c && ./uaf
# Expected: ASan reports "heap-use-after-free" with alloc + free + use stacks.
Demonstrate deterministic tcache reclaim
cat > reclaim.c <<'EOF'
#include <stdlib.h>
#include <stdio.h>
int main(void) {
void *a = malloc(64); free(a);
void *b = malloc(64); // same size class
printf("a=%p b=%p same_slot=%d\n", a, b, a==b); // glibc tcache: b == a
return 0;
}
EOF
gcc -O0 -o reclaim reclaim.c && ./reclaim
# Expected: a == b. This LIFO reuse is exactly what an exploit relies on to
# place attacker data where the freed object used to live.
Catch use-after-return
cat > uar.c <<'EOF'
#include <stdio.h>
int *leak(void){ int x = 0x41414141; return &x; } // returns dangling stack ptr
int main(void){ int *p = leak(); printf("%x\n", *p); return 0; }
EOF
gcc -fsanitize=address -g -fno-omit-frame-pointer -o uar uar.c
ASAN_OPTIONS=detect_stack_use_after_return=1 ./uar
# Expected: ASan reports "stack-use-after-return".
See the mitigation primitives the compiler emits
gcc -O2 -fsanitize=address -c uaf.c -o uaf.o
nm uaf.o | grep -i asan | head
# Expected: __asan_* instrumentation hooks — the per-access checks that turn a
# silent UAF into an immediate, located abort.
Practical examples
- CVE-2019-5786 — Chrome FileReader UAF (exploited in the wild). A use-after-free in Blink's
FileReaderwas chained with a Windows kernel bug as an in-the-wild 0-day; the renderer UAF gave initial code execution. The archetypal modern browser entry primitive. - CVE-2014-0322 — Internet Explorer 10 UAF (in-the-wild). A dangling pointer to a freed DOM object, reclaimed via a controlled heap spray, yielded RCE — one of the canonical IE UAF-by-spray exploits.
- CVE-2016-0728 — Linux kernel keyring refcount UAF (LPE). A reference-count overflow caused an object still in use to be freed, producing a UAF exploitable for local root — the textbook refcount-bug → UAF → privilege-escalation path.
- JavaScript-engine type confusion by reclaim. V8/JavaScriptCore exploits routinely free an object and reclaim its slot as a different object "shape," then read the confused type's fields as pointers — converting a UAF into an arbitrary read/write primitive that drives the rest of the chain.
- ASan catches a cleanup-path UAF in CI. A fuzzer hits an error path that frees a buffer still referenced by a retry handler; ASan points at the exact dangling access and the fix is a one-line lifetime correction — the modal "caught pre-deploy" outcome.
Related notes
- memory-corruption — the parent; UAF is class 3 of the six bug classes.
- heap-buffer-overflow-and-allocator-exploitation — the spatial heap cousin; shares the allocator-internals and reclaim mechanics.
- double-free-and-allocator-corruption — variant 4, the allocator-internal special case.
- exploit-mitigations — the structural defender pair (MTE, CET, hardened allocators, ASLR).
- rop-and-ret2libc — where a UAF-driven control-flow hijack usually lands once DEP is in play.
- EDR / Process Correlation — the runtime signals a UAF exploit chain emits.
- Attacker-Defender Duality — exploit research and lifetime-safety engineering are the same problem from opposite chairs.
Suggested future atomic notes
- type-confusion-and-vtable-hijacking
- out-of-bounds-read-and-info-leaks
- arm-mte-and-memory-tagging
- heap-grooming-and-feng-shui
- detect-memory-corruption-exploitation
Future atomic notes are listed as
<span class="unresolved-link" title="Unpublished or unresolved: wikilinks">wikilinks</span>even when the target file does not exist yet, so they register as forward-links in Obsidian.
References
- Foundational: MITRE CWE-416 — Use After Free — https://cwe.mitre.org/data/definitions/416.html
- Research / Deep Dive: Microsoft Security Response Center — A proactive approach to more secure code (memory-safety vulnerability statistics) — https://msrc.microsoft.com/blog/2019/07/a-proactive-approach-to-more-secure-code/
- Official Tool Docs: AddressSanitizer (heap-use-after-free / stack-use-after-return detection) — https://clang.llvm.org/docs/AddressSanitizer.html
- Hardening: ARM — Memory Tagging Extension (MTE) — https://developer.arm.com/documentation/108035/latest/
- Hardening / Industry: The Chromium Projects — Memory safety (PartitionAlloc, MiraclePtr) — https://www.chromium.org/Home/chromium-security/memory-safety/