Introduction / Overview
Disclaimer: This article is for education and authorized security testing only. Run the techniques below exclusively against binaries you own or have explicit written permission to test, inside an isolated lab. Unauthorized exploitation of live systems is illegal in most jurisdictions.
Use-After-Free (UAF, CWE-416) and double-free (CWE-415) bugs remain among the most reliable primitives for memory-corruption attacks on Linux userland binaries. Since glibc 2.26 introduced the thread-local caching layer (tcache), exploiting these bugs has become easier, because the early tcache design omitted most of the integrity checks that the older bins enforced. This post explains how glibc's allocator hands out chunks, how a freed-but-still-referenced pointer turns into arbitrary write, and — given equal weight — how defenders detect and kill these bugs.
How it works / Background
glibc malloc (ptmalloc2) carves memory into chunks. Each allocated chunk carries inline metadata: an 8-byte prev_size field and an 8-byte size field (the low 3 bits of size are flags: PREV_INUSE, IS_MMAPPED, NON_MAIN_ARENA). The pointer returned to the caller points just past the size field.
When a small chunk is freed, glibc pushes it onto a tcache bin — a singly linked LIFO list indexed by size, holding up to 7 entries per size class by default (tcache_count = 7). The free chunk's user data area is repurposed: the first 8 bytes (fd) store the "next" pointer, and on glibc >= 2.29 the next 8 bytes (bk) store a key pointing at the per-thread tcache_perthread_struct for double-free detection.
Two abuses follow directly:
- Use-After-Free: if the program keeps using a pointer after
free(), an attacker who can reallocate that same chunk controls its contents — including any function pointers orfdlink it holds. - tcache poisoning: by overwriting the
fdof a free tcache chunk (via UAF or overflow), you make the allocator's free list point at an attacker-chosen address. The next twomalloc()calls of that size return: (1) the original chunk, (2) a chunk at your target address — an arbitrary write primitive.
On glibc >= 2.32, fd is obfuscated with safe-linking: fd = (chunk_addr >> 12) XOR next. You must know (leak) a heap address to forge a valid pointer, and your target must be 16-byte aligned.
Prerequisites / Lab setup
You need a Linux box (a VM or container), gdb with the pwndbg or GEF plugin, and pwntools.
# Debian/Ubuntu lab
sudo apt update && sudo apt install -y gdb gcc python3-pip glibc-source
python3 -m pip install --user pwntools
# pwndbg for heap inspection
git clone https://github.com/pwndbg/pwndbg && cd pwndbg && ./setup.sh
# Check the local glibc version (decides safe-linking behavior)
ldd --version | head -n1BashCompile a deliberately vulnerable target:
// vuln.c — toy UAF / tcache target. Compile: gcc -O0 -no-pie -o vuln vuln.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void win() { system("/bin/sh"); } // target for the overwrite
int main() {
char *a = malloc(0x30);
char *b = malloc(0x30);
free(a); // a -> tcache[0x40]
// UAF: a is still readable/writable here
*(unsigned long *)a = 0; // attacker overwrites fd
char *c = malloc(0x30); // returns a again
char *d = malloc(0x30); // returns the poisoned target
printf("c=%p d=%p\n", c, d);
return 0;
}Cgcc -O0 -no-pie -o vuln vuln.cBashWalkthrough / PoC
Step 1 — observe the tcache. Run under gdb and inspect the free list after free(a).
gdb -q ./vuln
pwndbg> break main
pwndbg> run
pwndbg> next # step until after free(a)
pwndbg> tcachebinsBashtcachebins prints the 0x40 bin with one entry. Use vis_heap_chunks to see the inline metadata (size = 0x41, the 0x1 being PREV_INUSE).
Step 2 — confirm the LIFO reuse. Because tcache is last-in-first-out, the next malloc(0x30) returns the chunk you just freed. That is the UAF: the program still holds a while the allocator hands the same memory to c.
Step 3 — poison the free list. In a realistic target you overwrite a's fd with the address you want returned. A minimal pwntools driver:
#!/usr/bin/env python3
from pwn import *
elf = context.binary = ELF('./vuln')
libc_leak = None # in a real target, leak a heap addr first for safe-linking
target = elf.sym['win'] # address we want malloc() to return
p = process('./vuln')
# In a real interactive target, you'd: alloc, free, then UAF-write target into fd.
# For glibc >= 2.32, encode with safe-linking:
def protect_ptr(pos, ptr): # pos = address of the fd field (heap)
return (pos >> 12) ^ ptr
# log.info('forged fd = %#x', protect_ptr(heap_fd_addr, target))
p.interactive()PythonThe core of every tcache-poisoning exploit is identical: free a chunk, corrupt its fd, then malloc() twice. The first allocation drains the legitimate chunk; the second returns your forged pointer. From there you typically overwrite a saved return address, a GOT entry, or __free_hook / __malloc_hook (removed in glibc >= 2.34, where system-via-FILE or __exit_funcs chains are used instead).
Step 4 — defeat double-free detection. Naive double-free (free(a); free(a);) aborts on glibc >= 2.29 with free(): double free detected in tcache 2 because the chunk's key still equals the tcache pointer. Bypass: free a, free b, then free a again (a "double free with a chunk in between") so the immediate-neighbor check passes, or overwrite the key field via UAF before the second free.
# Reproduce the abort to see the guard in action
pwndbg> p (char*)__libc_version
pwndbg> # trigger free(a); free(a); -> "double free detected in tcache 2"BashMermaid diagram

