malloc source code analysis

because others suck at explaining i will just read that shit myself .
this has direct relation to THE HEAP article , only this deals with an older version of glibc , namely it is glibc 2.24 , no safe unlink and no tcache.
after i skipped some code that handles locks and arenas in multithreading , now I'm at real code i wanna see:

	_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */
  mchunkptr nextchunk;         /* next contiguous chunk */
  INTERNAL_SIZE_T nextsize;    /* its size */
  int nextinuse;               /* true if nextchunk is used */
  INTERNAL_SIZE_T prevsize;    /* size of previous contiguous chunk */
  mchunkptr bck;               /* misc temp for linking */
  mchunkptr fwd;               /* misc temp for linking */

  const char *errstr = NULL;
  int locked = 0;

  size = chunksize (p);

[...]
    nextchunk = chunk_at_offset(p, size);

    /* Lightweight tests: check whether the block is already the
       top block.  */
    if (__glibc_unlikely (p == av->top))
      {
	errstr = "double free or corruption (top)";
	goto errout;
      }
    /* 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))
      {
	errstr = "double free or corruption (out)";
	goto errout;
      }
    /* Or whether the block is actually not marked used.  */
    if (__glibc_unlikely (!prev_inuse(nextchunk)))
      {
	errstr = "double free or corruption (!prev)";
	goto errout;
      }
  • here where we see the funtion that actually handles the free operation . the part that interests us is where next chunk is defined
  • after that checks are made to make sure if it is the top block because in that case there would be no next block , then we see if the next chunk is beyond the boundaries of the arena bcs in that case it doesnt belong to this thread
  • then we see if the next block is used or not , this uses the flag bits at the lowest bytes of the next chunks header
    next we have :
    nextsize = chunksize(nextchunk);
    if (__builtin_expect (nextchunk->size <= 2 * SIZE_SZ, 0)
	|| __builtin_expect (nextsize >= av->system_mem, 0))
      {
	errstr = "free(): invalid next size (normal)";
	goto errout;
      }

    free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

    /* consolidate backward */
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink(av, p, bck, fwd);
    }

  • so after we "made sure that the next block is valid" , we go on to get its size and check it , i have no idea how that check is done and i do not care
  • then we check if the previous contiguous block is in use , if it is not , we merge our block with it by getting its size and adding it to our chunk's size ,after that we set our chunk the previous chunks by going at a negative offset of its size from our chunk , but we still need to fix the linked list (bin) so then we unlink ? lets see how is that done :
define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;								      \
	BK = P->bk;								      \
	FD->bk = BK;							      \
    BK->fd = FD;							      \
  • what this is doing is setting the forward pointer of the previous block and setting it to the next block and setting the backward pointer of the next block to to our previous block , and thus our block logically vanishes ! , this will be useful in exploitation later .
  • we continue now our analysis:
    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
      if (!nextinuse) {
	unlink(av, nextchunk, bck, fwd);
	size += nextsize;
      } else
	clear_inuse_bit_at_offset(nextchunk, 0);
  • we check if if the next block is at the top and if it is not we check if it is in use or not , if it is not we merge it by unlinking it not our chunk as we did previously (again , very exploit-ty) , and we add its size to our chunk's size and this clears it .

some notes

  • malloc alignes blocks to a 8 bytes alignement , it also adds 4 bytes to the size for the header and then aligns the size to the said 8 bytes , so it you malloc 0 it's going to malloc 4 and align it to 8 and so on.
  • aside from that , block that are malloced consecutively are usually contiguous