Skip to main content

Overview

Fastbin Dup Consolidate is an elegant technique that leverages malloc_consolidate() to bypass the typical double-free restrictions. By triggering consolidation of fastbins with the top chunk, we can create two pointers to the same memory region - one from the original allocation and one after it’s been merged and reallocated. This works even with large, tcache-sized chunks.
Glibc Version Compatibility: Latest (tested on glibc 2.23 - 2.41)This technique exploits a fundamental behavior of malloc_consolidate that exists across all versions.

What This Achieves

  • Chunk duplication: Two pointers to the same memory without direct double-free
  • Large chunk control: Works with tcache-sized chunks (up to 0x410) and larger
  • Bypass PREV_INUSE check: Large chunks normally resist double-free due to PREV_INUSE validation
  • Heap feng shui: Precise control over chunk placement

Prerequisites

  • Ability to trigger large allocation (>= 0x3f0 bytes requested)
  • Control over free operations
  • Ability to free the same chunk twice (double-free vulnerability)
  • Understanding of tcache fill behavior

Understanding malloc_consolidate

malloc_consolidate() is a critical internal function that merges all fastbin chunks with their neighbors, places them in the unsorted bin, and merges them with the top chunk if possible.When is it called?
  1. Large allocation: Requesting chunk size >= 0x400 (smallbin threshold)
  2. Top too small: No suitable bins and top chunk insufficient
  3. Large free: Freeing chunk >= 65536 bytes (FASTBIN_CONSOLIDATION_THRESHOLD)
  4. Manual triggers: malloc_trim() or mallopt()
For this technique, we target case #1.

The Technique

Step-by-Step Walkthrough

1

Fill the tcache

Allocate and free 7 chunks to fill the tcache for our target size (0x40).
void *ptr[7];
for(int i = 0; i < 7; i++)
    ptr[i] = malloc(0x40);

// Allocate target chunk
void* p1 = malloc(0x40);

// Fill tcache
for(int i = 0; i < 7; i++)
    free(ptr[i]);
Now the tcache is full, so freeing p1 will go to fastbin.
2

Free chunk to fastbin

Free p1, placing it in the fastbin (since tcache is full).
free(p1);  // Goes to fastbin
3

Trigger malloc_consolidate

Request a large allocation (>= 0x400). This triggers malloc_consolidate().
// Request 0x400 bytes = 0x410 chunk (tcache-sized!)
void* p2 = malloc(0x400);
What happens:
  1. malloc_consolidate merges p1 with the top chunk
  2. p2 is allocated from the new top
  3. Result: p1 == p2 (same address!)
4

Double-free the duplicated chunk

Now we have two pointers (p1 and p2) to the same memory. Free through p1 again.
free(p1);  // Double-free, but not detected!
This places the 0x410 chunk in the tcache. p2 still points to it but hasn’t been freed directly.
5

Reallocate to achieve duplication

Request 0x400 bytes again, getting back the chunk that p2 still points to.
void *p3 = malloc(0x400);
assert(p3 == p2);  // Same pointer!
Result: p2 and p3 both point to the same allocated chunk, neither has been freed directly from the allocator’s perspective.

Full Source Code

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

/*
Original reference: https://valsamaras.medium.com/the-toddlers-introduction-to-heap-exploitation-fastbin-dup-consolidate-part-4-2-ce6d68136aa8

This demonstrates malloc_consolidate and how it can be leveraged with a
double free to gain two pointers to the same large-sized chunk, which is usually difficult to do 
directly due to the previnuse check. Interestingly this also includes tcache-sized chunks of certain sizes.

malloc_consolidate(https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4714) essentially
merges all fastbin chunks with their neighbors, puts them in the unsorted bin and merges them with top
if possible.

As of glibc version 2.35 it is called only in the following five places:
1. _int_malloc: A large sized chunk is being allocated (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3965)
2. _int_malloc: No bins were found for a chunk and top is too small (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4394)
3. _int_free: If the chunk size is >= FASTBIN_CONSOLIDATION_THRESHOLD (65536) (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4674)
4. mtrim: Always (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L5041)
5. __libc_mallopt: Always (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L5463)

We will be targeting the first place, so we will need to allocate a chunk that does not belong in the 
small bin (since we are trying to get into the 'else' branch of this check: https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3901). 
This means our chunk will need to be of size >= 0x400 (it is thus large-sized). Notably, the 
biggest tcache sized chunk is 0x410, so if our chunk is in the [0x400, 0x410] range we can utilize 
a double free to gain control of a tcache sized chunk.   
*/

#define CHUNK_SIZE 0x400

