Free and its hidden details (tcache)
free()
Free and its hidden details (tcache)
What happens after free():
In our previous blog, we explored how malloc() works internally and why it behaves the way it does.
Today we go one step further and look at free**()**, and what really happens to memory after it is released.
Consider this small program:
int main(int argc, char *argv[])
{
char *a = malloc(50);
strncpy(a, "Is this enough ? Let's dive deep in memory", 50);
printf("This is the message : %s\n", a);
free(a);
printf("After free This is the message : %s\n", a);
}
Before Free() : a has address 0x5555555592a0 we could see that it has the string Is this enough ? Let's dive deep in memory in below dump 👇
(gdb) x/50cb a
0x5555555592a0: 73 'I' 115 's' 32 ' ' 116 't' 104 'h' 105 'i' 115 's' 32 ' '
0x5555555592a8: 101 'e' 110 'n' 111 'o' 117 'u' 103 'g' 104 'h' 32 ' ' 63 '?'
0x5555555592b0: 32 ' ' 76 'L' 101 'e' 116 't' 39 '\'' 115 's' 32 ' ' 100 'd'
0x5555555592b8: 105 'i' 118 'v' 101 'e' 32 ' ' 100 'd' 101 'e' 101 'e' 112 'p'
0x5555555592c0: 32 ' ' 105 'i' 110 'n' 32 ' ' 109 'm' 101 'e' 109 'm' 111 'o'
0x5555555592c8: 114 'r' 121 'y' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0x5555555592d0: 0 '\000' 0 '\000'
After Free() : a has address 0x5555555592a0 we could see that it has the string YUUU.(some junks)...Let's dive deep in memory in below dump 👇
(gdb) x/50cb a
0x5555555592a0: 89 'Y' 85 'U' 85 'U' 85 'U' 5 '\005' 0 '\000' 0 '\000' 0 '\000'
0x5555555592a8: 119 'w' -17 '\357' -78 '\262' 81 'Q' -123 '\205' -126 '\202' -22 '\352' 112 'p'
0x5555555592b0: 32 ' ' 76 'L' 101 'e' 116 't' 39 '\'' 115 's' 32 ' ' 100 'd'
0x5555555592b8: 105 'i' 118 'v' 101 'e' 32 ' ' 100 'd' 101 'e' 101 'e' 112 'p'
0x5555555592c0: 32 ' ' 105 'i' 110 'n' 32 ' ' 109 'm' 101 'e' 109 'm' 111 'o'
0x5555555592c8: 114 'r' 121 'y' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'
0x5555555592d0: 0 '\000' 0 '\000'
Notice something strange:
- The first few bytes are overwritten (
YUUU...) - But most of the string is still there
So **free() did not erase your data.
It only repurposed part of the memory. Why?**
Where freed memory really goes — tcache
Since glibc 2.26, freed chunks go first into tcache (Thread Cache).
What is Tcache?
Think of tcacheas a personal memory recycling bin for each thread in your program. It’s designed with one goal speed, we gonna see it’s purpose in next blog.
Here’s what makes tcache special:
- Per-thread storage: Each thread maintains its own independent cache
- Recently freed chunks: Stores your most recently deallocated memory
- Fast allocation: When you call
malloc()again, it can instantly reuse freed chunks - Lock-free access: No contention with other threads, avoiding expensive synchronization (we gonna see it in next blog)
How Tcache Works in Practice
When you free a chunk (like our variable a), here's what happens:
- The freed chunk is added to tcache
- Tcache organizes chunks by size (bin index formula!)
bin_index = (chunk_size / 16) — 2 - On the next
malloc()request, tcache is checked first - If a matching size is found, that chunk is returned immediately — no new allocation needed!
This is why tcache is so fast: it’s a thread-local recycling system that keeps freed memory ready for instant reuse.
Seeing Tcache in Action
Let’s examine our earlier example and see what tcache looks like before and after freeing chunk a.
Before free(a): Chunk a contains our user data, and tcache is empty 👇
Allocated chunk:
----------------
pwndbg> p *(struct malloc_chunk*)(a-16)
$4 = {
mchunk_prev_size = 0,
mchunk_size = 65,
fd = 0x2073696874207349, ---> Is this
bk = 0x3f206867756f6e65, ---> enough ?
fd_nextsize = 0x6420732774654c20, ---> Let's d
bk_nextsize = 0x7065656420657669, ---> ive deep
}
pwndbg>
Address of a:
-------------
pwndbg> p/x a
$1 = 0x5555555592a0
tcache struct:
--------------
pwndbg> p *tcache
$2 = {
counts = {0 <repeats 64 times>},
entries = {0x0 <repeats 64 times>}
}
After free : chunk abecomes a tcache entry
Freed chunk a: user data seems corrupted
--------------
pwndbg> p *(struct malloc_chunk*)(a-16)
$18 = {
mchunk_prev_size = 0,
mchunk_size = 65,
fd = 0x555555559,
bk = 0x22a843ceb220fa8e,
fd_nextsize = 0x6420732774654c20, ---> Let's d
bk_nextsize = 0x7065656420657669, ---> ive deep
}
tcache struct:
--------------
pwndbg> p *tcache
$19 = {
counts = {0, 0, 1, 0 <repeats 61 times>},
entries = {0x0, 0x0, 0x5555555592a0, 0x0 <repeats 61 times>}
}
pwndbg>
freed chunk a's corrupted data: (typecasted into tcache_entry)
-------------------------------
pwndbg> p *(struct tcache_entry*)a
$21 = {
next = 0x555555559,
key = 2497320548407769742
}
pwndbg>
- The Counts array in tcahe shows one Freed Chunk
counts = {0, 0, 1, 0 <repeats 61 times>}, The value1atindex 2tells us there’s one freed chunk in this bin of size 64 bytes. Using formula:chunk_size = (bin_index + 2) × 16=> (2 + 2) × 16 => 64 bytes. - The Entries array points to the freed chunk. The address
0x5555555592a0inentries[2]is the head of the linked list for 64-byte chunks—which is exactly where our freed chunkalives.entries = {0x0, 0x0, 0x5555555592a0, 0x0 <repeats 61 times>} - Inside the freed chunk
a, thenextpointer doesn't directly show the next chunk's address. It's XOR-mangled for security. To decrypt it: Raw_Next = (Address >> 12) ^ Mangled_Nextpwndbg> p/x (0x5555555592a0 >> 12) ^ 0x555555559 $24 = 0x0 - The result is
0x0(NULL), which makes perfect sense—this is the only freed chunk in the chain, so there's no "next" chunk to point to. - The
keyfield stores the random bits, which generates once for the thread
- When a chunk is freed,
keyis set. - On subsequent
free()calls for same pointer, malloc checks: "Does this chunk's key already match tcache_key?” (which already generated and global) - If yes → double-free detected! The program aborts to prevent exploitation.
Resusing Memory : How malloc reclaims freed chunks from the tcache
When multiple chunks of the same size are freed:
- They form a linked list (LIFO: Last In, First Out)
- Each chunk’s
nextpointer points to the previously freed chunk - The most recently freed chunk becomes the head of the list
When you call malloc() again with a matching size, tcache returns the head of the list (the last freed chunk), updates the head pointer, and decrements the count—all in constant time O(1).
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *a = malloc(50);
char *b = malloc(50);
char *c = malloc(50);
free(a);
free(b);
free(c);
char *d = malloc(50);
printf("It's now in pointer c");
}
- In above exampel after three free() this is the gdb trace 👇
Address of a,b,c:
------------------
pwndbg> p/x a
$10 = 0x5555555592a0
pwndbg> p/x b
$11 = 0x5555555592e0
pwndbg> p/x c
$12 = 0x555555559320
pwndbg>
tcache structure data:
----------------------
pwndbg> p *tcache
$1 = {
counts = {0, 0, 3, 0 <repeats 61 times>},
entries = {0x0, 0x0, 0x555555559320, 0x0 <repeats 61 times>}
}
head of entry points to last freed chunk c:
-------------------------------------------
pwndbg> p *(struct tcache_entry*)0x555555559320
$3 = {
next = 0x55500000c7b9,
key = 1543625295978182647
}
c's next points to b:
-----------------------
pwndbg> p/x (0x555555559320>>12)^0x55500000c7b9
$7 = 0x5555555592e0
pwndbg> p *(struct tcache_entry*)0x5555555592e0
$8 = {
next = 0x55500000c7f9,
key = 1543625295978182647
}
b's next points to a:
---------------------
pwndbg> p/x (0x5555555592e0>>12)^0x55500000c7f9
$9 = 0x5555555592a0
pwndbg>
pwndbg> p *(struct tcache_entry*)0x5555555592a0
$13 = {
next = 0x555555559,
key = 1543625295978182647
}
a's next point's to null:
-------------------------
pwndbg> p/x (0x5555555592a0>>12) ^ 0x555555559
$14 = 0x0
pwndbg>
- After freeing chunks
a,b, andc, the tcache structure reveals, the value3at index 2 confirms we have three freed chunks of size 64 bytes waiting in tcache.counts = {0, 0, 3, 0 <repeats 61 times>}, - Entries array points to the most recent freed chunk, which is c’s address.
entries = {0x0, 0x0, 0x555555559320, 0x0 <repeats 61 times>} chasmangled next — 0x55500000c7b9which is decrypted usingp/x (0x555555559320>>12) ^ 0x55500000c7b9 = 0x5555555592e0which isb.- And
bpoints to0x55500000c7f9which is0x5555555592a0i.e.address of a - And
apoints to NULL.
After next malloc char *d = malloc(50); last chunk gets returned to d and removed from tcache free list, and the entries now points to b 0x5555555592e0 👇
What Happens on the Next malloc?
When we call char *d = malloc(50);, here's what tcache does:
- Returns chunk c (the head of the list) to
d - Updates the head to point to chunk b
- Decrements the count from 3 to 2
pwndbg> p/x b
$11 = 0x5555555592e0
pwndbg> p *tcache
$16 = {
counts = {0, 0, 2, 0 <repeats 61 times>},
entries = {0x0, 0x0, 0x5555555592e0, 0x0 <repeats 61 times>}
}
pwndbg> p/x d
$17 = 0x555555559320
Result: d now points to the same memory that c used to occupy—instant recycling with zero overhead!

upcoming : why tcache ? how free handled before tcache ?
메타데이터
- post_id
- 4a49dd3b2f08
- slug
- free-and-its-hidden-details-tcache-4a49dd3b2f08
- url
- https://medium.com/@mrajagopalaswamy/free-and-its-hidden-details-tcache-4a49dd3b2f08
- canonical_url
- https://medium.com/@mrajagopalaswamy/free-and-its-hidden-details-tcache-4a49dd3b2f08
- author_url
- https://medium.com/@mrajagopalaswamy
- status
- ok
- fetched_at
- 2026-06-24 23:31:39