tcache
An interactive deep-dive into the per-thread cache that powers fast allocations in glibc malloc. Understand the struct internals, follow the linked-list mechanics of tcache_put and tcache_get, and learn how attackers subvert them.
tcache_entry & tcache_perthread_struct
The tcache was introduced in glibc 2.26 as a per-thread cache sitting in front of the fastbin / smallbin layer. Each thread gets its own tcache_perthread_struct allocated from the heap, referenced by thread_arena->tcache (glibc 2.29+) or a __thread variable.
tcache_entry (glibc 2.29+)
typedef struct tcache_entry
{
struct tcache_entry *next; /* singly-linked list next pointer */
struct tcache_perthread_struct *key;
/* pointer back to tcache struct;
set on free, checked on second free
to detect double-free (glibc 2.29+) */
} tcache_entry;- next - Points to the previously freed chunk in the same bin. The list is singly-linked and LIFO (stack discipline).
- key - Added in glibc 2.29. Set to the address of the owning
tcache_perthread_structwhen a chunk is freed. Checked on the next free to detect a double-free. If the chunk is already in a tcache bin and freed again, the key matches and glibc callsmalloc_printerr.
tcache_perthread_struct
#define TCACHE_MAX_BINS 64
#define TCACHE_FILL_COUNT 7
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
/* per-bin counter: number of chunks in this bin.
Upper bound is TCACHE_FILL_COUNT (7) per bin. */
tcache_entry *entries[TCACHE_MAX_BINS];
/* per-bin singly-linked list head.
entries[i] -> chunk_a -> chunk_b -> ... -> NULL */
} tcache_perthread_struct;Each bin is indexed by the chunk size shifted right by 4 (i.e. size >> 4). A chunk of size 0x20 goes into entries[2]. When the count reaches 7, subsequent frees bypass the tcache and go to the fastbin or regular free list. On the first allocation after the tcache is empty, malloc fills it by moving up to 7 chunks from the fastbin or smallbin into the tcache.
Memory Layout
The tcache_perthread_struct lives in its own heap chunk (0x290 bytes on 64-bit). Below is a visual map of how the counts, entries, and freed chunks connect in memory.
tcache_put (push)
- 1. Check
counts[idx] < TCACHE_FILL_COUNT(7) - 2.
e->next = entries[idx](current head) - 3.
e->key = tcache(set key for double-free detection) - 4.
entries[idx] = e(new head) - 5.
counts[idx]++
tcache_get (pop)
- 1.
e = entries[idx](head) - 2.
entries[idx] = e->next(pop head) - 3.
counts[idx]-- - 4.
e->key = 0(clear key) - 5. Return
eto caller
Exploitation
The tcache simplifies heap exploitation compared to fastbin attacks. Two of the most relevant techniques are tcache poisoning (overwriting the next pointer) and double-free (circumventing the key-based check).
Tcache Poisoning
If an attacker can overwrite the next pointer of a freed chunk in the tcache (e.g. via a heap overflow or use-after-free), the next two allocations from that bin will return an arbitrary address. This grants an arbitrary write primitive.
/* Victim chunk is in tcache bin 0x20 */
/* Attacker overwrites victim->next with target address */
victim->next = (tcache_entry *)0x414141414141;
/* First malloc(0x18) returns the original victim chunk */
ptr1 = malloc(0x18);
/* Second malloc(0x18) returns our arbitrary address */
ptr2 = malloc(0x18); /* returns 0x414141414141 */
/* Attacker now has write-what-where */
*ptr2 = controlled_data;Double-Free Detection & Bypass (glibc 2.29+)
Since glibc 2.29, tcache_put checks whether e->key == tcachebefore inserting. If it matches, the chunk is already in the tcache and the process aborts.
/* Double-free check in glibc 2.29+ */
if (__glibc_unlikely (e->key == tcache))
malloc_printerr ("free(): double free detected in tcache");
/* Bypass strategy 1: overwrite key before second free */
chunk->key = NULL; /* or any value != tcache address */
free(chunk); /* check passes, chunk added to tcache again */
/* Bypass strategy 2: leak tcache address, then restore it */
/* (Useful when you need the key for something else) */The most common bypass is to overwrite chunk->key (the first 8 bytes of the user data after the next pointer in the freed chunk) with any value that does not equal the tcache address. Since the key occupies user data space, a single write via the chunk's content is sufficient to nullify the check.
Interactive Heap Lab
Experiment with tcache_put and tcache_get in real time. Select a size class, then free and allocate chunks while watching the linked list, counts, and key fields update. Try the exploit buttons to see tcache poisoning and the double-free check in action.