Skip to content

CWE-416: Use After Free

Overview

Use-after-free occurs when memory is accessed after being freed. The block may already have been reallocated, so the read or write lands inside a different, live object. It is inherent to manual memory management (C, C++, and similar languages). A garbage-collected or reference-counted runtime removes it from ordinary code, but not from the code that steps outside those rules: an unsafe block in Rust or C#, a native call across JNI, cgo or a Python C extension, or Swift's unowned(unsafe), which is the variant that will read the old address - a plain unowned reference traps instead, turning the same mistake into a checked failure. Using an object after Dispose() or close() is a neighbouring weakness rather than this one: it normally raises something like ObjectDisposedException instead of touching freed memory, and MITRE files it as CWE-672 (Operation on a Resource after Expiration or Release), which has no page here. It is exploited widely in browsers, kernels, and other native applications.

Relationship to Other CWEs

Use-after-free is closely related to CWE-415 (Double Free) - both stem from unclear pointer ownership after a deallocation call, and the fix is the same for both: give each allocation an explicit ownership model - single ownership by preference, coordinated shared ownership where independent users must keep the object alive - and make every non-owning reference either shorter-lived than the object or able to detect that it has expired.

Nulling the pointer after freeing is a mitigation rather than a fix here, and a weaker one than it is for double free: it reaches only the variable it is applied to, and MITRE's note on this CWE records that its usefulness falls away as the data structure gets more complex, since there is no way to find every alias. The vulnerable pattern below is precisely that case - the pointer that gets used is not the one that was nulled.

If the freed memory is accessed because a signal interrupted execution and a handler ran concurrently, the root cause is CWE-364 (Signal Handler Race Condition), not a plain lifetime bug - fix the signal handling first.

Risk

Critical: Once the block has been reallocated, a read through the freed pointer returns whatever now occupies it, disclosing memory the program no longer owns, and a write corrupts that occupant - usually a live object of another type. An attacker who sprays the heap so that chosen data lands in the block controls what the program reads back, including the vtable pointer it calls through, which is how use-after-free reaches arbitrary code execution.

Remediation Steps

Core Principle: Give each allocation an explicit ownership model, and ensure every access occurs while the object is alive. Prefer single ownership; use coordinated shared ownership where needed. Non-owning references require a lifetime guarantee or a checked mechanism that keeps the object alive during use.

Trace the Data Path

  • Source: Every alias, callback, or stored pointer that could outlive the allocation it refers to
  • Sink: Any dereference, read, or write through one of those aliases after the underlying memory has been freed
  • Missing control: No mechanism that invalidates or tracks every reference to an allocation when it is freed, and no scoping that guarantees a reference can't outlive its target

Tie Lifetime to Ownership, Not Manual Tracking (Primary Defense)

// SECURE - pseudo-code
owner = acquire_with_single_ownership(source)
use(owner)
// released automatically when owner's scope ends - nothing outside that scope holds a pointer to it

Prefer a construct that makes it structurally impossible to hold a usable reference after release (scope-bound ownership, reference counting) over raw pointers whose validity depends on the programmer remembering every place they were copied.

Invalidate Aliases Immediately After Freeing (Defense in Depth)

Where automatic lifetime management isn't available, null every alias of a pointer as soon as it's freed, and check for null before dereferencing:

free(resource)
resource = null   // later use fails fast instead of reading freed memory

This only protects the specific reference that gets nulled. Any other alias to the same memory needs its own explicit invalidation, which is why manual tracking does not scale and automatic lifetime management is the primary defense.

Guard Callbacks and Deferred Access

  • Never hand a raw pointer to an asynchronous callback without a lifetime guarantee - use a reference-counted handle so the object stays alive until the callback runs, or a mechanism that lets the callback detect the object is gone
  • Cancel or unregister callbacks/listeners before the object they reference is freed

Test the Fix

  • Run the binary under a memory-error sanitizer (e.g. AddressSanitizer) and exercise every code path that frees a resource
  • Run under a memory debugger (e.g. Valgrind) for an independent check
  • Specifically test callback, asynchronous, and multi-alias code paths, since those are where use-after-free hides
  • Re-scan with the security scanner to confirm the finding is resolved

Common Vulnerable Patterns

// VULNERABLE - pseudo-code
resource = allocate()
alias = resource
free(resource)
alias.field = 5   // USE AFTER FREE - alias still points to the freed memory

Why this is vulnerable: freeing memory does not make it inaccessible. The allocator returns the block to its free list, but the pages stay mapped, so alias.field = 5 succeeds - no fault, no error, and no symptom at the line where the defect is. The value lands in memory the program no longer owns.

How bad that is depends on the gap between the free and the use. If nothing allocates in between, the write hits an idle block and execution continues; if something does, it lands inside a live object of a different type. Arranging that is what an attacker works at, which is why the same code is a latent bug when the use follows immediately and a dependable exploit primitive when the use is deferred to a callback, an event handler, or the next request. Timing the source does not control is part of the weakness.

Setting resource to null after freeing does not address this, because the pointer that gets used is alias and nothing connects the two. A fix has to act on the lifetime itself - either the allocation outlives every reference to it, or every reference can tell that it has expired.

Secure Patterns

Shared Ownership for References That Must Extend Lifetime

// SECURE - pseudo-code
owner = acquire_with_shared_ownership(source)
alias = share(owner)        // alias is a co-owner, not a copy of an address

use(alias)                  // valid: the allocation cannot be released while alias holds it
release(owner)              // count drops, object survives
use(alias)                  // still valid
release(alias)              // last owner gone - released here, exactly once

Why this works: The alias is the thing keeping the allocation alive, so there is no moment at which it refers to something released. This inverts the vulnerable version: instead of the code having to find and invalidate every reference at release time, each reference decides for itself when it is done, and release is whatever happens last. That removes the requirement to enumerate aliases - the part no reviewer can verify by reading.

Weak References for Aliases That Must Not Extend the Lifetime

Where an alias must not keep the object alive - an observer, a cache, a back-reference in a tree - use a weak reference that can be interrogated rather than a raw address:

// SECURE - pseudo-code, for a reference that must not extend the lifetime
weak = weaken(owner)

later:
    strong = weak.upgrade()   // null if the owner has released in the meantime
    if strong is null:
        return                // the object is gone; this is an expected outcome
    use(strong)               // valid for as long as strong is held

Why this works: upgrade() reports whether the object is still alive, which a raw pointer cannot do, and it reports it by producing an owner. The object therefore cannot be released between the check and the use. A null check on a raw pointer gives no such guarantee: its answer can go stale the moment it is returned.

Where the language offers neither, as in C, the fallback is to null the pointer at release through a pointer to the variable, and to hand deferred code a revalidatable handle rather than an address. Both are covered on the C page, along with what they do not reach.

Language-Specific Guidance

  • C - release functions taking T ** so they null the caller's pointer, generation-counted handles for deferred callbacks, sanitizer/Valgrind testing
  • C++ - std::unique_ptr/std::shared_ptr for ownership, std::weak_ptr for callbacks that must not keep an object alive, and the dangling-view shapes (std::string_view, iterators, references into a reallocated container) that ownership alone does not fix

Additional Resources