Double-Free Detection
Overview
Double-free is one of the most fundamental heap exploitation primitives, appearing across dozens of CVEs in components that rely on glibc's allocator. It has been observed in glibc's own regcomp(), in application-level parsers, and in many other components. Rather than tracking a single CVE, this page describes the general class of double-free vulnerabilities and how compatmalloc detects them.
Exploitation Technique: Tcache Dup
When a chunk is freed twice, it appears in the tcache freelist twice, creating a cycle:
tcache[64B]: chunk_A -> chunk_A -> chunk_A -> ... (cycle)
Two subsequent malloc() calls of the same size return the same pointer:
char *a = malloc(64); // returns chunk_A
char *b = malloc(64); // returns chunk_A again!
// a == b -- both point to the same memory
This enables type confusion: the program believes a and b are separate allocations, but writes through one are visible through the other. An attacker can use this to overwrite function pointers, vtable entries, or other security-sensitive data.
glibc's mitigation history
| glibc version | Detection mechanism | Bypassable? |
|---|---|---|
| < 2.29 | None | N/A -- no detection at all |
| 2.29+ | tcache key (random value stored at offset 8 in freed chunk) | Yes -- the key is stored inline and can be overwritten by a heap write primitive |
| 2.32+ | PROTECT_PTR (pointer mangling via XOR with address) | Harder but still inline -- can be bypassed with an info leak |
All of glibc's mitigations store detection data inline within the freed chunk's user data region. An attacker with any heap write capability can clear or forge these values before triggering the second free().
Proof of Concept
Source: tests/cve/double_free.c
gcc -o /tmp/double_free tests/cve/double_free.c
glibc output (>= 2.29)
=== Double-Free Detection Demo ===
[1] malloc(64) => 0x...
[2] free(0x...) => OK
[3] free(0x...) => double free! (should be caught)
free(): double free detected in tcache 2
Modern glibc (>= 2.29) does detect this case via the tcache key. However, the key is stored inline at chunk + 8 and can be overwritten by an attacker with a write-after-free primitive before the second free().
compatmalloc output
=== Double-Free Detection Demo ===
[1] malloc(64) => 0x...
[2] free(0x...) => OK
[3] free(0x...) => double free! (should be caught)
compatmalloc: double free detected
compatmalloc aborts immediately on the second free().
What compatmalloc catches
-
Out-of-band FLAG_FREED check. The metadata table stores a
FLAG_FREEDbit for every allocation in a separatemmapregion. On everyfree():- Look up the pointer in the metadata table
- If
FLAG_FREEDis already set, abort with "double free detected" - Otherwise, set
FLAG_FREED
-
Cannot be bypassed by heap writes. Because the metadata table is in a separate memory region (not adjacent to user data), an attacker cannot corrupt the
FLAG_FREEDbit via a buffer overflow or use-after-free write. This is the fundamental advantage over glibc's inline tcache key approach. -
No version-dependent behavior. The detection works identically regardless of glibc version, allocation size, or tcache state. Every
free()is checked, every time.
What compatmalloc does NOT catch
- Aliased pointer double-frees. If a program has two pointers to the same allocation (e.g.,
a = malloc(64); b = a;) and frees both, compatmalloc detects this because it tracks the allocation address, not the pointer variable. Bothfree(a)andfree(b)resolve to the same metadata entry. - Root cause identification. The abort happens at the second
free()call, not at the point where the bug was introduced. For complex programs, the stack trace at the abort may not directly reveal why the double-free occurred. - Deliberate double-free patterns. Some (buggy) programs intentionally double-free and rely on glibc silently accepting it. These programs will abort under compatmalloc. This is by design -- double-free is always a bug.