Use-after-free (UaF) vulnerabilities are one of the most exploited classes of memory corruption bugs. They’ve been at the heart of browser zero-days, Linux kernel privilege escalations, and countless CVEs. Despite being well understood, they remain stubbornly common — a testament to how easy they are to introduce and how hard they are to catch with conventional testing.


What Is a Use-After-Free?

A use-after-free occurs when a program:

  1. Allocates a chunk of memory on the heap
  2. Frees that memory (returning it to the allocator)
  3. Continues to use a pointer that still references the now-freed region

The memory is no longer “owned” by the program. The allocator is free to give it to something else. When the program reads or writes through the dangling pointer, it’s operating on memory that may now belong to an entirely different object — or may have been zeroed, corrupted, or repurposed by an attacker.

This is classified as CWE-416.


A Minimal Example

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char name[32];
    void (*greet)(void);
} User;

void say_hello(void) {
    printf("Hello!\n");
}

int main(void) {
    User *u = malloc(sizeof(User));
    strncpy(u->name, "alice", sizeof(u->name));
    u->greet = say_hello;

    free(u);           // memory is released

    u->greet();        // UaF: dangling pointer dereference
    return 0;
}

In the best case this crashes. In the worst case — especially in a long-running process like a browser or server — an attacker has placed controlled data at the same heap address before the dangling call happens.


Why It’s Dangerous

The classic exploitation path for a UaF looks like this:

  1. Trigger the free — cause the vulnerable object to be freed while a reference to it is still live.
  2. Reclaim the slot — spray the heap with attacker-controlled data of the same size so the allocator places it at the same address.
  3. Trigger the use — the program reads the stale pointer, now pointing at attacker-controlled memory.

If the object contains a function pointer or a vtable pointer (common in C++ objects), step 3 becomes a controlled call to an arbitrary address — game over.

For kernel UaFs the stakes are even higher: an attacker can often use this to overwrite cred structures or security hooks and escalate to root.


A More Realistic C++ Example

#include <iostream>
#include <string>

class Connection {
public:
    std::string host;
    virtual void send(const std::string &data) {
        std::cout << "Sending to " << host << ": " << data << "\n";
    }
    virtual ~Connection() = default;
};

class AttackerBlob {
public:
    uintptr_t fake_vtable[8];
};

Connection *conn = nullptr;

void on_disconnect() {
    delete conn;   // conn freed here
}

void on_data(const std::string &msg) {
    // conn still used here — UaF if on_disconnect fired first
    conn->send(msg);
}

int main() {
    conn = new Connection();
    conn->host = "example.com";

    on_disconnect();   // frees conn
    on_data("hello");  // UaF: conn->send() dispatches through stale vtable
}

The vtable pointer at the start of the Connection object now points wherever the allocator has reused that memory. In a real exploit, the attacker arranges for a crafted object to land there first.


Common Root Causes

UaFs don’t always look as obvious as the examples above. They typically arise from:

  • Reference counting bugs — an object is freed when a ref count hits zero, but another reference still exists elsewhere (e.g., in an observer list or callback closure).
  • Event-driven / async code — a callback fires after the object it was registered on has already been destroyed.
  • Error path cleanup — a goto err or exception unwind frees an object, but execution continues through code that still holds a raw pointer.
  • Iterator invalidation — a container is modified while being iterated; elements are freed and the iterator now dangles.
  • Shared ownership without smart pointers — raw malloc/free in C where ownership is informal and easy to get wrong.

How to Find Use-After-Free Bugs

1. AddressSanitizer (ASan)

The fastest way to catch UaFs at runtime. Compile with:

clang -fsanitize=address -g -O1 -fno-omit-frame-pointer target.c -o target

ASan inserts shadow memory tracking around every heap allocation. When freed memory is accessed, it reports the exact location of the bug with a full stack trace — including where the memory was allocated and where it was freed.

==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 8 at 0x... thread T0
    #0 0x... in on_data example.cpp:30
    #1 0x... in main   example.cpp:38

