01 - Pourquoi cet article
Le versant outillage de CHERI, documenté pas à pas
Notre article "Sécurité Matérielle : CHERI Capabilities en Profondeur" couvre l'architecture, le modèle de sécurité et deux PoCs, exécutés réellement sur un système purecap. Ce second article couvre l'autre moitié du travail, rarement documentée en détail ailleurs : comment on construit réellement, depuis les sources, tout ce qui est nécessaire pour faire tourner CHERI sur sa propre machine, sans matériel Morello ni carte de développement physique.
Chaque commande, chaque erreur de compilation rencontrée et chaque correctif présenté ici ont été réellement exécutés pour produire ce document, sur une machine Kali Linux ordinaire (4 cœurs, aucun matériel CHERI), sans accès à du silicium CHERI physique. Rien n'est reconstitué après coup.
"Construire son propre CHERI depuis zéro n'est pas qu'un exercice académique. C'est le seul moyen de vérifier, ligne de commande par ligne de commande, que les garanties annoncées se comportent vraiment comme documenté, et de découvrir les angles morts que la documentation officielle ne mentionne pas."
Ce guide suit l'ordre réel des opérations : la toolchain, le système cible, l'émulateur, le firmware, le démarrage, puis cinq PoCs supplémentaires qui approfondissent les mécanismes déjà présentés dans le premier article (dérivation, permissions, bornes, frontière hybride, révocation).
02 - Vue d'ensemble de cheribuild
L'outil qui orchestre tout
cheribuild est le projet officiel de build du groupe CTSRD-CHERI (Cambridge / SRI International). Écrit en Python, il orchestre des dizaines de cibles interdépendantes (toolchain, système cible, émulateur, bibliothèques, firmware) identifiées par un nom explicite, par exemple llvm-native, cheribsd-riscv64-purecap, qemu ou disk-image-riscv64-purecap. Chaque cible connaît ses propres dépendances et sait, en théorie, les construire automatiquement.
-d: construit également toutes les dépendances manquantes de la cible demandée, pas seulement la cible elle-même.--skip-update: n'essaie pas de mettre à jour (git fetch) les dépôts déjà clonés avant de builder. Utile quand le réseau est instable, ce qui a été notre cas à plusieurs reprises.--list-targets: liste toutes les cibles disponibles, plusieurs centaines une fois les variantes d'architecture comptées.- Emplacements par défaut : sources dans
~/cheri/<projet>, artefacts de build intermédiaires dans~/cheri/build/<cible>-build, résultat installé dans~/cheri/output/(toolchain dansoutput/sdk, système de fichiers cible dansoutput/rootfs-*).
03 - Construire la toolchain CHERI-Clang
LLVM/Clang recompilé pour comprendre les capabilities
La première brique est un LLVM/Clang patché par le projet CTSRD-CHERI, capable de comprendre l'extension CHERI-RISC-V (les instructions csetbounds, cincoffset, etc.) et de produire des binaires en ABI hybride ou purecap. C'est une compilation complète de LLVM depuis les sources, pas un simple téléchargement de paquet.
$ git clone https://github.com/CTSRD-CHERI/cheribuild $ cd cheribuild $ ./cheribuild.py --skip-update llvm-native # Compilation ninja réelle : 3912 unités, ~11 minutes si le cache # intermédiaire existe déjà, ~75 minutes pour une reconstruction # complète depuis zéro (voir section 06 sur ce piège précis). Built for target 'llvm-native' in 4467.46 seconds # ~74.5 min, reconstruction complète
04 - Construire CheriBSD
buildworld, buildkernel, installworld : le vrai système d'exploitation
La cible cheribsd-riscv64-purecap compile un noyau et un userland FreeBSD complets en mode purecap, avec la toolchain fraîchement construite. C'est l'étape la plus lourde : compilation de tout buildworld (bibliothèques, utilitaires), puis buildkernel (le noyau, avec la configuration CHERI-PURECAP-QEMU), puis installation des deux dans un rootfs de sortie.
$ ./cheribuild.py --skip-update -d cheribsd-riscv64-purecap # Une fois les correctifs de la section 05 appliqués : Built for target 'cheribsd-riscv64-purecap' in 4018.36 seconds # ~67 min # Résultat : noyau + userland installés dans ~/cheri/output/rootfs-riscv64-purecap/
Sur cette VM (4 cœurs, réseau parfois instable), la première tentative réussie a pris environ 67 minutes. Avant d'y arriver, plusieurs échecs de compilation bien réels ont dû être corrigés, ce qui fait l'objet de la section suivante.
05 - Pièges de compilation réels et correctifs
Ce que la documentation officielle ne dit pas toujours
Construire CheriBSD et QEMU CHERI depuis les sources, sur un système Kali à jour avec un Clang système récent (19.1.7), a déclenché plusieurs échecs de compilation réels que nous détaillons ici, avec leur correctif exact, pour faire gagner du temps à quiconque reproduit ce travail.
Le compilateur hôte trop strict pour du vieux code vendored
Plusieurs outils de bootstrap FreeBSD (getopt.c, getopt_long.c, le mandoc vendored, plusieurs fichiers de rpcgen) échouaient tous avec la même erreur : une assignation d'un const char * vers un char *, promue en erreur par un flag ajouté explicitement pour ces cibles hôte, indépendamment du flag général NO_WERROR.
lib/libc/stdlib/getopt_long.c:494:15: error: assigning to 'char *' from 'const char *' discards qualifiers [-Werror,-Wincompatible-pointer-types-discards-qualifiers] (oli = strchr(options, optchar)) == NULL) { # Trouvé dans getopt.c, getopt_long.c, contrib/mandoc/mdoc.c, # usr.bin/rpcgen/rpc_main.c et rpc_svcout.c -- 6 occurrences distinctes. # Source du flag, unique dans tout l'arbre : $ grep -rl 'incompatible-pointer-types-discards-qualifiers' tools/ share/mk/ tools/build/mk/Makefile.boot
Patcher chaque site d'appel un par un (cast explicite en (char *)) fonctionne mais ne passe pas à l'échelle : plus on avance dans le build, plus de nouveaux fichiers vendored déclenchent la même erreur. Le correctif efficace est centralisé, dans tools/build/mk/Makefile.boot, le seul endroit de tout l'arbre source où ce flag est ajouté pour les outils de bootstrap :
# tools/build/mk/Makefile.boot, juste apres la ligne qui pose problème : CWARNFLAGS.clang+=-Werror=incompatible-pointer-types-discards-qualifiers +CWARNFLAGS.clang+=-Wno-error=incompatible-pointer-types-discards-qualifiers # Clang applique les drapeaux -W de gauche à droite : le dernier # l'emporte sur le même diagnostic. Une seule ligne, tout l'arbre # bootstrap-tools recompile sans erreur.
Ce même flag, ajouté séparément par cheribuild lui-même pour la compilation de QEMU (dans pycheribuild/projects/build_qemu.py), a causé une erreur identique dans block/curl.c plus tard, avec un flag encore plus large (-Werror=incompatible-pointer-types, sans le suffixe) qui réactivait le problème même après un premier correctif. Les deux occurrences ont dû être retirées du code Python de cheribuild lui-même.
Dépendances hôte manquantes
Une checklist de paquets Kali/Debian à installer avant de commencer, découverte au fil des échecs réels :
libtool(fournitlibtoolize) : requis par la configuration de QEMU, absent par défaut.libpixman-1-dev,libglib2.0-dev,libslirp-dev,libcap-ng-dev,libattr1-dev: dépendances de build de QEMU, détectées une par une parpkg-configlors de la configuration.ninja-build: requis par la fois LLVM et QEMU (Meson).
Aucune de ces erreurs n'est un bug dans CHERI lui-même. Ce sont des frictions banales entre du code vendored ancien et un compilateur hôte plus strict que ce que les mainteneurs upstream utilisent pour leurs propres tests de non-régression. Elles disparaissent une fois identifiées, mais sans ce travail de diagnostic, un builder frais échoue silencieusement plusieurs fois avant d'aboutir.
06 - Générer l'image disque
makefs, mkimg, et une vraie crise d'espace disque
Une fois installworld/installkernel terminés, la cible disk-image-riscv64-purecap assemble le rootfs installé en une image disque bootable : une partition EFI (via makefs -t msdos), une partition racine UFS (makefs -t ffs), puis une image GPT combinant les deux plus un swap (mkimg).
$ ./cheribuild.py --skip-update disk-image-riscv64-purecap Calculated size of root.img: 7251951616 bytes, 228580 inodes Image root.img complete mkimg -s gpt -p efi:=efi.img -p freebsd-ufs:=root.img -p freebsd-swap/swap::2G Built for target 'disk-image-riscv64-purecap' in 56.47 seconds $ qemu-img info cheribsd-riscv64-purecap.img virtual size: 8.76 GiB (9401566720 bytes) disk size: 3.92 GiB # fichier sparse, espace réel occupé
Un vrai piège rencontré ici : l'espace disque de la VM est tombé à 98 Mo libres (100% utilisé) juste avant la génération de l'image, faisant échouer mkimg avec No space left on device. Notre première tentative de libérer de l'espace a supprimé tout le dossier de build intermédiaire de CheriBSD, y compris le sous-dossier tmp/legacy/bin/ qui contient makefs lui-même, un outil de bootstrap compilé pendant buildworld, jamais copié dans output/. Résultat : relancer -d a redéclenché une reconstruction complète de CheriBSD.
Le bon réflexe : ne supprimer que les sous-arbres d'objets compilés (sys/, lib/, obj-lib64/, etc.), jamais le dossier tmp/ qui contient les outils de bootstrap déjà installés. Cette suppression sélective a libéré 18 Go sans rien casser.
07 - QEMU CHERI et son firmware
Un fork dédié, pas le QEMU système
Le qemu-system-riscv64 standard, déjà présent sur Kali, ne comprend rien à CHERI : pas de mémoire taguée, pas de registres capability, pas d'instructions CHERI, pas d'exceptions matérielles associées. Un binaire purecap y plante silencieusement ou s'exécute sans aucune des protections qu'on cherche justement à observer.
cheribuild construit donc son propre fork, CHERI-Alliance/qemu, qui ajoute un modèle de CPU complet pour l'extension : mémoire taguée, registres capability, toutes les instructions CHERI, et les exceptions matérielles (SIGPROT, etc.) documentées dans notre premier article.
$ ./cheribuild.py --skip-update run-riscv64-purecap Would you like to install the dependency (qemu) using cheribuild? y/[N] y # Cibles compilées : arm, aarch64, morello, mips64, mips64cheri128, # riscv64, riscv64xcheri, riscv64cheristd, riscv32(...), x86_64 Built for target 'qemu' in 668.44 seconds # ~11 min, après le correctif -Werror $ ~/cheri/output/sdk/bin/qemu-system-riscv64xcheri --version QEMU emulator version 7.1.0 Built with instruction logging enabled
Le firmware RISC-V (bbl)
QEMU CHERI a encore besoin d'un firmware de démarrage RISC-V compatible capabilities pour charger le noyau : bbl-baremetal-riscv64-purecap (Berkeley Boot Loader). Absent lui aussi au premier lancement, mais rapide à construire, 9,45 secondes dans notre cas, une fois la toolchain déjà prête.
08 - Démarrer CheriBSD sous QEMU
Du premier message noyau au prompt root
Lancée dans une session tmux détachée pour garder un accès interactif à la console série malgré la déconnexion SSH, la machine démarre avec de vrais messages de noyau CheriBSD.
sbi0: <RISC-V Supervisor Binary Interface> intc0: <RISC-V Local Interrupt Controller> on ofwbus0 plic0: <RISC-V PLIC> mem 0xc000000-0xc5fffff irq 10,11 on simplebus1 Trying to mount root from ufs:/dev/ufs/root []... vtnet0: link state changed to UP Starting Network: lo0 vtnet0. Starting sshd. CheriBSD/riscv (cheribsd-riscv64-purecap) (ttyu0) login: root WARNING: INVARIANTS kernel option defined, expect reduced performance WARNING: WITNESS kernel option defined, expect reduced performance WARNING: capability revocation enabled by default, this may affect performance root@cheribsd-riscv64-purecap:~ # sysctl hw.machine_arch hw.machine_arch: riscv64c # le "c" final signale le support CHERI
Note pour la section 14 : le message d'accueil signale explicitement que la révocation de capabilities est compilée dans ce noyau (CHERI_CAPREVOKE), ce qui pourrait laisser penser que le fossé de sécurité temporelle est comblé par défaut. Le PoC #5 vérifie directement si c'est le cas.
09 - Environnement de travail (SSH)
Accès SSH fiable plutôt que console série
cheribuild configure par défaut un redirecteur de port (hostfwd=tcp:127.0.0.1:9999-:22) : le port 9999 de l'hôte Kali redirige vers le port 22 de l'invité CheriBSD. Après avoir fixé un mot de passe root via la console série, on installe une clé publique pour un accès SSH fiable, bien plus pratique que d'envoyer des touches une à une dans une session série.
# Sur l'hote Kali, depuis la console serie (tmux) : root@cheribsd-riscv64-purecap:~ # mkdir -p /root/.ssh root@cheribsd-riscv64-purecap:~ # echo ssh-ed25519 AAAA... kali@kali >> /root/.ssh/authorized_keys # Depuis le shell Kali (le port 9999 est local a la VM) : $ ssh -p 9999 root@localhost 'uname -a' FreeBSD cheribsd-riscv64-purecap 15.0-CURRENT FreeBSD 15.0-CURRENT #0 main-88f39900c329-dirty riscv $ scp -P 9999 poc.c root@localhost:/root/poc/ # transfert de fichiers trivial
À partir d'ici, le flux de travail pour chaque PoC est identique : compiler sur l'hôte Kali avec le cross-compiler purecap (~/cheri/output/sdk/bin/clang -target riscv64-unknown-freebsd -march=rv64gcxcheri -mabi=l64pc128d --sysroot=~/cheri/output/rootfs-riscv64-purecap), transférer le binaire via scp, puis l'exécuter réellement dans l'invité via SSH.
10 - PoC #1 : dérivation et inspection de capability
Voir les champs d'une capability, pas seulement les deviner
Premier des cinq PoCs supplémentaires : dériver une capability plus étroite depuis une capability large, et inspecter concrètement ses champs via les intrinsics CHERI (cheriintrin.h), plutôt que de les décrire abstraitement.
#include <stdio.h> #include <stdlib.h> #include <cheriintrin.h> int main(void) { char *buf = malloc(64); void * __capability cap = (void * __capability)buf; printf("capability initiale (malloc, 64 octets) :\n"); printf(" base = 0x%lx\n", (unsigned long)cheri_base_get(cap)); printf(" length = %lu\n", (unsigned long)cheri_length_get(cap)); printf(" perms = 0x%x\n", (unsigned)cheri_perms_get(cap)); printf(" tag = %d\n", cheri_tag_get(cap)); void * __capability narrowed = cheri_bounds_set(cap, 16); printf("\napres cheri_bounds_set(cap, 16) :\n"); printf(" base = 0x%lx\n", (unsigned long)cheri_base_get(narrowed)); printf(" length = %lu\n", (unsigned long)cheri_length_get(narrowed)); printf(" tag = %d\n", cheri_tag_get(narrowed)); free(buf); return 0; }
root@cheribsd-riscv64-purecap:~ # ./derive_inspect capability initiale (malloc, 64 octets) : base = 0x40a0f000 length = 64 perms = 0x6017d tag = 1 apres cheri_bounds_set(cap, 16) : base = 0x40a0f000 # adresse inchangee length = 16 # bornes reellement retrecies tag = 1 # toujours valide : derivation legitime
11 - PoC #2 : réduction de permissions
La monotonicité en pratique
CAndPerm ne peut que retirer des permissions, jamais en ajouter. Ce PoC dérive une capability read-only depuis une capability read-write, puis tente une écriture à travers la version restreinte.
#include <stdio.h> #include <stdlib.h> #include <cheriintrin.h> int main(void) { setvbuf(stdout, NULL, _IONBF, 0); char *buf = malloc(32); void * __capability cap = (void * __capability)buf; printf("permissions initiales : 0x%x\n", (unsigned)cheri_perms_get(cap)); /* Retrait legitime : capability restreinte a la lecture seule */ void * __capability readonly = cheri_perms_and(cap, CHERI_PERM_LOAD); printf("apres CAndPerm (LOAD uniquement) : 0x%x\n", (unsigned)cheri_perms_get(readonly)); printf("tentative d'ecriture via la capability read-only...\n"); *(volatile char * __capability)readonly = 'A'; /* CHERI fault attendu ici */ printf("jamais atteint\n"); free(buf); return 0; }
root@cheribsd-riscv64-purecap:~ # ./perm_narrowing permissions initiales : 0x6017d apres CAndPerm (LOAD uniquement) : 0x4 tentative d'ecriture via la capability read-only... In-address space security exception (core dumped) root@cheribsd-riscv64-purecap:~ # dmesg | tail -1 pid 1586 (perm_narrowing), jid 0, uid 0: exited on signal 34 (core dumped)
12 - PoC #3 : bornes étroites vs larges
Deux capabilities, un seul objet mémoire
Une capability large couvrant les 64 octets alloués, et une capability dérivée restreinte à seulement 8 octets sur le même bloc mémoire : le même accès à l'offset 16 réussit via l'une et échoue via l'autre.
#include <stdio.h> #include <stdlib.h> #include <cheriintrin.h> int main(void) { setvbuf(stdout, NULL, _IONBF, 0); char *buf = malloc(64); void * __capability wide = (void * __capability)buf; void * __capability narrow = cheri_bounds_set(wide, 8); printf("wide : base=0x%lx length=%lu\n", (unsigned long)cheri_base_get(wide), (unsigned long)cheri_length_get(wide)); printf("narrow : base=0x%lx length=%lu\n", (unsigned long)cheri_base_get(narrow), (unsigned long)cheri_length_get(narrow)); printf("acces a l'offset 16 via 'wide'... "); ((volatile char * __capability)wide)[16] = 'A'; printf("OK\n"); printf("acces a l'offset 16 via 'narrow' (hors de ses 8 octets d'autorite)...\n"); ((volatile char * __capability)narrow)[16] = 'A'; /* CHERI fault attendu ici */ printf("jamais atteint\n"); free(buf); return 0; }
root@cheribsd-riscv64-purecap:~ # ./bounds_narrow_vs_wide wide : base=0x40a0f000 length=64 narrow : base=0x40a0f000 length=8 acces a l'offset 16 via 'wide'... OK acces a l'offset 16 via 'narrow' (hors de ses 8 octets d'autorite)... In-address space security exception (core dumped)
13 - PoC #4 : la frontière hybride
Un pointeur classique ne devient jamais une capability tout seul
Compilé en ABI hybride (-mabi=lp64d, pas purecap), ce code passe un pointeur classique à une fonction qui déborde sans aucune vérification de bornes. Cela recoupe directement une découverte faite dans notre premier article : le matériel CHERI disponible ne protège rien sans compilation purecap explicite.
#include <stdio.h> /* Fonction ordinaire qui recoit un pointeur hybride classique */ void legacy_write(char *p, int len) { for (int i = 0; i < len; i++) p[i] = 'A'; /* pointeur brut : aucune verification de bornes ici */ } int main(void) { setvbuf(stdout, NULL, _IONBF, 0); char buf[16]; printf("Ecriture via un pointeur hybride classique (pas une capability) :\n"); legacy_write(buf, 40); /* deborde silencieusement */ printf("Ecriture terminee sans exception materielle -- 'p' n'a jamais ete une capability bornee.\n"); return 0; }
root@cheribsd-riscv64-purecap:~ # ./hybrid_boundary Ecriture via un pointeur hybride classique (pas une capability) : Ecriture terminee sans exception materielle -- 'p' n'a jamais ete une capability bornee. Segmentation fault (core dumped) # crash tardif, au retour de main(), signal 11
14 - PoC #5 : use-after-free sans révocation
Le tag reste valide même après free()
Retour sur l'avertissement du message d'accueil (section 08) : CHERI_CAPREVOKE est bien compilé dans ce noyau, mais sans allocateur qui déclenche activement une révocation, une capability vers un objet libéré garde un tag valide.
#include <stdio.h> #include <stdlib.h> #include <cheriintrin.h> int main(void) { setvbuf(stdout, NULL, _IONBF, 0); char *buf = malloc(32); void * __capability cap = (void * __capability)buf; printf("avant free() : tag=%d\n", cheri_tag_get(cap)); free(buf); /* sans revocation active, la capability garde un tag valide */ printf("apres free() : tag=%d (toujours valide, aucune revocation active)\n", cheri_tag_get(cap)); printf("tentative de reutilisation de la capability sur memoire liberee...\n"); ((volatile char * __capability)cap)[0] = 'X'; printf("ecriture reussie -- c'est precisement le fosse de la securite temporelle.\n"); return 0; }
root@cheribsd-riscv64-purecap:~ # ./uaf_no_revocation avant free() : tag=1 apres free() : tag=1 (toujours valide, aucune revocation active) tentative de reutilisation de la capability sur memoire liberee... ecriture reussie -- c'est precisement le fosse de la securite temporelle. # exit 0 : aucune exception materielle declenchee
CHERI_CAPREVOKE compilé dans le noyau fournit le mécanisme (balayage de la mémoire, marquage des régions en quarantaine), mais il faut un allocateur qui l'invoque explicitement lors d'un free() pour qu'une révocation ait réellement lieu. L'allocateur système par défaut de ce build ne le fait pas automatiquement, exactement comme documenté dans le premier article.
15 - Limites, coût réel et références
Ce que ça coûte vraiment de reproduire ce travail
- Temps : plusieurs heures au total (toolchain, CheriBSD, QEMU), avec au moins deux reconstructions complètes évitables rencontrées en cours de route (suppression prématurée d'un dossier de build).
- Disque : pic autour de 60 à 70 Go utilisés pendant la construction, avant nettoyage des dossiers d'objets intermédiaires.
- Réseau : plusieurs clones git (CheriBSD, QEMU CHERI, llvm-project) ont subi des coupures et ralentissements, nécessitant des scripts de retry avec
http.postBufferethttp.lowSpeedLimitajustés. - Mémoire : confortable sur 4 cœurs / 8 Go de RAM pour ce travail, mais la compilation LLVM en parallèle (
-j4) sature les cœurs disponibles pendant de longues périodes.
Rien de ce qui est décrit ici n'exige de matériel spécialisé, seulement du temps, de l'espace disque et de la patience face à des erreurs de compilation qui n'ont rien de mystérieux une fois identifiées. C'est précisément ce qui rend CHERI accessible à qui veut vérifier ses garanties par soi-même plutôt que de les prendre pour acquises.
Références
- cheribuild, l'outil de build officiel du projet CTSRD-CHERI
- CheriBSD, le portage FreeBSD purecap
- CHERI-Alliance/qemu, le fork QEMU avec support CHERI
- Notre premier article, "Sécurité Matérielle : CHERI Capabilities en Profondeur", pour l'architecture et le modèle de sécurité