Fastbin Dup and Double-Free Attacks

Fastbin Dup and Double-Free Attacks - article cover image RE & Pwn
Time it takes to read this article 7 minutes.

Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.

Introduction

Before the tcache existed, the fastbin was the fast path, and the fastbin dup was the canonical first heap exploit every CTF player learned. It remains highly relevant: the tcache only covers seven chunks per size class, so once a bin is full or a target runs an older or tcache-disabled glibc, frees fall through to the fastbins and the classic double-free primitive comes back into play. This article dissects the fastbin free-list, the single, deliberately weak double-free check glibc places on it, and how a fastbin dup produces an aliased chunk you can steer to an arbitrary address.

The theme is the gap between what a check *looks* like it enforces and what it actually enforces. Fastbins carry exactly one anti-double-free guard, and it only inspects the current head of the list — so freeing the same chunk with a single other free in between sails straight past it. Pair that with the fastbin size check on allocation and you get both the technique and its one real constraint. We cover glibc behaviour generally, note that fastbins predate the tcache (2.26), and contrast their check with the tcache double-free key added in 2.29.

Attack Prerequisites

Fastbin dup is a double-free technique, so it needs the ability to free a chunk twice and later control the contents of the reissued chunk:

  • A double-free — the same pointer freed twice, typically because free does not null the pointer and the program lets you free an index again.
  • Fastbin-sized chunks — the freed chunk’s size must be within global_max_fast (default up to 0x80 on 64-bit); larger chunks take the unsorted/small path instead.
  • The fastbin path in play — either a tcache-disabled build, or the relevant tcache bin already filled to its 7-chunk limit so frees overflow into the fastbin.
  • A writable, correctly-sized fake chunk location for the arbitrary-allocation step, because malloc validates that a fastbin chunk’s size matches the bin before returning it.

How It Works

Fastbins are single-linked LIFO lists, one per small size class, that never coalesce eagerly — speed over tidiness. On free, a fastbin-sized chunk is pushed by writing the old head into its fd and making it the new head. On malloc, the head is popped and its fd becomes the new head. The only double-free protection is one line in _int_free: if (__builtin_expect (old == p, 0)) malloc_printerr("double free or corruption (fasttop)"). It compares the chunk being freed against the *current top* of the fastbin only. Freeing the same chunk twice in a row trips it — but freeing A, then B, then A does not, because when the second free(A) runs the top is B, not A.

That is the fastbin dup: after free(A); free(B); free(A) the list is A -> B -> A, and A appears twice. Three allocations then hand back A, B, and A again — but you are still holding a reference to the first A, so two live pointers now alias the same chunk. Writing through one changes the other’s fd. The exploitation move is to allocate the first A, overwrite its fd with the address of a fake chunk, then allocate through the list until malloc returns that fake location. The result is a malloc that returns a pointer you chose — the same arbitrary-allocation primitive as tcache poisoning, reached through the fastbin.

The one real constraint is the allocation-time size check. When malloc pops a fastbin chunk it verifies chunksize(victim) maps to the same bin index it was taken from, aborting with malloc(): memory corruption (fast) otherwise. So the arbitrary address you point fd at must have a valid fastbin size in its size field. The famous trick is to find a 0x7f byte (or any value whose class matches) already sitting in memory just above the target — historically the bytes above __malloc_hook — and treat that as a fake 0x70-class chunk. On glibc >= 2.32 the fastbin fd is also protected by safe-linking, so forging it requires a heap leak and a PROTECT_PTR computation exactly as in the tcache.

Vulnerable Code / Setup

The vulnerable program is again the note manager whose del frees without nulling, letting the same index be freed twice. To force the fastbin path on a modern glibc, the harness first fills the corresponding tcache bin:

// gcc -O0 -fno-stack-protector -no-pie fast.c -o fast
#include <stdlib.h>
#include <string.h>
char *ptrs[32];

void add(int i, size_t n){ ptrs[i] = malloc(n); }
void del(int i){ free(ptrs[i]); }        // BUG: index still usable (double free)
void edit(int i, char *s, size_t n){ memcpy(ptrs[i], s, n); }
C

Because glibc >= 2.26 sends small frees to the tcache first, a practical demonstration frees seven chunks of the target size to saturate the tcache bin (count == 7) before the interesting frees reach the fastbin. Confirm the protections and glibc so the size/safe-linking constraints are known:

$ checksec --file=./fast
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$ ldd --version | head -1
ldd (Ubuntu GLIBC 2.31-0ubuntu9) 2.31   # fastbin fd not yet safe-linked
Bash

Walkthrough / Exploitation

Step one is to drain the tcache bin so subsequent frees populate the fastbin. Allocate seven chunks of the target size and free them all; the eighth free of that size class now goes to the fastbin:

