algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

tcache dup

2026-06-143 min readheap pwn glibc

Fastbin dup had to dance around a check. The tcache, for two whole releases, had nonefree a chunk twice and it cheerfully linked it into the tcache twice. glibc 2.29 finally added a guard, and it's a single field sitting in the freed chunk's own data, which means you can erase it. This is the most common double-free you'll actually use.

target./tdup
arch
amd64-64-little
libc
2.31
technique
tcache double-free -> duplicate chunk -> tcache poisoning
guard
the 2.29 `key` field (and how to clear it)

2.27–2.28: just free it twice#

On the first two tcache releases there is nothing to bypass:

the whole attack, pre-2.29c
1
2
3
4
free(a);            // tcache[idx]: a
free(a);            // tcache[idx]: a -> a    (no complaint at all)
x = malloc(sz);     // x == a
y = malloc(sz);     // y == a  -> two live pointers to one chunk

Now x and y alias. Free one, edit it through the other, and you control a freed chunk's fd — which is tcache poisoning: the next-but-one allocation lands wherever you point.

2.29+: the key field, and erasing it#

glibc 2.29 gave freed tcache chunks a key — the second quadword of the chunk's data is set to the address of the tcache_perthread_struct. On free, if the chunk you're freeing already carries that key, glibc walks the bin looking for a double-free and aborts:

glibc malloc.c — tcache_entry, and the checkc
1
2
3
4
5
6
7
8
typedef struct tcache_entry {
    struct tcache_entry *next;   // 1st quadword: the fd
    struct tcache_perthread_struct *key;  // 2nd quadword: double-free sentinel
} tcache_entry;

// in _int_free:
if (__glibc_unlikely (e->key == tcache))
    /* scan the bin; abort on "free(): double free detected in tcache 2" */

The bypass writes itself: between the two frees, overwrite the key to anything that isn't the tcache pointer. The check e->key == tcache then fails, the scan is skipped, and the second free goes through:

a freed tcache chunk — clobber the key (offset 8) before re-freeinghex
00000000  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
the modern versionc
1
2
3
free(a);                 // tcache[idx]: a   (a->key = tcache)
edit(a, p64(0) + p64(0)); // a->next = 0, a->key = 0  (key != tcache now)
free(a);                 // passes the check -> tcache[idx]: a -> a
the rest is poisoning

With the duplicate, poison the fd to __free_hook, allocate it out, write system, free "/bin/sh":

txt
segfault{th3_tc4ch3_n3v3r_l34rns}

safe-linking changes the fd write (2.32+)

From glibc 2.32 the next/fd field is mangled as (&fd >> 12) ^ target, so after the dup you need a heap leak to compute the poisoned fd. And glibc 2.34 hardened the tcache further with a per-bin count check — a sloppy double-free can now also trip "free(): double free detected" via the count, so keep the bin's bookkeeping consistent.

The tcache dup is the workhorse: simplest double-free, straight into the friendliest poison. Next we go back to the classics with a twist — the House of Einherjar, where one null byte lets you consolidate a chunk backward into a fake chunk anywhere.