free - HackTricks

# free

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.

## Free Order Summary

(No checks are explained in this summary and some case have been omitted for brevity)

1. If the address is null don't do anything
2. If the chunk was mmaped, mummap it and finish
3. Call `_int_free`:
   1. If possible, add the chunk to the tcache
   2. If possible, add the chunk to the fast bin
   3. Call `_int_free_merge_chunk` to consolidate the chunk is needed and add it to the unsorted list

## __libc_free

`Free` calls `__libc_free`.

- If the address passed is Null (0) don't do anything.
- Check pointer tag
- If the chunk is `mmaped`, `mummap` it and that all
- If not, add the color and call `_int_free` over it

__lib_free code

    void
    __libc_free (void *mem)
    {
      mstate ar_ptr;
      mchunkptr p;                          /* chunk corresponding to mem */

      if (mem == 0)                              /* free(0) has no effect */
        return;

      /* Quickly check that the freed pointer matches the tag for the memory.
         This gives a useful double-free detection.  */
      if (__glibc_unlikely (mtag_enabled))
        *(volatile char *)mem;

      int err = errno;

      p = mem2chunk (mem);

      if (chunk_is_mmapped (p))                       /* release mmapped memory. */
        {
          /* See if the dynamic brk/mmap threshold needs adjusting.
    	 Dumped fake mmapped chunks do not affect the threshold.  */
          if (!mp_.no_dyn_threshold
              && chunksize_nomask (p) > mp_.mmap_threshold
              && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX)
            {
              mp_.mmap_threshold = chunksize (p);
              mp_.trim_threshold = 2 * mp_.mmap_threshold;
              LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                          mp_.mmap_threshold, mp_.trim_threshold);
            }
          munmap_chunk (p);
        }
      else
        {
          MAYBE_INIT_TCACHE ();

          /* Mark the chunk as belonging to the library again.  */
          (void)tag_region (chunk2mem (p), memsize (p));

          ar_ptr = arena_for_chunk (p);
          _int_free (ar_ptr, p, 0);
        }

      __set_errno (err);
    }
    libc_hidden_def (__libc_free)

## _int_free

### _int_free start

It starts with some checks making sure:

- the pointer is aligned, or trigger error `free(): invalid pointer`
- the size isn't less than the minimum and that the size is also aligned or trigger error: `free(): invalid size`

_int_free start

    // From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4493C1-L4513C28

    #define aligned_OK(m) (((unsigned long) (m) &MALLOC_ALIGN_MASK) == 0)

    static void
    _int_free (mstate av, mchunkptr p, int have_lock)
    {
      INTERNAL_SIZE_T size;        /* its size */
      mfastbinptr *fb;             /* associated fastbin */

      size = chunksize (p);

      /* Little security check which won't hurt performance: the
         allocator never wraps around at the end of the address space.
         Therefore we can exclude some size values which might appear
         here by accident or by "design" from some intruder.  */
      if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
          || __builtin_expect (misaligned_chunk (p), 0))
        malloc_printerr ("free(): invalid pointer");
      /* We know that each chunk is at least MINSIZE bytes in size or a
         multiple of MALLOC_ALIGNMENT.  */
      if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
        malloc_printerr ("free(): invalid size");

      check_inuse_chunk(av, p);

### _int_free tcache

It'll first try to allocate this chunk in the related tcache. However, some checks are performed previously. It'll loop through all the chunks of the tcache in the same index as the freed chunk and:

- If there are more entries than `mp_.tcache_count`: `free(): too many chunks detected in tcache`
- If the entry is not aligned: free(): `unaligned chunk detected in tcache 2`
- if the freed chunk was already freed and is present as chunk in the tcache: `free(): double free detected in tcache 2`

If all goes well, the chunk is added to the tcache and the functions returns.