from pwn import *
io = process('./fast')
def add(i,n):     ...   # menu wrappers
def free(i):      ...
def edit(i,data): ...

for i in range(7):            # fill tcache[0x70] to its 7-chunk limit
    add(i, 0x68); 
for i in range(7):
    free(i)
Python

Now perform the dup. Allocate three fresh 0x70 chunks and free them in the A, B, A order so the fastbin becomes A -> B -> A. Inspect the fastbin in pwndbg to confirm the duplicate before touching it:

add(7, 0x68)  # A
add(8, 0x68)  # B
free(7)       # fastbin: A
free(8)       # fastbin: B -> A
free(7)       # fastbin: A -> B -> A   (passes the fasttop check)
Python
pwndbg> bins
fastbins
0x70: 0x5555...5290 -> 0x5555...5300 -> 0x5555...5290 (overlap!)
pwndbg> vis_heap_chunks   # confirm chunk A appears twice in the list
Bash

With the aliased chunk in hand, overwrite A‘s fd to point at the fake chunk. On glibc < 2.32 write the raw address; on >= 2.32 mangle it with safe-linking using A‘s address as pos. Then allocate along the list until malloc returns the fake location:

target = 0x404050            # just below a chosen writable target; valid size above
add(9, 0x68)                 # returns A; A->fd is now writable
edit(9, p64(target))         # A->fd = target  (mangle with PROTECT_PTR on >=2.32)

add(10, 0x68)               # returns B
add(11, 0x68)               # returns A again
arb = add(12, 0x68)         # returns 'target' — arbitrary allocation achieved
#  malloc() only returned 'target' because its size field held a valid 0x70 class
Python

From the arbitrary allocation the follow-on is standard: on old glibc, overwrite __malloc_hook/__free_hook with a one_gadget or system using the 0x7f fake-size trick to satisfy the fastbin size check near the hook; on modern glibc (hooks removed in 2.34), pivot the allocation onto a GOT entry under Partial RELRO or into an _IO_FILE structure. The primitive is identical to a tcache poison — an attacker-chosen malloc return — reached through a bin whose only guard checked the wrong thing.

Note: The fastbin size check is why fastbin dup is fussier than a tcache poison: the arbitrary target must present a valid fastbin size in its size field, so you cannot aim at a totally arbitrary aligned address the way the tcache struct route allows. Finding a serendipitous 0x7f/0x71 value above the target (the classic __malloc_hook - 0x23 misalignment) is the standard way to manufacture that fake size — and it is exactly the kind of trick the 2.32 tcache alignment check was added to spoil for the tcache path.

Opsec: On glibc >= 2.26 the tcache silently intercepts your frees, so a fastbin technique that ‘mysteriously’ does nothing is almost always a tcache bin that was not full. Fill it deliberately (seven frees) and verify with bins that the chunks are landing in fastbins, not tcachebins, before you build the dup. On >= 2.32 remember the fastbin fd is safe-linked too.

Detection and Defense

Double-free is a well-understood bug class with deterministic detection and cheap structural fixes:

  • Null after free (free(p); p = NULL;) — a freed pointer set to NULL turns a second free(NULL) into a no-op and eliminates the dup entirely.
  • Prefer recent glibc — the tcache double-free key (2.29) and fastbin safe-linking (2.32) make the equivalent moves far more expensive; do not run ancient libc on exposed services.
  • AddressSanitizer / Valgrind flag double-free and the resulting aliased use deterministically in CI, catching the bug long before it reaches a bin.
  • Full RELRO removes the GOT arbitrary-write target; combined with PIE and ASLR it forces a leak before any fake-chunk address can be aimed.
  • Monitor glibc abortsdouble free or corruption (fasttop) and malloc(): memory corruption (fast) in logs or cores are direct evidence of fastbin manipulation.

Real-World Impact

The fastbin dup is the historical root of the modern heap-exploitation curriculum — the technique the how2heap project popularised and that seeded the fake-size and hook-overwrite tricks reused across a decade of challenges. Double-free itself is CWE-415 and a steady presence in real CVEs across userland C, from parsers to daemons. Although the tcache stole the spotlight after 2.26, fastbin techniques never left: filling a tcache bin to force the fastbin path is a routine step, and on systems with older or specially configured glibc the fastbin dup is still the most direct route from a double-free to an arbitrary allocation.

Conclusion

Fastbin dup works because the fastbin’s sole double-free guard inspects only the current list head, so a single intervening free defeats it and the same chunk ends up in the bin twice. The resulting aliasing lets you overwrite a chunk’s fd, and — subject to the allocation-time size check that demands a valid fake size at the target — steer malloc to an address of your choice. It is the fastbin sibling of tcache poisoning, and understanding its one real constraint (the size check) and its modern speed-bumps (tcache interception, safe-linking) is what lets you reach for it exactly when the tcache path is closed.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL