# Tcache Keys

Starting from glibc 2.29, the tcache was hardened by the addition of a second field in the `tcache_entry` struct, the [`key`](https://elixir.bootlin.com/glibc/glibc-2.29/source/malloc/malloc.c#L2908):

```c
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()`](https://elixir.bootlin.com/glibc/glibc-2.29/source/malloc/malloc.c#L2928) function, we can see what `key` is set to:

```c
/* 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()`](https://elixir.bootlin.com/glibc/glibc-2.29/source/malloc/malloc.c#L4189):

```c
#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:

```c
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:

```c
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.

{% hint style="danger" %}
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.
{% endhint %}

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()`](https://elixir.bootlin.com/glibc/glibc-2.39/source/malloc/malloc.c#L3174), 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`](https://elixir.bootlin.com/glibc/glibc-2.34/source/malloc/malloc.c#L3017). Instead of `tcache_put()` setting `key` to the location of the `tcache_perthread_struct`, it sets it to [a new variable called `tcache_key`](https://elixir.bootlin.com/glibc/glibc-2.34/source/malloc/malloc.c#L3068):

```c
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]);
}
```

{% hint style="info" %}
Note the [Safe-Linking `PROTECT_PTR`](https://ir0nstone.gitbook.io/notes/binexp/heap/safe-linking) as well!
{% endhint %}

What is `tcache_key`? It's defined [here](https://elixir.bootlin.com/glibc/glibc-2.34/source/malloc/malloc.c#L3035) and set directly below, in the [`tcache_key_initialise()`](https://elixir.bootlin.com/glibc/glibc-2.34/source/malloc/malloc.c#L3047) function:

```c
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](https://elixir.bootlin.com/glibc/glibc-2.34/source/stdlib/getrandom.c#L25) and for Linux [here](https://elixir.bootlin.com/glibc/glibc-2.34/source/sysdeps/unix/sysv/linux/getrandom.c#L27); it just uses a syscall to read `n` random bytes. If that fails for some reason, it calls the [`random_bits()`](https://elixir.bootlin.com/glibc/glibc-2.34/source/include/random-bits.h#L31) 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](https://elixir.bootlin.com/glibc/glibc-2.34/source/malloc/malloc.c#L4346), 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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ir0nstone.gitbook.io/notes/binexp/heap/tcache-keys.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
