Les buffer overflows figurent dans le CWE Top 25 depuis vingt ans. Nous avons les stack canaries, l’ASLR, le DEP, la control-flow integrity et les langages memory-safe, et pourtant ils continuent d’apparaître dans du code critique. La raison est simple : chacune de ces mitigations s’exécute dans le logiciel, et le logiciel peut être contourné, mal configuré ou tout simplement non utilisé.
Et si le matériel lui-même refusait de vous laisser lire au-delà de la fin d’un tableau ?
Pourquoi les mitigations logicielles continuent de perdre
Un buffer overflow se produit quand un programme écrit au-delà des limites d’une région de mémoire allouée. En C, c’est trivial car un pointer n’est qu’une adresse. Le compilateur et le CPU font confiance au fait que le programmeur sait ce qu’il fait. Si vous allouez 64 octets et écrivez à l’offset 80, le CPU le fera. Il n’a aucune idée de où le buffer se termine.
Les mitigations logicielles essaient de détecter cela après coup. Les stack canaries placent une valeur connue avant la return address et la vérifient avant de revenir. L’ASLR randomise la disposition de la mémoire pour rendre les exploits plus difficiles à construire. La control-flow integrity restreint où les sauts indirects peuvent atterrir. Cela aide, mais c’est probabiliste ou incomplet. Un attaquant déterminé avec assez de temps et un infoleak peut généralement les contourner.
Les langages memory-safe comme Rust résolvent le problème au niveau du langage, mais ils n’aident pas les milliards de lignes de C et C++ déjà en production. Réécrire le noyau Linux, OpenSSL ou votre backend legacy en Rust n’arrivera pas cette décennie. Nous avons besoin d’une défense qui protège le code existant sans exiger de réécriture.
Ce que CHERI fait réellement à un pointer
CHERI, qui signifie Capability Hardware Enhanced RISC Instructions, est une extension d’ISA développée à l’University of Cambridge et maintenant supportée par Arm sous la forme de Morello. Elle change ce qu’est un pointer.
Dans un système 64 bits conventionnel, un pointer fait 64 bits : juste une adresse. Dans un système CHERI, un pointer devient une capability de 128 ou 256 bits. Les bits supplémentaires stockent des métadonnées : la base address de l’allocation, les bounds (jusqu’où elle s’étend) et les permissions (read, write, execute). La capability est protégée par un hardware integrity tag, de sorte que toute manipulation des metadata l’invalide.
Quand vous compilez du code pour CHERI, malloc ne retourne pas seulement une adresse. Il retourne une capability dont les bounds sont exactement la taille demandée. Quand vous incrémentez ce pointer, le matériel vérifie chaque accès contre ces bounds. Si vous essayez de lire ou d’écrire en dehors, le CPU lève une synchronous exception. Il est impossible de forger une capability avec des bounds plus larges. Le matériel ne vous le permet tout simplement pas.
C’est la différence clé. Le software bounds checking insère des checks autour des opérations mémoire, que les compilateurs peuvent optimiser, que les programmeurs peuvent oublier et que les attaquants peuvent contourner. Le hardware bounds checking se produit à chaque load et store, inconditionnellement, avec zéro instruction ajoutée à votre programme.
Comment l’enforcement des bounds fonctionne au niveau de l’instruction
Voici à quoi ressemble un buffer overflow typique en C :
#include <string.h>
void vulnerable(char *input) {
char buf[64];
strcpy(buf, input); // No bounds check. Classic overflow.
}
Sur une architecture normale, cela corrompt le stack. Sur CHERI, buf n’est pas une raw address. C’est une capability dont les bounds sont exactement 64 octets. Quand strcpy essaie d’écrire au-delà de l’octet 64, le matériel lève une exception de Capability Bounds Violation. Le programme crash immédiatement à l’instruction exacte qui offense.
La même protection s’applique aux allocations heap :
#include <stdlib.h>
#include <cheri.h> // CHERI intrinsics header
void heap_example(void) {
char *buf = malloc(32);
// buf is a capability with base=buf, length=32
// This works fine
buf[0] = 'A';
buf[31] = 'Z';
// This traps at the hardware level
buf[32] = 'X'; // Capability bounds violation
}
Le crash est précis. Vous obtenez l’instruction exacte, la capability exacte et les bounds exacts qui ont été violés. Déboguer cela est plus facile que de traquer un stack frame corrompu trois frames plus tard.
CHERI empêche aussi la pointer forgery. Vous ne pouvez pas caster un integer en pointer et commencer à déréférencer de la mémoire arbitraire. Le matériel ne reconnaît que les capabilities créées par des instructions légitimes comme malloc, stack allocation ou dérivation explicite de capability. Les casts integer-to-pointer produisent des valeurs untagged qui trappent à l’utilisation.
Le problème de compatibilité est réel
Si CHERI est si bon, pourquoi chaque serveur ne tourne-t-il pas dessus ? Parce que changer ce qu’est un pointer brise des suppositions sur lesquelles des décennies de code C reposent.
Le premier problème est la taille. Les capabilities sont plus grandes que les raw pointers. Sur Morello, une capability fait 128 bits plus un tag de 1 bit stocké séparément par le matériel. Cela augmente l’utilisation mémoire pour les structures de données lourdes en pointers. Une linked list ou un arbre plein de pointers devient sensiblement plus grand. Pour de nombreuses applications, l’overhead est de quelques pourcents, mais pour les workloads de pointer-chasing, cela peut être pire.
Le deuxième problème est le casting. Le code C cast des pointers en uintptr_t, fait de l’arithmétique et recaste. Sur CHERI, uintptr_t est en réalité un type capability, pas un integer. Le code qui suppose pouvoir faire des maths entières arbitraires sur des valeurs de pointer échouera à la compilation ou trappera au runtime. La solution est généralement d’utiliser ptrdiff_t pour les offsets et de les appliquer avec cheri_address_set, mais cela nécessite de changer le source.
Le troisième problème est l’écosystème. Le matériel CHERI est rare. Les boards Arm Morello existent mais ne sont pas des commodity servers. CheriBSD et Linux activé pour CHERI sont assez matures pour faire tourner du logiciel réel, mais la plupart des distributions Linux ne livrent pas de packages CHERI. Votre container registry, vos CI runners et votre cloud provider ne le supportent pas encore.
Vous pouvez l’essayer aujourd’hui sans silicium sur mesure
Vous n’avez pas besoin d’une board Morello pour expérimenter avec CHERI. Le projet CHERI maintient le support QEMU, vous pouvez donc faire tourner un userspace CHERI sur n’importe quel hôte Linux ou macOS.
Voici comment obtenir une toolchain CHERI et exécuter un exemple simple dans QEMU :
# Clone the CHERI SDK setup
$ git clone https://github.com/CTSRD-CHERI/cheribuild.git
$ cd cheribuild
# Build the CheriBSD disk image and QEMU
$ ./cheribuild.py cheribsd-riscv64-purecap -d
$ ./cheribuild.py qemu -d
# Boot CheriBSD in QEMU
$ ./cheribuild.py run-cheribsd-riscv64
Une fois dans CheriBSD, le compilateur est clang avec le support de target CHERI. Vous compilez avec le bounds checking enforce par défaut :
$ clang -o test test.c
$ ./test
# Buffer overflows trap immediately with a clear message
Si vous voulez tester du logiciel existant, cheribuild peut compiler des paquets populaires de FreeBSD ports avec le support CHERI. L’équipe CHERI a porté OpenSSH, nginx, PostgreSQL et de grandes parties du système de base de FreeBSD. Beaucoup de programmes fonctionnent sans changement. Ceux qui cassent le font généralement à cause de casts de pointer non sûrs ou de suppositions sur la taille des pointers.
Pour un démarrage plus rapide sans QEMU, l’University of Cambridge publie aussi une image Docker avec la toolchain LLVM de CHERI préinstallée. Vous pouvez compiler des binaires CHERI et inspecter l’assembly capability-aware généré sans booter un OS complet.
Ce que la hardware memory safety signifie pour les codebases existantes
CHERI ne remplace pas l’écriture de code sûr. C’est un filet de sécurité pour le code que vous avez déjà. Un système CHERI exécutant du C legacy aura toujours des logic bugs, des race conditions et des erreurs de use-after-free. Mais il n’aura pas de buffer overflows qui écrasent des return addresses, corrompent des métadonnées heap adjacentes ou filtrent des secrets au-delà des limites d’allocation.
La progression est déjà visible. Arm a intégré des features dérivées de CHERI dans sa Memory Tagging Extension (MTE), qui est expédiée aujourd’hui dans des appareils Android de production. MTE est plus grossier que CHERI (il tagge des granules de 16 octets plutôt que des allocations individuelles), mais il attrape beaucoup des mêmes bugs avec moins d’overhead. Le CHERI complet suivra probablement à mesure que l’écosystème mûrit.
Si vous maintenez du code en C ou C++ qui traite de l’input non fiable, la question n’est pas de savoir si la hardware memory safety arrivera. C’est de savoir si vous serez prêt quand ce sera le cas. Commencez par auditer votre code pour les casts pointer-to-integer, l’arithmétique de pointer sur des objets non liés et les suppositions sur sizeof(void*). Ce sont les patterns qui cassent sous CHERI, et les réparer rend votre code plus propre même sur du matériel conventionnel.
Le matériel est enfin prêt à dire non. Nous devrions le laisser faire.