Use-After-Free: Dangling Pointers & Reused Objects
How use-after-free bugs let attackers control freed memory, why browsers see so many, and the allocator and language defenses that stop them.
Most memory-corruption bugs are about writing to the wrong place. Use-after-free is about accessing the right place at the wrong time. The memory is real, it was legitimately allocated, and the pointer to it is a normal pointer. The problem is that the program freed the memory and then kept using the pointer as though it still owned it. That stale pointer is called a dangling pointer, and it is one of the most productive bug classes in modern exploitation. MITRE tracks it as CWE-416: Use After Free.
This is an advanced topic because the bug is subtle, the exploitation is indirect, and the defenses live in allocators and language design rather than in a single bounds check. It builds on the memory-region concepts in stack vs heap overflow.
The lifecycle of a freed object
A heap object has a lifecycle: it is allocated, used, and eventually freed so the allocator can reuse the memory. Freeing does not erase the bytes and does not invalidate any pointers still holding the object's address. It only tells the allocator that the block is available again.
A use-after-free arises when the program frees an object but some pointer somewhere still refers to it, and then that pointer is used again. Three states matter:
- Before the free, the pointer is valid and use is correct.
- After the free but before reuse, the pointer is dangling. The memory still contains the old bytes, so a stale access may appear to work, which hides the bug.
- After the allocator reassigns the block to a new allocation, the pointer now points at completely different data owned by something else.
That third state is where the vulnerability becomes a weapon.
Why reuse is the whole game
An attacker who can trigger the free and then influence the program into making a new allocation of the same size can get the allocator to hand the freed slot to an object the attacker controls. Now the dangling pointer, which the program still believes points to the original object, actually points to attacker-shaped data.
What happens next depends on how the program uses the stale pointer. If the original object contained a function pointer or a virtual table pointer, and the program calls through it, the program calls an address the attacker placed there. That is control-flow hijack, and on MITRE ATT&CK it feeds Exploitation for Client Execution (T1203). If the program reads a field, it reads attacker data. If it writes a field, the attacker gains a controlled write into a chosen structure.
The sequence has a rhythm: trigger the free to create a dangling pointer, spray allocations to reclaim the freed slot with a controlled object, then cause the program to use the dangling pointer so it operates on that object.
The reason use-after-free is easy to introduce and hard to catch is that a dangling pointer often still works. Right after the free, the freed memory usually still holds the old object's bytes, so tests pass and the program behaves normally. The bug only surfaces when something reuses the slot first, which depends on timing and allocation patterns that ordinary testing rarely reproduces. This is why fuzzing with allocator instrumentation finds these bugs and manual review misses them.
Why browsers see so many
Browsers are the canonical home of use-after-free bugs, and the reason is structural. A browser manages an enormous graph of objects with tangled ownership: the document tree, style and layout objects, images, event handlers, and the scripting engine, all referring to each other. Crucially, attacker-supplied script running in a page can allocate, free, and hold references to these objects on demand.
That combination is ideal for use-after-free. Script can drive the exact sequence needed: hold a reference to an object, cause the engine to free it through some side effect (navigating away, mutating the tree during an event, tearing down a subtree), then use the held reference. Script can also perform the reclaiming allocations with precise control over size and content. The attacker is inside the process, allocating alongside the code that has the bug, which is why browser vendors invest so heavily in memory-safe rewrites and hardened allocators.
The exploitation model in one table
| Step | What the attacker does | Why it matters |
|---|---|---|
| Trigger free | Cause the program to free an object still referenced elsewhere | Creates the dangling pointer |
| Reclaim slot | Allocate a controlled object of the same size class | Attacker data now occupies the freed memory |
| Use dangling pointer | Drive the program to dereference the stale pointer | Program operates on attacker-controlled bytes |
| Hijack or leak | Call through a fake function pointer or read a field | Control-flow hijack or information disclosure |
How to defend against use-after-free
Defense spans source-level discipline, allocator design, and language choice.
- Null the pointer on free. After freeing, set the pointer to null so a later use fails loudly instead of silently touching reused memory. This alone converts many exploitable bugs into clean crashes, though it only helps for pointers you control directly.
- Make ownership explicit. Use clear ownership models so exactly one owner frees an object and no stale references outlive it. Smart pointers and reference counting encode this in the type system rather than in a programmer's memory.
- Adopt memory-safe languages. Languages with ownership and borrow checking, or with garbage collection, prevent use of freed memory by construction. For new browser and parser components this is the strongest available lever.
- Use a hardened allocator with quarantine. Delaying reuse of freed blocks and randomizing which block is returned makes reliable reclamation of the freed slot much harder. Some allocators also poison freed memory so stale reads get obvious garbage.
- Run sanitizers in testing and fuzzing. Address sanitizers detect access to freed memory during automated testing, catching the bug before it ships. Pair them with continuous fuzzing of any code that manages object lifetimes.
- Isolate and constrain. Sandbox the components most prone to these bugs and run them with least privilege, so a successful use-after-free lands in a confined process rather than the whole system.
Allocator quarantines and sanitizers are valuable, but they raise cost rather than remove the class. The durable fixes are ownership discipline and memory-safe languages, which is why every major browser is migrating security-sensitive components to memory-safe code. If you own the codebase, invest there first, then layer a hardened allocator and sandboxing underneath as defense in depth.
Use-after-free endures because it lives at the intersection of manual memory management and complex object graphs, exactly the setting browsers, document parsers, and language runtimes operate in. The bug is a timing error dressed as an ordinary pointer use, which is why it slips past tests and why the industry's real answer is to remove manual lifetime management from the code that faces attackers. For which memory-safety flaws are being exploited in the wild right now, see our Exploit Intelligence dashboard.
A conceptual walkthrough
A concrete pattern makes the abstraction land. Imagine a program that manages a connection object. One part of the code holds a pointer to that connection and expects to keep using it. Another part, reacting to some event such as the connection closing, decides the object is finished and frees it. Crucially, the first part was never told, so it still holds the address and still believes the object is alive.
At the moment of the free, the allocator marks that block as available. The bytes of the old connection object are usually still sitting there untouched, so if the first part of the code reads through its pointer right away, it sees the old data and behaves normally. This is why the bug hides. Tests that exercise this path immediately after the free see nothing wrong.
Now introduce an attacker who can influence the program's allocations, which is exactly the situation in a browser where script runs in the page. The attacker triggers the free, then causes the program to allocate a new object of the same size. Allocators commonly satisfy a new request of a given size from a recently freed block of that size, so the new object lands in the slot the connection used to occupy. The attacker controls the contents of that new object.
The dangling pointer in the first part of the code now points at attacker-controlled bytes while the code still treats them as a connection. If the connection object had a field that the code calls as a function, a callback or a virtual method dispatched through a table of function addresses, then the code reads an address from attacker-controlled memory and jumps to it. The attacker has redirected control flow. If instead the code reads a length or an index from the object, the attacker gains a corrupted value that can turn into an out-of-bounds read or write elsewhere.
The whole exploit is a choreography of timing: free at the right moment, reclaim with the right shape, then use the stale pointer. None of the individual operations is unusual on its own, which is why the class is both common to introduce and hard to catch by inspection.
The size class detail that makes reclaiming work
Reliable exploitation depends on getting the attacker's object into the exact slot the freed object left behind, and that is governed by how allocators group allocations. Most modern allocators divide memory into size classes and serve requests of a given size from a pool dedicated to that size. A freed block of a particular size class is typically the first candidate to satisfy the next request in the same class.
This behavior is convenient for performance and predictable enough for an attacker to exploit. To reclaim a freed connection object, the attacker allocates something of the same size class, often choosing an object type they can fill with chosen bytes, such as a string or a typed array in a scripting environment. By matching the size class, they steer the allocator into handing back the freed slot. Understanding this is what connects use-after-free to techniques like heap grooming and heap spraying, where the attacker shapes the heap in advance so the reclaim lands where they want. The defensive counterpart, quarantining freed blocks and randomizing which block is returned, attacks precisely this predictability.
Detection signals
Use-after-free is primarily a bug to prevent in development, but there are concrete ways to surface it, both in testing and, imperfectly, at runtime.
- Sanitizer reports during testing. An address sanitizer maintains metadata about which memory is freed and flags any access to a freed region, printing the allocation and free sites. This is the single most productive way to catch the bug, and it belongs in continuous testing and fuzzing.
- Crashes with recognizable poison values. Hardened allocators overwrite freed memory with a fixed pattern. A crash where a pointer or length equals that pattern is a strong sign that freed memory was used, and the pattern is a quick tell during triage.
- Non-deterministic crashes tied to allocation load. A bug that appears only under memory pressure or heavy allocation churn, and disappears when run in isolation, often points to a reclaim-dependent temporal error rather than a simple logic fault.
- Fuzzing with lifetime-heavy inputs. Feeding a target inputs that create, destroy, and re-create objects in varied orders exercises the timing windows where these bugs live, and paired with a sanitizer it turns rare production crashes into reproducible test failures.
- Static analysis of ownership. Analyzers that track pointer lifetimes can flag paths where a pointer is used after a free on the same path, catching a subset of cases before runtime, though they miss cases that span complex control flow.
Because a dangling pointer often reads valid-looking old data, a test suite can pass on code riddled with use-after-free bugs. The access has to race the reuse to misbehave, and ordinary tests rarely reproduce the required allocation pattern. Treat the absence of crashes as uninformative for this class, and rely on allocator-instrumented fuzzing and sanitizers, which deliberately manipulate timing and memory state to force the bug into the open.
Use-after-free among memory-safety bugs
Use-after-free is one member of a family of memory-safety errors, and placing it alongside its relatives clarifies what is distinctive about it.
| Bug class | Nature of the error | What goes wrong | Typical primitive |
|---|---|---|---|
| Use-after-free | Temporal | Memory used after it is freed | Control-flow hijack or controlled read/write |
| Buffer overflow | Spatial | Write past the end of a buffer | Overwrite adjacent memory |
| Double free | Temporal | The same block freed twice | Allocator corruption |
| Type confusion | Semantic | Object used as the wrong type | Fields misinterpreted |
| Uninitialized use | Definitional | Reading memory before it is set | Leak of stale data |
The defining word for use-after-free is temporal. A buffer overflow is a spatial error, the access goes to the wrong place. Use-after-free is a timing error, the access goes to the right place at the wrong time. Type confusion is closely related in effect, because a reclaimed slot often ends up being interpreted as the wrong type, which is why exploits blur the line between them. What unites the temporal bugs is that the pointer itself is ordinary and the fault is in when it is used, which is exactly why bounds checks, the classic defense against spatial bugs, do nothing for them.
Common misconceptions
"Setting the pointer to null after free fixes it." Nulling the pointer helps for the specific pointer you control, converting a later use into a clean null dereference. It does nothing for other pointers elsewhere that still hold the freed address, which is the usual situation in a tangled object graph. It is a good habit and a partial mitigation, not a complete fix.
"Garbage collection makes use-after-free impossible." Managed languages remove the classic form by not freeing objects that are still referenced. They can still expose the class through unsafe or native interop, through bugs in the runtime itself, and through logical variants where an object is reused after being logically retired. The class is greatly reduced, not always eliminated.
"It only causes crashes, so it is a stability bug." A use-after-free that an attacker can influence is frequently exploitable for code execution or information disclosure, precisely because the reclaimed memory can be attacker-shaped. Treating it as a mere reliability issue underestimates it, and severity ratings for these bugs are often high.
"Address randomization defeats it." Address space layout randomization raises the cost of some exploitation steps, but a use-after-free that yields an information leak can defeat randomization by disclosing an address, and one that hijacks a function pointer within reclaimed memory may not need to know absolute addresses at all. Randomization is a hurdle, not a cure.
"The bug is in the free call." The free is correct in most of these bugs. The defect is that a reference outlived the object, so the real problem is ownership: something kept using the object after its lifetime ended. Fixing it means correcting who owns the object and when it may be released, not removing the free.
How the defenses evolved
For decades the response to memory-safety bugs was to find and fix each instance, one at a time. That approach never closed the class, because complex codebases keep producing new instances faster than review catches them. The industry gradually shifted toward removing the class rather than patching members of it.
Allocator hardening was an early structural move: quarantines that delay reuse, randomized allocation to make reclaiming unreliable, and poisoning of freed memory so stale reads produce obvious garbage. Sanitizers made the bug detectable during automated testing, and continuous fuzzing turned rare production faults into reproducible test failures. The most decisive shift has been toward memory-safe languages for the components that face attackers, where ownership and borrow checking prevent use of freed memory by construction, or where garbage collection removes manual freeing entirely. Every major browser is now migrating security-sensitive components in this direction, which reflects a hard-won conclusion: the durable answer is to take manual lifetime management out of the code attackers can reach.
Frequently asked questions
What exactly is a dangling pointer? A dangling pointer is a pointer that still holds the address of memory that has been freed. The pointer looks and behaves like any other pointer, and the address it holds is a real address, but the object that used to live there is gone. Using the pointer is undefined behavior, and it becomes dangerous once the allocator reassigns that memory to something else.
Why are use-after-free bugs so common in browsers specifically? Browsers manage a huge graph of interconnected objects with tangled ownership, and attacker-supplied script inside the page can allocate, free, and hold references to those objects on demand. That combination lets an attacker drive the exact free-then-reuse sequence the bug requires, with precise control over the reclaiming allocation. Few other environments give an attacker that much control over the target's heap.
Is use-after-free the same as a double free? No, though both are temporal errors. A use-after-free is accessing memory after it is freed. A double free is calling free on the same block twice, which corrupts the allocator's own bookkeeping. They can appear together, and both stem from confused ownership, but the mechanisms and the resulting primitives differ.
Can a use-after-free be exploited for more than a crash? Yes, and often it is. If the attacker can reclaim the freed slot with controlled data, the program's continued use of the stale pointer operates on attacker bytes. A function pointer or virtual table pointer in the reused object leads to control-flow hijack, and a corrupted field can produce a controlled read or write. These are the ingredients of full code execution, well beyond a simple crash.
How do memory-safe languages prevent it? Languages with ownership and borrow checking enforce at compile time that no reference outlives the object it points to, so a use of freed memory cannot be expressed in safe code. Garbage-collected languages take a different route, freeing an object only when no references remain, so a live reference keeps the object alive. Both approaches remove the manual timing error at the root of the class.
What is the fastest way to find these bugs in an existing codebase? Build the code with an address sanitizer and run it under continuous fuzzing that exercises object creation and destruction in varied orders. The sanitizer flags any access to freed memory with the allocation and free sites, and the fuzzer generates the allocation patterns that expose timing-dependent bugs. This combination finds far more than manual review, which tends to miss the timing element.
Does nulling every pointer after free solve the problem? It helps but does not solve it. Nulling the specific pointer you just freed turns a later use of that pointer into a clean crash rather than silent corruption. It cannot help the common case where other references to the same object exist elsewhere and are not nulled. Clear ownership and, where possible, memory-safe constructs are the real fix.
Related guides
Sources & further reading
Related guides
- Buffer Overflow Explained: Overwriting the Return Address
How stack buffer overflows work, why writing past a buffer overwrites the return address, and the mitigations that stop hijacked control flow.
- Format String Vulnerabilities: When Input Becomes Format
How uncontrolled format strings turn user input into read and write primitives, why the bug is so severe, and how to defend against it.
- Integer Overflow Explained: When Arithmetic Wraps Around
How integer overflow and wraparound lead to undersized allocations and buffer overflows, and the safe-arithmetic practices that prevent it.