A simple program to allocate and free heap memory:
int main(int argc, char **argv) {
char *b1, *b2, *b3, *b4, *b_large;
b1 = malloc(8);
memset(b1, 0xaa, 8);
b2= malloc(16);
memset(b2, 0xbb, 16);
b3 = malloc(25);
memset(b3, 0xcc, 25);
b4= malloc(1000);
memset(b4, 0xdd, 1000);
free(b1);
free(b2);
free(b3);
free(b4);
Before first free():
(gdb) x/20gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0xaaaaaaaaaaaaaaaa 0x0000000000000000
0x5555555592b0: 0x0000000000000000 0x0000000000000021
0x5555555592c0: 0xbbbbbbbbbbbbbbbb 0xbbbbbbbbbbbbbbbb
0x5555555592d0: 0x0000000000000000 0x0000000000000031
0x5555555592e0: 0xcccccccccccccccc 0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc 0x00000000000000cc
0x555555559300: 0x0000000000000000 0x00000000000003f1
0x555555559310: 0xdddddddddddddddd 0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd 0xdddddddddddddddd
And after first free():
(gdb) x/20gx 0x555555559290
0x555555559290: 0x0000000000000000 0x0000000000000021
0x5555555592a0: 0x0000000555555559 0xd13e7903c502febc
0x5555555592b0: 0x0000000000000000 0x0000000000000021
0x5555555592c0: 0xbbbbbbbbbbbbbbbb 0xbbbbbbbbbbbbbbbb
0x5555555592d0: 0x0000000000000000 0x0000000000000031
0x5555555592e0: 0xcccccccccccccccc 0xcccccccccccccccc
0x5555555592f0: 0xcccccccccccccccc 0x00000000000000cc
0x555555559300: 0x0000000000000000 0x00000000000003f1
0x555555559310: 0xdddddddddddddddd 0xdddddddddddddddd
0x555555559320: 0xdddddddddddddddd 0xdddddddddddddddd
I was expecting to see readable forward and back pointers in the second line of memory, and
in the third line 0x20 in both 8-bytes segments.
Can anyone explain why the free() function would behave in this way?
2
Answers
Following Marco's answer, I did some playing around with GLIBC_TUNABLES as follows:
Disabling the tcache and fast bins as above give the behaviour that I expected in my original question, an unsorted bin in response to the free() call.
Assuming you are working with Ubuntu 22.04 and therefore glibc 2.36.
Modern heap allocators nowadays are… quite complex, and glibc is a prime example of this. Don’t expect to see nice things such as plain pointers that easily.
The seemingly random value you see (
0xd13e7903c502febc
) is the internaltcache_key
. It is indeed a randomuintptr_t
value initialized once at program startup usinggetrandom(2)
and later inserted into free chunks that are kept in tcache.The value of
tcache_key
is inserted into chunks that go in tcache and then checked on free as a simple hardening against double-free. It is removed when a tcache chunk is re-allocated. This was implemented in glibc 2.34. Previously (glibc 2.29-2.33) a fixed value was used instead oftcache_key
.If you are wondering what the "tcache" is, it is a per-thread cache consisting of several buckets. Each bucket is reserved for a given allocation size and holds freed chunks of exactly that size in a singly linked list of at most 7 elements in LIFO order (newly freed chunks are inserted in the head). When a bucket is full, freeing a chunk of that size will not add it to tcache but instead follow the "normal" freeing procedure and end up in one of the normal arena "bins" according to the rather convoluted glibc algorithm.
The pointers to the next chunk in tcache buckets are also mangled on free and unmangled on alloc (as you can see here) through dedicated macros that use the implicit randomness of the mapping (
mmap_base
) provided by the kernel through ASLR. This is why you don’t see a clear pointer in the chunks either.After
free(b1); free(b2);
I have the following:On free, both
b1
andb2
end up in the sametcache
bucket, the smallest one, for size 16 (malloc(8)
is rounded up to 16). We haveb2
as the head since the list is LIFO.The
tcache
looks like this:As you can see above,
0xdefa7fb306dd6989
is the value oftcache_key
. Looking at the free chunk at0x5555555592c0
, its->next
pointer is mangled to0x000055500000c7f9
. The real value can be obtained as:The
->next->next
is simplyNULL
(mangled to0x0000000555555559
).When you say "I was expecting to see readable forward and back pointers in the second line of memory" you are describing non-tcache behavior of large chunks. Even ignoring/disabling tcache, small enough chunks can also be kept in singly linked lists (fastbins) again up to a certain fixed number (IIRC). It is only for larger sizes that you actually have doubly linked lists where both pointers are used as you expect.
If you want to experiment more, fill tcache first by freeing 7 chunks of the same size, then do some more frees of that size and check again.
Installing debug symbols from the
libc6-dbg
package and using thepwndbg
GDB plugin, you will have very useful commands to inspect the glibc heap and the various bins. For example:heap
showing all chunksbins
showing the state of arena binsarena
showing the actual arena structtcache
showing the state of tcacheand more…
For more info, also take a look at this interesting article: Tcache Keys
A primitive double-free protection.