Search code examples
cheap-memoryfreeglibc

C free() on Ubuntu VM, a question regarding heap memory


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?


Solution

  • 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 internal tcache_key. It is indeed a random uintptr_t value initialized once at program startup using getrandom(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 of tcache_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:

    (gdb) x/20gx 0x5555555592a0 - 0x10
    0x555555559290: 0x0000000000000000  0x0000000000000021  -- b1
    0x5555555592a0: 0x0000000555555559  0xdefa7fb306dd6989
    0x5555555592b0: 0x0000000000000000  0x0000000000000021  -- b2
    0x5555555592c0: 0x000055500000c7f9  0xdefa7fb306dd6989
    0x5555555592d0: 0x0000000000000000  0x0000000000000031  -- b3
    0x5555555592e0: 0xcccccccccccccccc  0xcccccccccccccccc
    0x5555555592f0: 0xcccccccccccccccc  0x00000000000000cc
    0x555555559300: 0x0000000000000000  0x00000000000003f1  -- b4
    0x555555559310: 0xdddddddddddddddd  0xdddddddddddddddd
    0x555555559320: 0xdddddddddddddddd  0xdddddddddddddddd
    

    On free, both b1 and b2 end up in the same tcache bucket, the smallest one, for size 16 (malloc(8) is rounded up to 16). We have b2 as the head since the list is LIFO.

    The tcache looks like this:

    {
      counts = {2, 0, 0, 0, ...},
      entries = {0x5555555592c0, 0x0, 0x00, 0x00, ...}
    }
    

    As you can see above, 0xdefa7fb306dd6989 is the value of tcache_key. Looking at the free chunk at 0x5555555592c0, its ->next pointer is mangled to 0x000055500000c7f9. The real value can be obtained as:

    (0x5555555592c0UL >> 12) ^ 0x55500000c7f9UL == 0x5555555592a0UL
    

    The ->next->next is simply NULL (mangled to 0x0000000555555559).


    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 the pwndbg GDB plugin, you will have very useful commands to inspect the glibc heap and the various bins. For example:

    • heap showing all chunks
    • bins showing the state of arena bins
    • arena showing the actual arena struct
    • tcache showing the state of tcache

    and more...


    For more info, also take a look at this interesting article: Tcache Keys A primitive double-free protection.