The diagram shows the path from a benign free() to an arbitrary-write primitive when a dangling pointer lets the attacker forge the tcache fd link.
Detection & Defense (Blue Team)
Defending against UAF and tcache abuse spans build-time, runtime, and detection — give it the same attention as the offense.
Find the bug before shipping.
- Compile with sanitizers in CI:
gcc -fsanitize=address,undefined -g. AddressSanitizer quarantines freed memory and reportsheap-use-after-freewith allocation/free stacks. MemorySanitizer and Valgrind'smemcheck(valgrind --leak-check=full ./vuln) catch the same class dynamically. - Static analysis:
clang --analyze, CodeQL'scpp/use-after-freequery, andgcc -fanalyzerflag many dangling-pointer paths.
Make exploitation harder at runtime.
- Keep glibc current. Safe-linking (>= 2.32) and pointer alignment checks raise the bar; the
tcache key(>= 2.29) blocks naive double-free. - Set
GLIBC_TUNABLES=glibc.malloc.check=3or exportMALLOC_CHECK_=3to enable heap consistency checks and abort on corruption. - Build hardened:
-D_FORTIFY_SOURCE=3 -fstack-protector-strong -Wl,-z,relro,-z,now(full RELRO removes the writable GOT target), and ship PIE binaries for ASLR. - Consider a hardened allocator such as GrapheneOS hardened_malloc or Scudo (default on Android), which add randomized free lists and stronger metadata separation.
Detect at runtime / in production.
- Watch for
*** Error in ...: free(): ... ***andmalloc(): ... corruptedlines injournald/dmesg; pipe them to your SIEM. - Hardware-assisted: enable MTE (ARM Memory Tagging) or CHERI where available; both detect UAF on access.
- Map findings to MITRE ATT&CK T1203 (Exploitation for Client Execution) and T1068 (Exploitation for Privilege Escalation) for alerting and IR playbooks.
For broader context, see my notes on Ghidra for static triage, the pwntools ROP workflow, and format string exploitation.
Conclusion
tcache turned a decade-old allocator into a fast attack surface: a single dangling pointer plus a controlled write over fd yields arbitrary allocation, and from there code execution. Defenders are not helpless — sanitizer-gated CI, current glibc with safe-linking, hardened build flags, and runtime allocators close most of the gap. Understand both sides: the same chunk metadata you abuse offensively is exactly what the blue team instruments to catch you.
References
- MITRE CWE-416 Use After Free — https://cwe.mitre.org/data/definitions/416.html
- MITRE CWE-415 Double Free — https://cwe.mitre.org/data/definitions/415.html
- glibc malloc internals (sourceware) — https://sourceware.org/glibc/wiki/MallocInternals
- HackTricks — Heap / tcache exploitation — https://book.hacktricks.xyz/binary-exploitation/libc-heap
- how2heap (shellphish) — https://github.com/shellphish/how2heap
- Safe-Linking (Check Point Research, 2020) — https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/
- MITRE ATT&CK T1203 — https://attack.mitre.org/techniques/T1203/



Comments