09 · CWE-415

Double Free

Calling free() on the same pointer twice. The first call is fine. The second corrupts the heap's bookkeeping structures. From there it's a race to arbitrary write.

The heap allocator (glibc's ptmalloc, jemalloc, the Windows heap) maintains a freelist — a linked list of available chunks. When you call free(p), the allocator links p onto the appropriate freelist. When you call malloc(size), it pulls a chunk off the front.

Calling free twice on the same pointer puts the chunk on the freelist twice. Subsequent malloc calls hand out the same memory to two different parts of the program. Each thinks it owns the chunk exclusively. Anything one writes, the other reads — including freelist pointers.

The simplest example

Vulnerable Cvulnerable
char *buf = malloc(64); read_into(buf); process(buf); free(buf); // ... later, in error handling ... if (error_occurred) { cleanup(buf); // cleanup also calls free(buf) — oops }

Real production double-frees rarely look this obvious. The typical pattern: a parent function frees a pointer it passed to a child, but the child also freed it in an error path. Or a refcount goes to zero in two places. Or an object's destructor calls free on a member that the caller already freed.

What actually happens on the heap

Step 1
p = malloc(64) → allocator returns chunk at 0x600100. Bookkeeping: chunk is IN-USE.
Step 2
free(p) → allocator marks chunk FREE, links it to head of size-64 freelist. p still holds the value 0x600100 but it's now a dangling pointer.
Step 3
q = malloc(64) → allocator returns the same chunk at 0x600100. q == p. The chunk is IN-USE again, but two pointers reference it.
Step 4
free(p) (second free) → allocator marks chunk FREE, links it onto the freelist even though it's currently allocated to q. The freelist now contains an in-use chunk.
Step 5
r = malloc(64) → allocator pulls the (already-allocated) chunk off the freelist. Now p, q, and r all point at 0x600100. Any write through one is visible through all three.
Step 6
The attacker controls writes to q. The attacker overwrites the chunk's freelist next-pointer (which the allocator stores inside the chunk while it's free). Next malloc follows that pointer and writes a chunk header to attacker-chosen memory: a function pointer, a vtable, a GOT entry. Arbitrary write → arbitrary execution.

Where double frees hide

Famous incidents

YearBugOutcome
2002zlib double-free (CVE-2002-0059)Found in compression library used by almost every product on Earth. Pre-auth exploitable in PostgreSQL, Apache, IIS, many others.
2014OpenSSL CVE-2014-3567Double-free in SSL session ticket handling. DoS, potentially worse.
2017WebKit (CVE-2017-2403)Used in JavaScriptCore. Sandbox escape from Safari WebContent process.
2020OpenSSL CVE-2021-3711SM2 decryption double-free. The bug had been latent for years.
2022OpenSSL CVE-2022-3786X.509 parser; double-free during certificate verification with certain Punycode email addresses. The "Heartbleed 2" that wasn't, but still serious.

Defenses

1. Set pointers to NULL after free

The cheapest defense. free(NULL) is defined and safe. Make every free(p) into free(p); p = NULL;. A second free becomes a no-op instead of corruption.

free(buf); buf = NULL; // safe to call free(buf) again later

Convention many shops adopt: a macro FREE(p) that does both. Some style guides require it.

2. Use RAII in C++ / smart pointers

std::unique_ptr moves ownership explicitly. There's only one owner; only one deallocation. std::shared_ptr refcounts; deallocation happens exactly once. Either eliminates manual delete.

3. Modern allocators detect some double frees

glibc's tcache freelist, since glibc 2.27, checks whether the chunk being freed is already at the head of the freelist (the simplest double-free case). If yes, it aborts the program rather than corrupt the freelist. This catches naive double frees in seconds but not the more complex patterns where the attacker shuffles allocations between the two free calls.

4. AddressSanitizer

Compile your code with -fsanitize=address during testing. ASan detects double-free at runtime and aborts with a clean stack trace, pointing at both the first and second free. Integrate into CI. Many double-frees that ship to production were caught locally by ASan but ignored.

5. Move to memory-safe languages

Rust's borrow checker makes double-free impossible at compile time. The ownership model statically enforces that each allocation has exactly one owner who is responsible for freeing it. Go, Java, C#, Swift, Python — all use garbage collection or refcounting that prevents manual double-free.

The "use-after-free vs double-free" question. They're cousins. UAF is reading or writing a pointer after it's been freed. Double-free is freeing the same pointer twice. Both stem from not knowing who owns the allocation. The same ownership discipline that prevents one prevents the other.
The point

Double-free is what happens when ownership of an allocation is unclear. Two parts of the code each think they're responsible for the cleanup; both call free; the second call corrupts the freelist.

The single-line fix — p = NULL after each free — defeats the simplest cases. The structural fix — a memory-safe language or RAII — defeats the whole class. The cost of either is far less than the cost of an exploited heap.

References

Formatted in APA 7.

  1. MITRE. (2024). CWE-415: Double free. Common Weakness Enumeration. https://cwe.mitre.org/data/definitions/415.html
  2. Free Software Foundation. (2024). The GNU C library reference manual: Memory allocation. https://www.gnu.org/software/libc/manual/html_node/Memory-Allocation.html
  3. Google. (2024). AddressSanitizer. https://github.com/google/sanitizers/wiki/AddressSanitizer
  4. OpenSSL Project. (2022). CVE-2022-3786: X.509 email address buffer overflow. https://www.openssl.org/news/secadv/20221101.txt
  5. Klabnik, S., & Nichols, C. (2023). The Rust programming language (2nd ed.). No Starch Press.