Vous disposez d’une codebase C trop volumineuse pour être rewrite en Rust et trop critique pour rester exposée aux dépassements de tampon. Le matériel à capacités CHERI promet d’intercepter les violations de sécurité mémoire au niveau du processeur, mais Internet ne cesse de vous répéter que les pointeurs CHERI font 128 bits et que votre code suppose qu’ils en font 64.
La bonne nouvelle est que CHERI n’impose pas une migration en tout ou rien. L’ABI hybride vous permet de compiler du code C existant avec des modifications minimales, de l’exécuter sur du vrai matériel CHERI, et de resserrer la sécurité de manière incrémentale là où elle compte le plus. Vous n’avez pas besoin de porter un million de lignes de code avant d’obtenir votre premier pointeur protégé.
Pourquoi le portage incrémental est possible aujourd’hui
Les premières architectures à capacités étaient intransigeantes. L’iAPX 432 exigeait que chaque pointeur soit une capacité, point final. Cela a cassé chaque compilateur et système d’exploitation existant, et le projet a péri.
CHERI a tiré les leçons de cet échec. Il prend en charge trois modes de compilation : baseline (pointeurs entiers hérités uniquement), hybride (pointeurs entiers et capacités coexistent), et purecap (chaque pointeur est une capacité). L’ABI hybride est le pont. Elle vous permet de conserver votre code existant quasi intact tout en introduisant sélectivement des capacités là où elles apportent le plus de valeur.
En mode hybride, void* fait toujours 64 bits. Un pointeur capacité est un type distinct, void* __capability, que vous adoptez explicitement. Vos structures, vos listes chaînées et vos tables de hachage ne changent pas de taille à moins que vous le demandiez. Ce n’est pas une couche de compatibilité. C’est une ABI de premier classe prise en charge par le matériel et le compilateur.
Ce qui casse réellement lorsque vous compilez pour CHERI hybride
La première étape consiste à essayer de compiler votre code avec une chaîne d’outils CHERI et à voir ce qui explose. La plupart du code C bien conçu se compile sans modification. Les problèmes se concentrent autour d’une poignée de motifs que CHERI rend illégaux, ce qui est exactement le but recherché.
Conversions de pointeur vers entier. Le code qui convertit un pointeur en uintptr_t, masque certains bits, puis reconvertit échouera. Sur CHERI, uintptr_t est un type capacité, pas un simple entier. Les opérations bit à bit sur des capacités suppriment le bit de balise, et la valeur résultante déclenche une trappedereference.
Hypothèses sur la taille des pointeurs. Le code qui code en dur sizeof(void*) == 8 ou sérialise des pointeurs sur 8 octets cassera en mode purecap. En mode hybride cela fonctionne généralement, mais cela devient un problème dès que vous mélangez des pointeurs capacité et entiers dans la même structure.
Arithmétique de pointeurs entre objets non liés. Les programmeurs C calculent parfois la distance entre deux pointeurs arbitraires ou comparent des pointeurs provenant d’allocations différentes. Les capacités CHERI portent des métadonnées de bornes, et soustraire des capacités d’allocations différentes est indéfini. Le compilateur le rejettera ou le matériel déclenchera une trap.
Assembleur en ligne. Tout assembleur écrit à la main qui déplace des pointeurs dans des registres entiers devra être mis à jour. CHERI dispose de registres capacité dédiés, et le compilateur doit savoir lesquels vous touchez.
La chaîne d’outils LLVM CHERI vous fournit des diagnostics excellents. Vous n’obtenez pas d’erreurs de lien cryptiques. Vous obtenez des messages clairs comme « cast from capability to integer is not allowed » ou « arithmetic on capabilities from different allocations ». Corrigez la première erreur, recompilez, et poursuivez la suivante.
Un exemple concret : envelopper un analyseur avec des capacités
Imaginez que vous avez un analyseur de paquets réseau qui lit une entrée non fiable dans un tampon, puis analyse les en-têtes. C’est exactement le genre de code qui bénéficie de la vérification des bornes matérielle. Voici comment l’envelopper sans faire de rewrite de l’analyseur lui-même.
D’abord, compilez l’analyseur en mode hybride. La chaîne d’outils CHERI est du LLVM standard avec une cible CHERI :
# Compiler un fichier unique avec l'ABI hybride
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-mno-relax \
-c parser.c -o parser.o
Le flag -mabi=lp64d maintient les pointeurs entiers sur 64 bits. Vos types void* et char* existants restent inchangés. L’analyseur se compile tel quel.
Ajoutez maintenant une enveloppe consciente des capacités dans un fichier séparé :
#include <cheriintrin.h>
#include <stddef.h>
#include <stdint.h>
// Analyseur existant de parser.c
extern int parse_packet(const char *data, size_t len);
// Point d'entrée conscient des capacités
int parse_packet_safe(const char * __capability data, size_t len) {
// Réduire la capacité exactement à la longueur du tampon.
// Même si l'appelant a passé une allocation plus grande,
// l'analyseur ne peut pas lire au-delà de 'len' octets.
const char * __capability narrowed =
cheri_bounds_set(data, len);
// En mode hybride, nous passons un pointeur simple à l'analyseur hérité.
// Le compilateur insère une conversion capacité-vers-entier ici.
// Si la capacité ne tient pas sur 64 bits (ce qui sera le cas pour
// les adresses à grande capacité), cela générera un avertissement ou une erreur.
//
// Pour une migration vers purecap, vous modifierez plutôt parse_packet
// pour accepter un pointeur capacité.
return parse_packet((const char *)narrowed, len);
}
Dans cet exemple, parse_packet utilise toujours des pointeurs entiers hérités. L’enveloppe réduit la capacité de l’appelant à la longueur exacte de l’entrée, puis la reconvertit. L’étape de réduction signifie que même si l’appelant passe accidentellement un tampon de 4 Ko alors qu’il voulait en passer 64, le matériel appliquera la borne de 64 octets sur la capacité réduite.
Ce n’est pas l’état final. C’est une étape intermédiaire. Vous obtenez le bounds enforcement à la frontière de l’API dès aujourd’hui, et vous pourrez migrer parse_packet vers purecap lors d’un refactor ultérieur.
Le chemin de migration de l’hybride vers le purecap
Le mode hybride est un point de départ, pas une destination. Il vous offre la compatibilité mais pas le bénéfice de sécurité complet des capacités. L’objectif à long terme est le purecap, où chaque pointeur porte des bornes.
La migration pratique se déroule ainsi :
-
Compilez en mode hybride et corrigez les erreurs de compilation. Cela signifie généralement corriger les conversions pointeur-vers-entier et les hypothèses
sizeof(void*). Ne modifiez pas encore vos structures de données. Contentez-vous d’obtenir une compilation propre. -
Identifiez les cibles à haute valeur. Les analyseurs réseau, les décodeurs de formats de fichier et les routines de désérialisation sont les meilleurs candidats pour le capability enforcement. Ils traitent des entrées non fiables et c’est là que vivent la plupart des bogues de sécurité mémoire.
-
Enveloppez ces cibles avec des capacités réduites. Utilisez
cheri_bounds_setpour créer des capacités restreintes aux frontières de confiance. Passez-les dans votre code existant. -
Migrez les fonctions feuilles vers le purecap. Commencez par les fonctions utilitaires qui allouent et retournent des pointeurs. Changez leurs signatures pour utiliser
__capabilityet compilez-les avec-mabi=purecap. Remontez progressivement la pile d’appels. -
Finalement basculez l’ensemble du module vers le purecap. Quand une unité de compilation ne contient plus de pointeurs entiers, compilez-la avec
-mabi=purecapet liez-la avec le reste de votre code hybride. La chaîne d’outils CHERI prend en charge le lien d’ABI mixtes.
Ce n’est pas un projet d’un week-end pour une grande codebase. Mais ce n’est pas non plus un rewrite. Vous pouvez protéger vos chemins de code les plus vulnérables en quelques jours, et migrer le reste de manière incrémentale au fur et à mesure que vous y touchez.
À quoi ressemblent réellement les compromis
L’ABI hybride a des coûts réels, et vous devriez les connaître avant de vous engager.
Premièrement, les ABI mixtes compliquent votre compilation. Vous avez désormais des fichiers objet compilés avec des tailles de pointeurs différentes dans le même binaire. L’éditeur de liens doit gérer des réadressages capacité et entiers. La chaîne d’outils CHERI prend cela en charge, mais votre système de compilation ne le sait probablement pas encore. Vous devrez l’apprendre.
Deuxièmement, la conversion capacité-vers-entier aux frontières hybrides est destructive. Si vous réduisez une capacité puis la convertissez en pointeur simple, vous perdez les bornes. La mémoire sous-jacente est toujours protégée par la capacité initiale, mais la fonction héritée ne reçoit aucun hardware enforcement. C’est pourquoi le purecap est l’objectif : chaque pointeur dans la chaîne d’appels porte ses propres bornes.
Troisièmement, le débogage change. GDB sur CHERI comprend les registres capacité et peut afficher les métadonnées de bornes. La prise en charge de LLDB s’améliore. Si votre flux de débogage actuel repose sur l’inspection de valeurs de pointeurs brutes, vous devrez apprendre les noms des registres CHERI.
Le surcoût de performance du mode hybride est généralement négligeable pour le code qui utilise principalement des pointeurs entiers. Le surcoût du purecap est typiquement de quelques pourcents pour la plupart des charges de travail, montant à 10-15% pour les structures de données riches en pointeurs. C’est compétitif avec les atténuations logicielles comme ASAN, et contrairement à ASAN, CHERI s’exécute à pleine vitesse en production.
Vous pouvez commencer dès aujourd’hui avec QEMU
Vous n’avez pas besoin d’une carte Morello pour expérimenter. Le projet CHERI maintient la prise en charge de QEMU et des images Docker avec la chaîne d’outils LLVM préinstallée.
# Récupérer l'image Docker de la chaîne d'outils CHERI
$ docker run --rm -it ctsrd/cheri-sdk:latest
# À l'intérieur du conteneur, compiler votre code pour RISC-V CHERI
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-o myapp myapp.c
Commencez par compiler votre projet en mode hybride et comptez les erreurs. Le nombre vous indiquera le volume de travail à venir. Une compilation propre du premier coup est rare mais pas impossible pour du C moderne et conforme aux standards. Quelques centaines d’erreurs est typique pour des bases de code plus anciennes avec beaucoup de conversions de pointeurs.
Corrigez les erreurs dans cet ordre : d’abord les conversions entier-vers-pointeur, puis l’arithmétique de pointeurs entre allocations, puis l’assembleur en ligne. Chaque catégorie a une correction mécanique. Le projet CHERI publie un guide de portage avec des exemples avant-après pour les motifs les plus courants.
Le matériel à capacités n’est pas un futur théorique. C’est une chaîne d’outils fonctionnelle, une ABI prise en charge, et un chemin de migration qui ne nécessite pas de réduire votre codebase en cendres. Commencez par un fichier, une fonction, une capacité réduite. Le matériel fera le reste.