glibc ptmalloc ยท thread cache

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.

#include <structs>#include <layout>#include <exploits>#include <lab>
01

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;

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;
Key Detail

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.

02

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_perthread_struct @ 0x7f0000000000counts[0..63] (uint16_t each) | total: 128 bytescounts[2] = 3entries[0..63] (tcache_entry* each) | total: 512 bytesentries[2] ---> chunk_Cchunk_C @ 0x555500100080next ---> chunk_Bkey ---> tcachenextchunk_B @ 0x555500100040next ---> chunk_Achunk_A @ 0x555500100000next = NULLkey ---> tcache_perthread_structLayout notessizeof(tcache_perthread_struct) = 64 * 2 + 64 * 8 = 640 bytes (0x280), chunk size = 0x290Singly-linked LIFO: most recently freed chunk is at the head. tcache_get pops the head; tcache_put pushes at the head.

tcache_put (push)

  1. 1. Check counts[idx] < TCACHE_FILL_COUNT (7)
  2. 2. e->next = entries[idx] (current head)
  3. 3. e->key = tcache (set key for double-free detection)
  4. 4. entries[idx] = e (new head)
  5. 5. counts[idx]++

tcache_get (pop)

  1. 1. e = entries[idx] (head)
  2. 2. entries[idx] = e->next (pop head)
  3. 3. counts[idx]--
  4. 4. e->key = 0 (clear key)
  5. 5. Return e to caller
03

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) */
Detection Bypass

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.

04

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.

bin: 0x20 (32 B)count: 0/7head: NULL
00100200next: NULLkey: NULL00100240next: 00100200key: NULL00100280next: 00100240key: NULL001002c0next: 00100280key: NULL
transaction log
> tcache lab initialized