Aller au contenu principal
own2pwn
heap/pt2-tcache.tsx

tcache : pourquoi free() est devenu si rapide

Le tcache (thread local cache) est arrivé avec glibc 2.26 : structures tcache_entry et tcache_perthread_struct, indexation par taille, et pourquoi free l'atteint avant les fastbins.

Maxime Jérôme··4 min de lecture

Prérequis

Hello ! o/

Le tcache (thread local cache, ou Thread Cache) a été implémenté à partir de la glibc 2.26, soit fin août 2017. Il a permis de gagner d'importantes performances.

Chaque thread contient une variable locale threadée qui se souvient de quelle arena il a utilisé en dernier. Si cette arena est en cours d'utilisation quand notre thread veut l'utiliser, le thread se bloque et attend que le mutex soit libéré.

Par conséquent, on introduit 2 nouvelles structures de données :

c
typedef struct tcache_entry
{
  struct tcache_entry *next;
  struct tcache_perthread_struct *key; // double free detection
} tcache_entry;

typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread tcache_perthread_struct *tcache = NULL;

Chaque thread contient un pointeur tcache vers une tcache_perthread_struct, qui stocke un petit nombre de chunks libérés accessibles sans avoir à locker une arena. tcache_entry pointe les chunks dans une liste chaînée, comme pour les fastbins, mais pointe sur le user data. Le tableau entries de tcache_perthread_struct est indexé par taille de chunk :

ArchitectureTaille min (bytes)Taille max (bytes)Pas (bytes)
x86 (32 bits)125168
x86-64 (64 bits)24103216
tcache-layout.txt
  Thread A
  ┌─────────────────────────────────────────────────────────┐
  │  tcache (tcache_perthread_struct*)                       │
  │                                                         │
  │  counts[0..63]   entries[0..63]                         │
  │  ┌────────────┐  ┌────────────────────────────────────┐ │
  │  │  count[0]  │  │  entries[0] → tcache_entry → NULL  │ │
  │  │  count[1]  │  │  entries[1] → tcache_entry → NULL  │ │
  │  │  ...       │  │  ...                               │ │
  │  │  count[63] │  │  entries[63]→ NULL                 │ │
  │  └────────────┘  └────────────────────────────────────┘ │
  └─────────────────────────────────────────────────────────┘

  tcache_entry (pointe sur le user data, pas le header)
  ┌──────────────────────────────────────────────┐
  │  next  →  tcache_entry suivant (ou NULL)     │
  │  key   →  tcache_perthread_struct du thread  │
  └──────────────────────────────────────────────┘
Chaque thread possède sa propre tcache_perthread_struct ; chaque bin est une liste chaînée de tcache_entry.

Source : MallocInternals - glibc wiki

Priorité sur les fastbins

Du coup, avant de tomber dans un fastbin, free() peut faire tomber le chunk dans le tcache ! L'accès au tcache sera donc plus rapide qu'aux fastbins, ce qui en fait une structure très intéressante d'un point de vue performance.

Ordre de priorité de free()
Quand un chunk est libéré, glibc vérifie d'abord si sa taille entre dans un bin tcache disponible. Si oui, le chunk y atterrit immédiatement, sans locker d'arena. Les fastbins, unsorted bin et consorts ne sont atteints qu'ensuite.
Pour aller plus loin
Si tu veux creuser le tcache dans un contexte encore plus bas-niveau, l'article de Google sur TCMalloc est une excellente lecture : TCMalloc Design.

Merci pour votre lecture :)