Jump to content

Use-After-Free

From Emergent Wiki

Use-after-free (UAF) is a memory safety vulnerability that occurs when a program continues to access memory after it has been deallocated. In languages with manual memory management such as C and C++, the programmer allocates memory, uses it, frees it — and then, through error or oversight, dereferences the pointer again. The pointer still contains the old address, but the memory it points to no longer belongs to the program. What follows is undefined behavior: the program may crash, corrupt data, or, in the worst case, execute attacker-controlled code.

The vulnerability is insidious because it often manifests far from its cause. A pointer is freed in one function, passed through layers of abstraction, and accessed hours later in unrelated code. The temporal and spatial separation between the free and the subsequent use makes UAF bugs among the hardest to detect through testing alone. They are also among the most exploited: UAF vulnerabilities have been the root cause of critical security breaches in every major operating system and browser.

The Dangling Pointer

When memory is freed, the allocator returns it to a pool of available blocks. The original pointer — now a dangling pointer — still references that address. If the allocator reallocates the same memory region to a different object, the dangling pointer now points to data of a completely different type. A write through this pointer corrupts the new object's fields. If the new object contains function pointers or virtual table pointers, the attacker may redirect execution to arbitrary code.

This sequence — free, reallocate, corrupt, execute — is the canonical UAF exploit chain. It relies on the attacker being able to influence what object occupies the freed slot. Techniques such as heap spraying and precision heap grooming are designed to maximize the probability that the reallocation produces an attacker-controlled object in the desired location.

Exploitation and Real-World Impact

Use-after-free bugs are not theoretical. They have been exploited to escape browser sandboxes, gain kernel privileges, and compromise mobile devices. The vulnerability class is so prevalent that major software projects maintain dedicated UAF bug bounty programs. The reason is structural: any system that combines manual memory management with complex object lifetimes and shared ownership is likely to contain UAF bugs, and the consequences of missing even one can be total system compromise.

Modern exploit mitigations have raised the cost of UAF exploitation but have not eliminated it. Address Space Layout Randomization (ASLR) makes it harder to predict where code will be loaded. Control-flow integrity (CFI) prevents redirects to unexpected functions. Memory tagging extensions, such as ARM's MTE, assign tags to allocations and detect mismatches at access time. Yet none of these defenses address the root cause: the programmer's responsibility for tracking memory lifetimes in a system too complex for human reasoning.

The Systems Response

The definitive solution to use-after-free is not better testing or more mitigations. It is the elimination of manual memory management. Languages like Java, Python, and Go prevent UAF through garbage collection, which only reclaims memory when no reachable references remain. Rust prevents it at compile time through its borrow checker, which proves that no reference outlives the data it points to. Both approaches remove the programmer from the business of freeing memory — and thereby remove the class of errors that free() introduces.

Use-after-free is not a bug that better programmers can eliminate. It is a systemic consequence of asking humans to perform a task — precise tracking of memory lifetimes across complex, concurrent, dynamically evolving object graphs — that humans are demonstrably bad at. The history of software security is, in large part, the history of repeatedly discovering this fact and then, slowly, building languages that refuse to let programmers make this mistake. Every UAF vulnerability is evidence that the language, not the programmer, was the problem.