_int_free tcache

    // From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4515C1-L4554C7
    #if USE_TCACHE
      {
        size_t tc_idx = csize2tidx (size);
        if (tcache != NULL && tc_idx < mp_.tcache_bins)
          {
    	/* Check to see if it's already in the tcache.  */
    	tcache_entry *e = (tcache_entry *) chunk2mem (p);

    	/* This test succeeds on double free.  However, we don't 100%
    	   trust it (it also matches random payload data at a 1 in
    	   2^<size_t> chance), so verify it's not an unlikely
    	   coincidence before aborting.  */
    	if (__glibc_unlikely (e->key == tcache_key))
    	  {
    	    tcache_entry *tmp;
    	    size_t cnt = 0;
    	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
    	    for (tmp = tcache->entries[tc_idx];
    		 tmp;
    		 tmp = REVEAL_PTR (tmp->next), ++cnt)
    	      {
    		if (cnt >= mp_.tcache_count)
    		  malloc_printerr ("free(): too many chunks detected in tcache");
    		if (__glibc_unlikely (!aligned_OK (tmp)))
    		  malloc_printerr ("free(): unaligned chunk detected in tcache 2");
    		if (tmp == e)
    		  malloc_printerr ("free(): double free detected in tcache 2");
    		/* If we get here, it was a coincidence.  We've wasted a
    		   few cycles, but don't abort.  */
    	      }
    	  }

    	if (tcache->counts[tc_idx] < mp_.tcache_count)
    	  {
    	    tcache_put (p, tc_idx);
    	    return;
    	  }
          }
      }
    #endif

### _int_free fast bin

Start by checking that the size is suitable for fast bin and check if it's possible to set it close to the top chunk.

Then, add the freed chunk at the top of the fast bin while performing some checks:

- If the size of the chunk is invalid (too big or small) trigger: `free(): invalid next size (fast)`
- If the added chunk was already the top of the fast bin: `double free or corruption (fasttop)`
- If the size of the chunk at the top has a different size of the chunk we are adding: `invalid fastbin entry (free)`

_int_free Fast Bin

     // From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4556C2-L4631C4

     /*
        If eligible, place chunk on a fastbin so it can be found
        and used quickly in malloc.
      */

      if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

    #if TRIM_FASTBINS
          /*
    	If TRIM_FASTBINS set, don't place chunks
    	bordering top into fastbins
          */
          && (chunk_at_offset(p, size) != av->top)
    #endif
          ) {

        if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
    			  <= CHUNK_HDR_SZ, 0)
    	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
    			     >= av->system_mem, 0))
          {
    	bool fail = true;
    	/* We might not have a lock at this point and concurrent modifications
    	   of system_mem might result in a false positive.  Redo the test after
    	   getting the lock.  */
    	if (!have_lock)
    	  {
    	    __libc_lock_lock (av->mutex);
    	    fail = (chunksize_nomask (chunk_at_offset (p, size)) <= CHUNK_HDR_SZ
    		    || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
    	    __libc_lock_unlock (av->mutex);
    	  }

    	if (fail)
    	  malloc_printerr ("free(): invalid next size (fast)");
          }

        free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);

        atomic_store_relaxed (&av->have_fastchunks, true);
        unsigned int idx = fastbin_index(size);
        fb = &fastbin (av, idx);

        /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
        mchunkptr old = *fb, old2;

        if (SINGLE_THREAD_P)
          {
    	/* Check that the top of the bin is not the record we are going to
    	   add (i.e., double free).  */
    	if (__builtin_expect (old == p, 0))
    	  malloc_printerr ("double free or corruption (fasttop)");
    	p->fd = PROTECT_PTR (&p->fd, old);
    	*fb = p;
          }
        else
          do
    	{
    	  /* Check that the top of the bin is not the record we are going to
    	     add (i.e., double free).  */
    	  if (__builtin_expect (old == p, 0))
    	    malloc_printerr ("double free or corruption (fasttop)");
    	  old2 = old;
    	  p->fd = PROTECT_PTR (&p->fd, old);
    	}
          while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
    	     != old2);

        /* Check that size of fastbin chunk at the top is the same as
           size of the chunk that we are adding.  We can dereference OLD
           only if we have the lock, otherwise it might have already been
           allocated again.  */
        if (have_lock && old != NULL
    	&& __builtin_expect (fastbin_index (chunksize (old)) != idx, 0))
          malloc_printerr ("invalid fastbin entry (free)");
      }