int main() {
    printf("This technique will make use of malloc_consolidate and a double free to gain a duplication in the tcache.\n");
    printf("Lets prepare to fill up the tcache in order to force fastbin usage...\n\n");

    void *ptr[7];

    for(int i = 0; i < 7; i++)
        ptr[i] = malloc(0x40);

    void* p1 = malloc(0x40);
    printf("Allocate another chunk of the same size p1=%p \n", p1);

    printf("Fill up the tcache...\n");
    for(int i = 0; i < 7; i++)
        free(ptr[i]);

    printf("Now freeing p1 will add it to the fastbin.\n\n");
    free(p1);

    printf("To trigger malloc_consolidate we need to allocate a chunk with large chunk size (>= 0x400)\n");
    printf("which corresponds to request size >= 0x3f0. We will request 0x400 bytes, which will gives us\n");
    printf("a tcache-sized chunk with chunk size 0x410 ");
    void* p2 = malloc(CHUNK_SIZE);

    printf("p2=%p.\n", p2);

    printf("\nFirst, malloc_consolidate will merge the fast chunk p1 with top.\n");
    printf("Then, p2 is allocated from top since there is no free chunk bigger (or equal) than it. Thus, p1 = p2.\n");

    assert(p1 == p2);

    printf("We will double free p1, which now points to the 0x410 chunk we just allocated (p2).\n\n");
    free(p1); // vulnerability (double free)
    printf("It is now in the tcache (or merged with top if we had initially chosen a chunk size > 0x410).\n");

    printf("So p1 is double freed, and p2 hasn't been freed although it now points to a free chunk.\n");

    printf("We will request 0x400 bytes. This will give us the 0x410 chunk that's currently in\n");
    printf("the tcache bin. p2 and p1 will still be pointing to it.\n");
    void *p3 = malloc(CHUNK_SIZE);

    assert(p3 == p2);

    printf("We now have two pointers (p2 and p3) that haven't been directly freed\n");
    printf("and both point to the same tcache sized chunk. p2=%p p3=%p\n", p2, p3);
    printf("We have achieved duplication!\n\n");

    printf("Note: This duplication would have also worked with a larger chunk size, the chunks would\n");
    printf("have behaved the same, just being taken from the top instead of from the tcache bin.\n");
    printf("This is pretty cool because it is usually difficult to duplicate large sized chunks\n");
    printf("because they are resistant to direct double free's due to their PREV_INUSE check.\n");

    return 0;
}

Key Concepts

Normally, double-freeing a large chunk triggers a PREV_INUSE check that crashes the program. However:
  1. We free a small fastbin chunk (p1)
  2. malloc_consolidate merges it with top
  3. We allocate a large chunk (p2) from the new top
  4. p1 and p2 now point to the same address
  5. We free p1 again - but this time it’s treated as a 0x410 chunk (the size at p2)
  6. No double-free detection because p2 was never freed directly
The key insight: the allocator “forgets” about p1’s original size and location after consolidation.
The magic number 0x400 (requesting 0x3f0-0x3ff bytes) is significant:
  • Actual chunk size allocated: 0x410 (includes metadata)
  • This is the largest tcache-sized chunk
  • Triggers malloc_consolidate (>= large bin threshold)
  • Allows duplication of a tcache-manageable chunk
You can use larger sizes too, but then chunks go to unsorted bin instead of tcache, making the exploit different.
When malloc_consolidate is triggered:
// Simplified pseudocode
for (each fastbin chunk) {
    unlink from fastbin;
    if (next chunk is free) merge();
    if (prev chunk is free) merge();
    if (borders top chunk) merge_with_top();
    else add_to_unsorted_bin();
}
In our case, p1 is adjacent to top, so it gets merged with top. When we allocate p2, it comes from this expanded top, landing at the same address as p1.

Common Use Cases

  1. Overlapping chunks: Create multiple valid pointers to the same large chunk
  2. Bypass large bin protections: Circumvent PREV_INUSE checks on large chunks
  3. Tcache manipulation: Control tcache behavior with large allocations
  4. Heap grooming: Precisely control heap layout through consolidation

Variations

The technique works with any large size:
#define CHUNK_SIZE 0x1000  // Much larger

void* p1 = malloc(0x40);  // Small chunk
free(p1);                 // To fastbin

void* p2 = malloc(CHUNK_SIZE);  // Triggers consolidate
assert(p1 == p2);               // Still works!

free(p1);  // Double-free
void* p3 = malloc(CHUNK_SIZE);  // From unsorted bin
assert(p2 == p3);
Difference: with sizes > 0x410, chunks come from unsorted bin instead of tcache.
You can consolidate multiple fastbin chunks simultaneously:
void* chunks[10];
for (int i = 0; i < 10; i++) {
    chunks[i] = malloc(0x40);
    free(chunks[i]);  // All to fastbin
}

malloc(0x1000);  // Consolidates all at once
This can create complex heap layouts.

Defense Mechanisms

Why this still works:
  • malloc_consolidate is a fundamental performance optimization
  • No practical way to prevent the merging behavior
  • The double-free happens across different “contexts” (small vs large chunk)
  • Allocator state tracking doesn’t span consolidation events
This makes it one of the more robust heap exploitation techniques.

CTF Challenges

Hitcon CTF 2016

SleepyHolder - Classic challenge using fastbin consolidationWriteup
  • Fastbin Dup - Basic double-free primitive
  • [House of Einherjar/techniques/house/house-of-einherjar) - Another consolidation-based technique
  • [Poison Null Byte/techniques/advanced/poison-null-byte) - Chunk consolidation through metadata corruption

Practice & Resources

Ret2 Wargames

Debug this technique interactively in your browser using GDB

References

Build docs developers (and LLMs) love