0x... is located 0 bytes inside of 64-byte region [0x...,0x...)
freed by thread T0 here:
    #0 0x... in operator delete example.cpp:24

ASan has moderate overhead (~2x slowdown) and is suitable for CI pipelines and internal testing builds.

2. Valgrind / Memcheck

A heavier-weight alternative that works without recompilation:

valgrind --tool=memcheck --track-origins=yes ./target

Slower than ASan (~10-20x) but useful when you can’t recompile (e.g., testing a third-party binary with debug symbols).

3. Fuzzing

Fuzz testing is the most effective way to discover UaFs that only trigger on specific input sequences. libFuzzer and AFL++ both integrate naturally with ASan:

# Build a libFuzzer target with ASan
clang -fsanitize=address,fuzzer -g target.c -o fuzz_target

# Run it
./fuzz_target corpus/

When a crashing input is found, ASan’s output pinpoints the UaF. Combine with a corpus of real-world inputs for better coverage.

4. Static Analysis

Static analyzers catch UaFs before the code ever runs:

  • Clang Static Analyzer (clang --analyze) — finds many UaFs through dataflow analysis, built into the standard Clang toolchain.
  • CodeQL — GitHub’s semantic code analysis engine. The query cpp/use-after-free is purpose-built for this class of bug and runs across the entire call graph.
  • Coverity / Infer — commercial and open-source options respectively; both have strong UaF detection.

A CodeQL query to find raw pointer use after free():

import cpp
import semmle.code.cpp.dataflow.DataFlow

from FunctionCall free, VariableAccess use, Variable v
where
  free.getTarget().hasName("free") and
  free.getArgument(0) = v.getAnAccess() and
  use.getTarget() = v and
  free.getASuccessor+() = use
select use, "Possible use-after-free of $@.", v, v.getName()

5. Manual Code Review

Automated tools have blind spots. When reviewing manually, look for:

  • Any place a raw pointer is stored in multiple locations (observer lists, global state, closures)
  • free() or delete calls in error paths where the pointer is still in scope
  • Event/callback registration patterns where the lifetime of the registrant is shorter than the event source
  • C++ destructor chains that invalidate sibling references

Defense in Depth

Finding bugs is important; preventing them is better:

  • Use smart pointers in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr) to make ownership explicit and automatic.
  • Enable hardened allocators — modern allocators like tcmalloc and jemalloc offer options to detect or mitigate use-after-free in production.
  • CFI (Control Flow Integrity)clang -fsanitize=cfi limits where indirect calls (function pointers, vtables) can jump, breaking many UaF exploit chains.
  • Memory-safe languages — Rust’s borrow checker makes UaF a compile-time error by design. Projects like the Linux kernel and Android are actively adopting Rust for new unsafe subsystems precisely for this reason.

Notable CVEs

UaFs are not theoretical — they show up constantly in production software. A few well-known examples:

CVEComponentImpact
CVE-2022-0609Chrome — AnimationUaF exploited in the wild; attributed to North Korean threat actors. Remote code execution via a crafted HTML page.
CVE-2022-29582Linux kernel — io_uringUaF in the io_uring subsystem under file-expiry conditions; local privilege escalation to root.
CVE-2022-2588Linux kernel — route4_changeUaF in the cls_route traffic classifier; local privilege escalation, widely used in container escape PoCs.
CVE-2020-1054Windows — Win32kUaF in the Windows kernel graphics subsystem; local privilege escalation, exploited in targeted attacks.
CVE-2021-30551Chrome — V8Type confusion leading to UaF in V8; exploited in the wild for renderer remote code execution.

These span browsers, OS kernels, and both Windows and Linux — a reminder that no platform is immune.


Conclusion

Use-after-free bugs are deceptively simple in description but notoriously tricky in practice. They hide in async callbacks, reference count edge cases, and complex object lifetime interactions. The good news is that the tooling to find them — ASan, libFuzzer, CodeQL — is mature, free, and highly effective. Building them into your development and CI workflow is the single highest-leverage step you can take to eliminate this class of vulnerability before it ships.