Soixante-dix pour cent des CVE sont des bugs de memory safety. Des buffer overflows, des use-after-free, des double frees. Le genre de vulnérabilités qui permettent à un attaquant de passer d’un JPEG malformé à un accès root.
Nous savons comment en arrêter la plupart en hardware depuis 1975. L’ordinateur Cambridge CAP utilisait l’adressage basé sur les capabilities. Celui de Carnegie Mellon, Hydra, aussi. L’idée était simple : chaque accès mémoire transporte son propre laissez-passer. Le hardware l’impose. Pas de laissez-passer, pas d’accès.
Et puis le hardware de capabilities a passé les quatre décennies suivantes à perdre face aux modèles de mémoire plate. Ce n’est que maintenant, avec la carte Morello d’ARM et le jeu d’instructions CHERI, que les architectures de capabilities amorcent un retour crédible.
La question n’est pas de savoir si les capabilities fonctionnent. C’est le cas. La question est de savoir pourquoi il a fallu quarante ans à l’industrie pour s’y intéresser.
Un capability pointer est un ticket infalsifiable, pas seulement une adresse
Un pointeur normal est un entier. Une adresse. Il n’a ni longueur, ni permissions, ni provenance. Si vous pouvez deviner une adresse, vous pouvez y accéder. Ce n’est pas une fonctionnalité. C’est un bug de conception que C a hérité du PDP-11.
Un capability pointer, en revanche, est un tuple imposé par le hardware. Il contient l’adresse virtuelle, la base et la longueur de la région autorisée, les permissions lecture/écriture/exécution, et une balise de validité d’1 bit. La balise est stockée en dehors de la mémoire normale, dans des métadonnées hardware dédiées, ce qui rend les capabilities infalsifiables. Vous ne pouvez pas construire un capability valide à partir d’un entier. Vous ne pouvez le recevoir que de quelque chose qui possédait déjà l’autorité.
Sur CHERI, cela ressemble à un pointeur de 128 bits sur une machine 64 bits. Les 64 bits supplémentaires portent les bounds et les permissions. Le bit de balise réside dans une table d’ombre ou dans des bits ECC de rechange, selon l’implémentation.
Voici à quoi ressemble la restriction d’un capability en pratique sur un système CHERI :
#include <cheriintrin.h>
#include <stdio.h>
int main(void) {
char buffer[64];
// In the purecap ABI, &buffer[0] is already a capability
// carrying 64-byte bounds and read/write permissions.
char *cap = buffer;
printf("Original length: %zu\n", cheri_length_get(cap));
// Narrow authority to the first 16 bytes.
char *narrow = cheri_bounds_set(cap, 16);
printf("Narrowed length: %zu\n", cheri_length_get(narrow));
// This access is safe and hardware-authorized.
narrow[15] = 'x';
// This would trap with a CHERI bounds violation:
// narrow[20] = 'y';
return 0;
}
Si un attaquant corrompt un capability pointer, le bit de balise s’efface. Le hardware lève une exception lors de la prochaine déréférenciation. La chaîne d’exploitation meurt au premier saut.
C’est un modèle de sécurité radicalement meilleur que l’ASLR et les stack canaries, qui ne sont que des ralentisseurs.
Le iAPX 432 a empoisonné le puits pour une génération
Alors pourquoi avons-nous passé quarante ans à prétendre que les pointeurs n’étaient que des entiers ?
Intel a commercialisé le iAPX 432 en 1981. Il disposait de capabilities hardware, de segments de mémoire orientés objet et même d’un garbage collection assisté par hardware. Il était aussi environ cinq à dix fois plus lent que le 8086, énormément complexe et incompatible avec chaque compilateur existant.
IBM a eu plus de chance avec le System/38, qui utilisait des capabilities pour son single-level store. Cela a fonctionné. C’était stable. C’était aussi un système propriétaire de gamme moyenne sans chemin vers l’écosystème ouvert émergent. Lorsque la révolution UNIX est arrivée, elle a apporté des espaces d’adressage plats, des pointeurs C et l’hypothèse qu’un pointeur tient dans un registre. Cette hypothèse est devenue l’ABI de toute l’industrie.
Dans les années 1990, le hardware de capabilities était une curiosité de recherche. L’impératif commercial était la rétrocompatibilité et la performance brute. La memory safety était une préoccupation au niveau du langage, si c’en était une. C et C++ l’ont tout simplement déclarée comme le problème du programmeur.
Cela a bien fonctionné jusqu’à ce qu’Internet rende chaque buffer overflow exploitable à distance.
Le mode hybride de CHERI brise l’impasse de compatibilité
Le hardware de capabilities moderne, spécifiquement CHERI, a tiré la leçon du iAPX 432. Il n’impose pas une migration en tout ou rien.
CHERI prend en charge un ABI hybride où les pointeurs d’entiers legacy et les capability pointers coexistent dans le même processus. Vous pouvez compiler uniquement votre parser réseau ou votre bibliothèque sandboxed avec des capabilities, tandis que le reste de votre application utilise des pointeurs normaux. Le système d’exploitation (CheriBSD, ou le port Linux CHERI) gère la transition.
Cela compte parce que la vraie barrière n’a jamais été la surface de silicium. C’était l’inertie logicielle. Il existe des milliards de lignes de C et C++ qui supposent que sizeof(void *) == 8. Une architecture de capabilities qui exige de tout réécrire est morte-née. Celle qui peut être adoptée progressivement a une chance.
Le mécanisme hardware est élégant. Les loads et stores aware de capabilities utilisent des instructions dédiées qui vérifient la balise et les bounds en parallèle avec la traduction d’adresses. Les pointeurs d’entiers contournent entièrement les vérifications. Vous ne payez pour la sécurité que là où vous l’utilisez.
Le vrai coût est la pression sur le TLB et le gonflement des pointeurs, pas le nombre de cycles
Les capabilities ne sont pas gratuites. L’overhead se répartit en trois catégories.
Premièrement, la taille des pointeurs. Dans un ABI purecap, chaque pointeur fait 128 bits. Cela double la pression sur le cache pour les structures de données pointer-heavy. Les listes chaînées, les arbres et les vtables deviennent tous plus gros.
Deuxièmement, la granularité des bounds. CHERI impose des bounds à la granularité de l’octet, ce qui signifie que le sous-système mémoire doit vérifier les bounds à chaque déréférenciation. La vérification elle-même est rapide, mais des capabilities à grain fin peuvent augmenter la pression sur le TLB et le cache si vous créez de nombreuses petites régions protégées.
Troisièmement, et surtout, les changements logiciels. Les compilateurs doivent générer des prologues aware de capabilities. Les ABIs changent. Les allocateurs mémoire doivent renvoyer des capabilities bornées plutôt que des pointeurs raw. Les debuggers doivent comprendre les registres de 128 bits.
L’overhead mesuré en runtime sur le hardware CHERI se situe typiquement dans la fourchette d’un seul chiffre en pourcentage pour la plupart des charges de travail, et jusqu’à dix à quinze pour cent pour les benchmarks pointer-heavy. C’est moins que l’overhead de nombreuses mitigations modernes comme le memory tagging ou le sandboxing, et cela fournit des garanties plus fortes.
Le trade-off n’est pas la performance. Le trade-off est le bouleversement de l’écosystème.
Vous pouvez exécuter cela aujourd’hui sur une carte de développement
Si vous voulez essayer le hardware de capabilities, vous n’avez besoin ni d’une machine à remonter le temps ni d’une bourse de recherche.
La carte Morello d’ARM implémente les extensions CHERI sur un cœur Neoverse N1. CheriBSD fonctionne dessus out of the box. Si vous n’avez pas le hardware, QEMU-CHERI émule l’architecture de capabilities complète.
La toolchain est LLVM standard. Vous compilez avec cheri-clang et le flag -march=morello+cheri. Des ports FreeBSD et Linux existent. L’assembleur, le linker et le debugger intégrés de LLVM comprennent tous les capabilities.
Commencez par une seule fonction. Enveloppez un parser ou une routine de désérialisation dans un capability réduit. Lancez votre fuzzer contre elle. Lorsqu’une écriture hors limites aurait corrompu le heap, vous obtiendrez à la place une exception hardware propre.
Le hardware de capabilities n’est pas un correctif théorique pour la memory safety. C’est une technologie fonctionnelle et déployée que l’industrie a ignorée pendant quarante ans parce que les incitations étaient mauvaises. La mémoire plate était plus rapide à construire, plus facile à porter et suffisamment bonne jusqu’à ce que l’exploitation à distance devienne le threat model par défaut.
Le silicium fonctionne. Les compilateurs fonctionnent. Les systèmes d’exploitation fonctionnent. La seule chose qui reste est de décider quelles parties de votre codebase méritent d’être protégées en premier.