Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mallopt(M_PERTURB) does not perturb the memory on free

I am trying to catch memory-related bugs such as use-after-free by mallopt(M_PERTURB, <value>). According to the doc, the memory will be initialized to value when it has been released by free.

However, I cannot observe this effect. Consider the following code:

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

int main()
{
    mallopt(M_PERTURB, 0x33);
    uint64_t *ptr = malloc(sizeof(uint64_t) * 4);
    if (!ptr) {
        return -1;
    }
    // cccccccccccccccc cccccccccccccccc cccccccccccccccc cccccccccccccccc
    printf("%016lx %016lx %016lx %016lx\n", ptr[0], ptr[1], ptr[2], ptr[3]);
    memset(ptr, 0x01, sizeof(uint64_t) * 4);

    free(ptr);
    // 000000055ea63e85 0b9c17e5a296ee24 0101010101010101 0101010101010101
    printf("%016lx %016lx %016lx %016lx\n", ptr[0], ptr[1], ptr[2], ptr[3]);

    return 0;
}

It did initialize memory to all 0xcc after malloc, but failed to do so in free. Is there something that I am missing? Or is it a bug of libc?

libc version: ldd (Debian GLIBC 2.36-9+deb12u13) 2.36

like image 684
davidhcefx Avatar asked Jan 23 '26 16:01

davidhcefx


2 Answers

As Andrew points out in this other answer, this is somewhat documented in the mallopt manual page, which states that "there is an off-by-sizeof(size_t) error in the implementation". This hints at the fact that the first sizeof(size_t) bytes are somehow not initialized on free. This is however wrong/misleading in two ways:

  1. The M_PERTURB option does cause free to initialize the chunk. This can also be seen in Glibc source code (e.g., here and here). However, immediately after this is done, bookkeeping information is stored in the same place.

    M_PERTURB does not give any guarantee about the persistence of this "perturb" state after free. There is indeed a brief a moment where the chunk is completely initialized to the perturb byte, it's just not externally observable by a library user because data is overwritten before free() returns.

    In case of chunks larger than M_MMAP_THRESHOLD it is not even possible to observe the data after free(), as the memory is unmapped. Such chunks are however still initialized on free if M_PERTURB is used, even though it's pointless.

  2. If we want to be pedantic, as you can clearly see from your test, this quirk can affect more than the first sizeof(size_t) bytes. Internal bookkeeping information varies by chunk size/type. In the current Glibc implementation, up to 4 * sizeof(size_t) bytes from the start of the chunk may be needed for internal bookkeeping.

For reference, the structure used internally by Glibc to represent heap chunks looks like this, where mchunk_prev_size and prev_size are right before the actual chunk. The pointer returned to the user starts at fd.

struct malloc_chunk {

  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

See also the source code comments in malloc.c for more information on this. They are very detailed.

like image 128
Marco Bonelli Avatar answered Jan 25 '26 07:01

Marco Bonelli


Per the Linux man page for mallopt():

BUGS

...

If mallopt() is used to set M_PERTURB, then, as expected, the bytes of allocated memory are initialized to the complement of the byte in value, and when that memory is freed, the bytes of the region are initialized to the byte specified in value. However, there is an off-by-sizeof(size_t) error in the implementation: instead of initializing precisely the block of memory being freed by the call free(p), the block starting at p+sizeof(size_t) is initialized.

Also, as printf() is known to use malloc() internally, it can modify the contents of the memory you freed. You need to copy the contents of the freed memory to a valid buffer immediately after calling free() and emit the copy in order to ensure you're seeing the actual contents of the freed buffer before anything could have modified it. Accessing freed memory is undefined behavior, so strictly speaking you can't expect any particular value.

And pedantically, %16lx is not a valid format specifier for a uint64_t. Either cast the value to [long] long unsigned (just don't use long unsigned on Windows...) and use the appropriate format specifier, or use PRIx64.

like image 22
Andrew Henle Avatar answered Jan 25 '26 09:01

Andrew Henle



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!