Tcache Keys

A primitive double-free protection

Starting from glibc 2.29, the tcache was hardened by the addition of a second field in the tcache_entry struct, the key:

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

It's a pointer to a tcache_perthread_struct. In the tcache_put() function, we can see what key is set to:

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache;

  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

When a chunk is freed and tcache_put() is called on it, the key field is set to the location of the tcache_perthread_struct. Why is this relevant? Let's check the tcache security checks in _int_free():

#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))
	  {
	    tcache_entry *tmp;
	    LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
	    for (tmp = tcache->entries[tc_idx];
		 tmp;
		 tmp = tmp->next)
	      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

The chunk being freed is variable e. We can see here that before tcache_put() is called on it, there is a check being done:

if (__glibc_unlikely (e->key == tcache))

The check determines whether the key field of the chunk e is set to the address of the tcache_perthread_struct already. Remember that this happens when it is put into the tcache with tcache_put()! If the pointer is already there, there is a very high chance that it's because the chunk has already been freed, in which case it's a double-free!

It's not a 100% guaranteed double-free though - as the comment above it says:

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.

There is a 1/2^<size_t> chance that the key being tcache_perthread_struct already is a coincidence. To verify, it simply iterates through the tcache bin and compares the chunks to the one being freed:

tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
    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.  */

Iterates through each entry, calls it tmp and compares it to e. If equal, it detected a double-free.

You can think of the key as an effectively random value (due to ASLR) that gets checked against, and if it's the correct value then something is suspicious.

So, what can we do against this? Well, this protection doesn't affect us that much - it stops a simple double-free, but if we have any kind of UAF primitive we can easily overwrite e->key. Even with a single byte, we still have a 255/256 chance of overwriting it to something that doesn't match key. Creating fake tcache chunks doesn't matter either, as even in the latest glibc version there is no key check in tcache_get(), meaning tcache poisoning is still doable.

In fact, the key can even be helpful for us - the fd pointer of the tcache chunk is mangled, so a UAF does not guarantee a heap leak. The key field is not mangled, so if we can leak the location of tcache_perthread_struct instead, this gives us a heap leak as it is always located at heap_base + 0x10.


In glibc 2.34, the key field was updated from a tcache_perthread_struct * to a uintptr_t. Instead of tcache_put() setting key to the location of the tcache_perthread_struct, it sets it to a new variable called tcache_key:

static __always_inline void tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

  /* Mark this chunk as "in the tcache" so the test in _int_free will
     detect a double free.  */
  e->key = tcache_key;

  e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

Note the Safe-Linking PROTECT_PTR as well!

What is tcache_key? It's defined here and set directly below, in the tcache_key_initialise() function:

static void tcache_key_initialize (void)
{
  if (__getrandom (&tcache_key, sizeof(tcache_key), GRND_NONBLOCK)
      != sizeof (tcache_key))
    {
      tcache_key = random_bits ();
#if __WORDSIZE == 64
      tcache_key = (tcache_key << 32) | random_bits ();
#endif
    }
}

It attempts to call __getrandom(), which is defined as a stub here and for Linux here; it just uses a syscall to read n random bytes. If that fails for some reason, it calls the random_bits() function instead, which generates a pseudo-random number seeded by the time. Long story short: tcache_key is random. The check in _int_free() still exists, and the operation is the same, just it's completely random rather than based on ASLR. As the comment above it says

The value of tcache_key does not really have to be a cryptographically secure random number. It only needs to be arbitrary enough so that it does not collide with values present in applications. [...]

This isn't a huge change - it's still only straight double-frees that are affected. We can no longer leak the heap via the key, however.

Last updated