CWE-415: Double Free
Overview
Double free occurs when a deallocation function is called twice on the same pointer without a reallocation between the calls, corrupting heap metadata. It is inherent to manual memory management (C, C++, and similar languages) and does not occur in garbage-collected or reference-counted-by-default languages.
Relationship to Other CWEs
CWE-675 (Multiple Operations on Resource in Single-Operation Context) is the general form of the same mistake - calling a once-only operation twice - and it cedes the memory case to this page. Go there for a duplicated unlock, close or bind, none of which has a page of its own. It carries a lower severity than this page because the class spans those milder outcomes, a deadlock or a closed handle; a duplicated free() specifically is the one that reaches arbitrary code execution.
Double free is closely related to CWE-416 (Use After Free). Both stem from unclear pointer ownership after a deallocation call, and the fix is the same for both: give each allocation exactly one owner, with a single-ownership smart pointer where the language has one. Nulling the pointer after freeing addresses both as well, but as a mitigation rather than a fix. It reaches only the variable it is applied to, and MITRE's own note on CWE-416 records that its usefulness falls away as the data structure gets more complex, which is why Secure Patterns below leads with ownership. If the double free happens because a signal interrupted a function mid-deallocation and a handler frees the same pointer again, the root cause is CWE-364 (Signal Handler Race Condition) rather than a plain ownership bug; fix the signal handling first.
Risk
Critical: Corrupted heap metadata can be steered into arbitrary code execution by overwriting function pointers or GOT entries, into information disclosure by reading freed memory, or into a crash. Double free is a commonly exploited bug class.
Remediation Steps
Core Principle: Enforce single ownership for every allocation and never free the same allocation twice.
Trace the Data Path
- Source: Every place that calls a deallocation function on a pointer - normal cleanup, error handlers, destructors, and any function that walks a shared structure
- Sink: The second deallocation call that reaches the same, already-freed pointer
- Missing control: No single, unambiguous owner responsible for freeing the allocation, and no invalidation of the pointer after the first free
Enforce Single Ownership (Primary Defense)
// SECURE - pseudo-code
owner = acquire_with_single_ownership(source)
use(owner)
// released automatically, exactly once, when owner's scope ends
Prefer a language construct that ties deallocation to ownership and enforces it at compile time or runtime (single-owner smart pointers, reference counting) over manual, hand-paired allocate/free calls.
Invalidate Pointers Immediately After Freeing (Defense in Depth)
Where automatic single ownership isn't available, set the pointer to a null/invalid sentinel immediately after freeing it, and make cleanup idempotent so a second call is a safe no-op rather than a second free:
This is a safety net, not a substitute for clear ownership. It converts a memory-corruption bug into a harmless no-op without settling who owns the pointer; Nulling at Release (Fallback) under Secure Patterns covers what it does and does not reach.
Test the Fix
- Run the binary under a memory-error sanitizer (e.g. AddressSanitizer) or a memory debugger (e.g. Valgrind) and exercise every error and exception path
- Specifically test paths that free a resource and then hit an error, exception, or early return
- Test any code that walks or destroys a shared structure (linked list, tree) more than once
- Re-scan with the security scanner to confirm the finding is resolved
Common Vulnerable Patterns
// VULNERABLE - pseudo-code
resource = allocate()
free(resource)
if error:
free(resource) // DOUBLE FREE - same pointer freed twice
Why this is vulnerable: free receives the address, not the variable, so it cannot and does not change resource. After the first call the variable still holds a value indistinguishable from a valid pointer, and nothing about the second free looks wrong at the point it is written.
The sketch puts both calls in one function. Real code splits the pair: a helper that frees on failure and a caller that frees again, or an explicit cleanup method followed by a destructor running during unwinding. What connects them is not a visible duplicate but an unanswered question, namely whether releasing this belongs to the code that allocated it or the code that last used it.
That is also why resource = null immediately after the free is only half a fix. It works where a single variable holds the address, since freeing a null pointer is defined to do nothing, and achieves nothing where a second pointer or a second owner holds the same address, because there is no way to reach every copy. Assigning ownership to exactly one place removes the ambiguity instead.
Secure Patterns
Single Ownership (Primary Defense)
// SECURE - pseudo-code
owner = acquire_with_single_ownership(source) // release is bound to the owner
use(owner)
if error:
return error // nothing to release by hand
// released exactly once when owner's scope ends, on every path out
Why this works: No path out of the function contains a release call, so no path can contain a second one, and "has this already been freed?" never has to be answered. This is what std::unique_ptr does in C++ (see the C++ guidance); the equivalents elsewhere are Rust's move semantics, a defer-style scope guard, or a language whose allocator owns the object outright.
Nulling at Release (Fallback)
Where the language offers no such construct, C being the case that matters here, the fallback is to null the pointer at the moment of release and route every release through one function that treats null as "already done":
// SECURE - pseudo-code, fallback where ownership cannot be enforced by the language
release(resource) // no-op when resource is already null
resource = null // set by release() itself, through a pointer to the variable
if error:
release(resource) // no-op: resource is already null
Why this works: Making the release function take the variable rather than its value lets it null the caller's copy, so the second call sees null and does nothing. The limit is that it protects only the variable passed in. Any other copy of the address - a caller's local, a struct field, a node still linked into a list - is untouched, and freeing through one of those is the same bug. That is why single ownership is the fix and this is the mitigation.
Language-Specific Guidance
- C - release functions that take
T **so they can null the caller's pointer, theSAFE_FREEmacro and why thevoid **function form of it does not compile, single-entry destroy for shared structures, sanitizer/Valgrind testing - C++ -
std::unique_ptr/std::shared_ptrfor compile-time single ownership,std::weak_ptrfor breaking cycles, RAII with custom deleters