### _int_free finale

If the chunk wasn't allocated yet on any bin, call `_int_free_merge_chunk`

_int_free finale

    /*
        Consolidate other non-mmapped chunks as they arrive.
      */

      else if (!chunk_is_mmapped(p)) {

        /* If we're single-threaded, don't lock the arena.  */
        if (SINGLE_THREAD_P)
          have_lock = true;

        if (!have_lock)
          __libc_lock_lock (av->mutex);

        _int_free_merge_chunk (av, p, size);

        if (!have_lock)
          __libc_lock_unlock (av->mutex);
      }
      /*
        If the chunk was allocated via mmap, release via munmap().
      */

      else {
        munmap_chunk (p);
      }
    }

## _int_free_merge_chunk

This function will try to merge chunk P of SIZE bytes with its neighbours. Put the resulting chunk on the unsorted bin list.

Some checks are performed:

- If the chunk is the top chunk: `double free or corruption (top)`
- If the next chunk is outside of the boundaries of the arena: `double free or corruption (out)`
- If the chunk is not marked as used (in the `prev_inuse` from the following chunk): `double free or corruption (!prev)`
- If the next chunk has a too little size or too big: `free(): invalid next size (normal)`
- if the previous chunk is not in use, it will try to consolidate. But, if the prev_size differs from the size indicated in the previous chunk: `corrupted size vs. prev_size while consolidating`

_int_free_merge_chunk code

    // From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L4660C1-L4702C2

    /* Try to merge chunk P of SIZE bytes with its neighbors.  Put the
       resulting chunk on the appropriate bin list.  P must not be on a
       bin list yet, and it can be in use.  */
    static void
    _int_free_merge_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T size)
    {
      mchunkptr nextchunk = chunk_at_offset(p, size);

      /* Lightweight tests: check whether the block is already the
         top block.  */
      if (__glibc_unlikely (p == av->top))
        malloc_printerr ("double free or corruption (top)");
      /* Or whether the next chunk is beyond the boundaries of the arena.  */
      if (__builtin_expect (contiguous (av)
    			&& (char *) nextchunk
    			>= ((char *) av->top + chunksize(av->top)), 0))
        malloc_printerr ("double free or corruption (out)");
      /* Or whether the block is actually not marked used.  */
      if (__glibc_unlikely (!prev_inuse(nextchunk)))
        malloc_printerr ("double free or corruption (!prev)");

      INTERNAL_SIZE_T nextsize = chunksize(nextchunk);
      if (__builtin_expect (chunksize_nomask (nextchunk) <= CHUNK_HDR_SZ, 0)
          || __builtin_expect (nextsize >= av->system_mem, 0))
        malloc_printerr ("free(): invalid next size (normal)");

      free_perturb (chunk2mem(p), size - CHUNK_HDR_SZ);

      /* Consolidate backward.  */
      if (!prev_inuse(p))
        {
          INTERNAL_SIZE_T prevsize = prev_size (p);
          size += prevsize;
          p = chunk_at_offset(p, -((long) prevsize));
          if (__glibc_unlikely (chunksize(p) != prevsize))
            malloc_printerr ("corrupted size vs. prev_size while consolidating");
          unlink_chunk (av, p);
        }

      /* Write the chunk header, maybe after merging with the following chunk.  */
      size = _int_free_create_chunk (av, p, size, nextchunk, nextsize);
      _int_free_maybe_consolidate (av, size);
    }

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.