La base de calcul de confiance : pourquoi la taille compte
Les discussions de sécurité se concentrent souvent sur des bogues individuels :
- un débordement de tampon
- une faille de député confus
- un parseur insuffisamment vérifié
- un chemin d’escalade de privilèges
Ces bogues comptent, mais ils sont les symptômes d’une question plus profonde :
Combien de code doit être correct pour que le système reste sûr ?
Ce code est la base de calcul de confiance, généralement abrégée en TCB.
La taille et la forme de la TCB déterminent combien de code doit être jugé fiable, audité, testé et raisonné. Un système peut avoir de fortes abstractions sur le papier, mais si ces abstractions dépendent du comportement parfait d’une grande quantité de code privilégié, l’argument de sécurité devient beaucoup plus faible.
Cet article explique ce qu’est la TCB, pourquoi sa taille affecte la surface d’attaque et comment EriX essaie de garder le code de confiance petit et explicite.
Qu’est-ce que la TCB ?⌗
La base de calcul de confiance est l’ensemble des composants dont la correction est nécessaire pour que les propriétés de sécurité du système tiennent.
Si un composant de la TCB est compromis, les garanties de sécurité du système peuvent ne plus être vraies.
Dans un système d’exploitation, la TCB inclut souvent :
- le bootloader
- le kernel
- les services système privilégiés
- la logique d’authentification et d’autorisation
- les parseurs des données de démarrage de confiance
- le code de vérification cryptographique
- le code qui distribue ou transfère l’autorité
La TCB exacte dépend de la conception du système.
Dans un kernel monolithique traditionnel, une grande partie du kernel fait généralement partie de la TCB parce que de nombreux sous-systèmes s’exécutent avec tous les privilèges du kernel. Un bogue dans un pilote, un système de fichiers ou une pile réseau peut devenir une compromission du kernel.
Dans un système microkernel, la TCB du kernel peut être plus petite, mais le système de confiance total inclut toujours les composants qui distribuent l’autorité et appliquent la politique.
Cette distinction est importante.
Les microkernels réduisent la quantité de code qui s’exécute avec l’autorité complète de la machine. Ils ne rendent pas automatiquement tous les services en espace utilisateur non fiables.
Fiable ne veut pas dire sûr⌗
Le mot “fiable” est facile à mal comprendre.
Un composant fiable n’est pas un composant dont on sait qu’il est correct.
C’est un composant dont le système dépend pour être correct.
C’est une définition moins confortable, mais c’est la bonne.
Si un service est chargé de distribuer des capacités, un bogue dans ce service peut accorder une autorité incorrecte. Si un parseur est chargé de valider un exécutable avant le démarrage, un bogue du parseur peut miner la chaîne de démarrage. Si un gestionnaire de syscall du kernel est chargé de valider les droits d’endpoints, une vérification manquante peut casser l’isolation.
La confiance n’est pas un compliment.
La confiance est un risque.
Le but n’est donc pas d’étiqueter autant de code que possible comme fiable. Le but est de rendre l’ensemble fiable aussi petit, étroit et auditable que possible.
Pourquoi la taille compte⌗
La taille de la TCB compte pour plusieurs raisons.
1. Plus de code signifie plus de bogues⌗
Tout logiciel contient des bogues.
À mesure que la quantité de code de confiance augmente, la probabilité de bogues importants pour la sécurité augmente aussi. C’est particulièrement vrai pour le code qui :
- parse des entrées non fiables
- gère la mémoire
- traite la concurrence
- interprète des permissions
- traduit un modèle d’autorité en un autre
Les systèmes d’exploitation contiennent tous ces motifs.
Réduire la taille de la TCB n’élimine pas les bogues, mais réduit la quantité de code où un bogue peut compromettre tout le système.
2. Plus d’interfaces signifie plus de surface d’attaque⌗
La surface d’attaque ne se résume pas aux lignes de code.
Elle inclut aussi les points d’entrée.
Chaque interface vers du code de confiance est un endroit où un attaquant peut fournir une entrée :
- arguments de syscall
- messages IPC
- métadonnées d’image de démarrage
- en-têtes ELF
- structures de système de fichiers
- descripteurs de périphériques
- événements d’interruption
- tables fournies par le firmware
Chaque interface nécessite une validation.
Un petit composant fiable avec une interface mal conçue peut rester dangereux. Mais quand le nombre d’interfaces de confiance augmente, la charge de validation augmente avec lui.
3. Plus d’état rend le raisonnement plus difficile⌗
Les échecs de sécurité ne se produisent souvent pas parce qu’une seule vérification manque, mais parce que l’état change dans un ordre inattendu.
Par exemple :
- une capacité est copiée avant que ses droits soient réduits
- un service démarre avant que son paquet de démarrage soit entièrement validé
- un slot local périmé est traité comme preuve d’autorité
- un périphérique est considéré présent avant la fin de la découverte
- un processus conserve une autorité après un chemin de lancement échoué
Plus un système possède d’état mutable de confiance, plus il est difficile de prouver que chaque transition préserve les invariants voulus.
C’est pourquoi EriX met l’accent sur le démarrage déterministe, les enregistrements de transfert explicites et le comportement fail-closed.
4. Plus de privilège augmente le rayon d’impact⌗
Le même bogue a des conséquences différentes selon l’endroit où il se produit.
Un bogue de parsing dans un outil non privilégié peut faire planter cet outil.
Un bogue de parsing dans le bootloader peut compromettre tout le système avant le démarrage du kernel.
Un bogue dans un pilote en espace utilisateur qui n’a accès qu’à une plage d’E/S spécifique est grave, mais ce n’est pas la même chose qu’un bogue dans un pilote qui s’exécute dans le kernel avec l’autorité complète de la machine.
Réduire la TCB consiste aussi à réduire le rayon d’impact des bogues individuels.
Le kernel n’est qu’une partie de la TCB⌗
Il est tentant de dire :
La TCB, c’est le kernel.
C’est généralement trop simple.
Le kernel est central, mais un système sûr dépend aussi du code qui prépare le kernel, démarre la première tâche en espace utilisateur, définit les formats d’autorité et distribue les capacités.
Dans EriX, le kernel est explicitement dans la TCB.
Il possède les tables de capacités de l’espace kernel, l’état de planification et les ressources de la machine. Il valide le handoff du bootloader vers le kernel, crée la tâche racine, gère les objets kernel de base et expose les points d’entrée trap, syscall et interruption propres à l’architecture.
Si le kernel n’applique pas correctement les vérifications de capacités, les droits d’endpoints, les limites d’espaces d’adressage ou les durées de vie des objets, le modèle d’isolation du système échoue.
Mais le kernel n’est pas toute l’histoire.
Le code de démarrage est aussi de confiance⌗
Le bootloader s’exécute avant le kernel.
Cela le rend critique pour la sécurité.
Dans EriX, le bootloader est responsable de charger et vérifier un boot.img
signé, de parser les images du kernel et des services, de construire une
structure de handoff déterministe et de transférer le contrôle au kernel.
Cela place le bootloader dans la TCB.
Il détient l’autorité fournie par le firmware pendant le démarrage et contrôle le saut final vers le kernel. Il doit traiter le support de démarrage comme non fiable, valider la structure et l’intégrité cryptographique de l’image, rejeter les binaires ELF malformés et échouer de manière fermée en cas d’ambiguïté.
Si le bootloader accepte une image modifiée ou construit un handoff incohérent, le kernel peut démarrer depuis une base compromise.
C’est pourquoi le code de démarrage doit être petit, strict et ennuyeux.
Les parseurs peuvent être des frontières de TCB⌗
Les parseurs sont souvent sous-estimés en sécurité système.
Ils se trouvent exactement là où des octets non fiables deviennent une structure fiable.
EriX traite plusieurs crates de parsing et d’ABI comme des composants de TCB ou adjacents à la TCB :
lib-bootimgparse et vérifie la structure, les hachages et les signatures deboot.imglib-elfvalide les binaires ELF64 avant que le bootloader fasse confiance aux segments de chargementlib-handoffvalide les structures de handoff versionnées entre étapes de démarragelib-ipcdéfinit et valide les layouts des messages IPClib-capabidéfinit les types de capacités, les droits, les constantes de slots et les descripteurs de transfert
Ces bibliothèques peuvent ne posséder aucune capacité runtime elles-mêmes.
Cela ne les rend pas hors sujet pour la TCB.
Si lib-bootimg accepte une image de démarrage modifiée, le bootloader peut
faire confiance à du code qu’il devrait rejeter. Si lib-elf accepte un
exécutable malformé, la chaîne de démarrage peut charger les mauvais octets ou
faire confiance à des plages de segments invalides. Si lib-ipc décode mal un
message, une opération peut être interprétée incorrectement. Si lib-capabi
définit une politique de rôle trop large, un service peut recevoir une autorité
qu’il ne devrait jamais posséder.
Du code pur peut quand même être du code de confiance.
La propriété importante est que ces bibliothèques sont étroites. Elles ne font pas d’E/S, ne possèdent pas la politique du système et évitent l’autorité ambiante. Leur rôle est de parser, valider et rejeter.
Les services en espace utilisateur peuvent être des composants fiables⌗
Déplacer la politique hors du kernel ne la fait pas disparaître.
Elle se déplace dans des services en espace utilisateur où elle peut être isolée, contrainte et auditée séparément.
Dans EriX, rootd est la première autorité en espace utilisateur porteuse de
politique. Il valide le handoff du kernel vers root, parse la configuration de
démarrage, exécute le DAG de démarrage et transfère des capacités de moindre
privilège aux services requis.
rootd est très privilégié.
Mais ce n’est pas le kernel.
Cette distinction est importante. rootd est fiable pour la politique précoce
et la distribution des capacités, mais il n’implémente pas les objets kernel et
ne possède pas directement l’autorité de la machine. Son rôle est de distribuer
l’autorité selon des contrats de démarrage explicites.
D’autres services se trouvent aussi dans des frontières de confiance précises :
procdest fiable pour l’orchestration du cycle de vie des processusdevicedest fiable pour la politique des pilotes et la livraison de leurs capacitésvfsdest fiable comme frontière de l’espace de noms public du système de fichiers- les fournisseurs privés de systèmes de fichiers ne sont fiables que pour leur rôle de fournisseur
Cela ne rend pas ces services peu importants.
Cela rend leur autorité plus étroite que l’autorité complète du kernel.
Surface d’attaque dans une conception monolithique⌗
Dans un kernel monolithique, de nombreux sous-systèmes partagent un seul espace d’adressage privilégié.
Cela peut simplifier les chemins rapides, mais crée aussi une large surface d’attaque.
Une vulnérabilité dans n’importe quel sous-système du kernel peut devenir une vulnérabilité du kernel :
- métadonnées de système de fichiers malformées
- traitement défectueux de paquets réseau
- code de pilote dangereux
- descripteurs matériels inattendus
- conditions de course dans l’état partagé du kernel
L’attaquant n’a besoin que d’un chemin vers du code privilégié.
Cela ne veut pas dire que les kernels monolithiques ne peuvent pas être sûrs. Ils peuvent être conçus, durcis, fuzzés, sandboxés et audités largement.
Mais l’architecture commence avec une grande surface privilégiée.
L’argument de sécurité doit ensuite expliquer comment cette grande surface est contrôlée.
Surface d’attaque dans une conception microkernel⌗
Un microkernel change la forme de la surface d’attaque.
Le kernel expose toujours des interfaces critiques :
- syscalls
- livraison IPC
- planification
- opérations d’espace d’adressage
- opérations de capacités
- gestion des interruptions
Ces interfaces doivent être correctes.
Mais les services de plus haut niveau ne s’exécutent pas automatiquement avec le privilège complet du kernel. Si un fournisseur de système de fichiers traite un média malformé, le but est que le bogue reste dans l’autorité de ce fournisseur. Si un pilote échoue, le but est qu’il échoue seulement avec l’autorité de périphérique qui lui a été explicitement donnée.
Cela transforme une grande surface privilégiée en plusieurs surfaces d’autorité plus petites.
Ce n’est pas automatiquement plus simple.
Cela ne fonctionne que si les frontières sont strictes et si l’autorité qui les traverse est étroite.
Comment EriX minimise la TCB⌗
EriX réduit la taille de la TCB et la surface d’attaque par plusieurs choix de conception.
1. Un kernel minimal en politique⌗
Le kernel EriX est responsable des mécanismes, pas de la politique système.
Il gère :
- les objets kernel de base
- la sémantique des capacités
- la planification et l’exécution des tâches
- les primitives d’espaces d’adressage
- IPC et le dispatch des endpoints
- les points d’entrée d’interruptions et exceptions
Il ne possède pas :
- la politique de démarrage des services
- la politique d’orchestration des processus
- la politique de l’espace de noms des systèmes de fichiers
- la politique d’activation des pilotes
- la politique d’allocation mémoire de haut niveau
Cela garde le kernel concentré sur l’application de l’isolation plutôt que sur la forme globale du système.
2. Des capacités explicites au lieu d’une autorité ambiante⌗
EriX modélise l’autorité par des capacités.
Un composant ne peut agir que s’il détient une capacité avec les droits requis. Le kernel valide les références de capacités à chaque utilisation, applique les droits d’endpoints lors du dispatch syscall et traite les transferts comme des événements explicites.
Cela évite de dépendre de noms globaux ou de numéros de slots conventionnels comme permissions.
Connaître un numéro de slot n’est pas une autorité.
Détenir la bonne capacité dans le CSpace local est une autorité.
Cette distinction est centrale pour réduire la TCB : le code de confiance n’a pas besoin d’inférer des permissions depuis un état global quand l’autorité est portée explicitement.
3. Des endpoints étroits de contrôle du kernel⌗
Les anciens designs de systèmes d’exploitation concentrent souvent le contrôle derrière de larges interfaces privilégiées.
EriX va dans l’autre sens.
Le runtime actuel utilise des familles étroites d’endpoints de contrôle du kernel pour des tâches spécifiques :
- temps
- interruptions
- hotplug
- configuration PCI
- console et framebuffer
- E/S COM1
- E/S i8042
- retypage mémoire
- mappage VSpace
- résolution de défauts du pager
- contrôle des processus
- lectures ACPI
Le démarrage runtime normal ne donne pas aux services un large endpoint root pour toutes les opérations kernel.
À la place, un service reçoit la famille d’endpoint précise dont il a besoin.
timed reçoit le contrôle du temps. irqd reçoit le contrôle des interruptions.
drv-serial reçoit l’E/S spécifique à COM1. drv-i8042 reçoit l’E/S spécifique
à i8042. drv-acpi reçoit l’autorité de lecture ACPI.
Cela réduit à la fois l’interface de confiance et les dégâts causés par un mauvais usage.
4. Des contrats de démarrage exacts⌗
EriX traite le transfert de capacités au démarrage comme un contrat, pas comme une suggestion.
Les enveloppes de démarrage décrivent quelles capacités un service doit recevoir, où elles doivent apparaître et quels droits elles doivent porter.
Les services valident les slots locaux réels qu’ils ont reçus avant de se déclarer prêts. Les transferts d’endpoints sont vérifiés par slot source, slot destination, droits et type d’endpoint attendu. Les transferts inconnus, incorrects ou supplémentaires sont rejetés.
Cela évite un bogue d’autorité courant :
“Un slot est occupé, donc ce doit être le bon objet.”
Dans EriX, l’occupation d’un slot ne prouve pas l’autorité.
La capacité doit correspondre à la forme d’autorité déclarée.
5. Création de processus par étapes⌗
La création de processus est une opération à haut risque parce qu’elle combine exécution, espaces d’adressage, endpoints et autorité initiale.
EriX fait passer la création de processus runtime par une création d’enfant par étapes plutôt que par une ancienne opération de création directe.
Le flux par étapes est explicite :
- créer un enfant en étape
- recevoir un grant d’installation et un alias d’endpoint de l’enfant
- installer le paquet déclaré de capacités de démarrage
- démarrer le processus seulement une fois la population complète
Le kernel refuse de démarrer le processus tant que des grants d’installation vivants ciblent encore cette étape d’enfant.
Cela empêche des processus partiellement peuplés de devenir exécutables avec un état d’autorité ambigu.
6. Autorité de pilotes spécifique au rôle⌗
Les pilotes sont une source majeure de risque dans les systèmes d’exploitation.
EriX ne traite pas tout contrôle matériel comme une permission large unique.
L’autorité des pilotes est spécifique au rôle :
drv-serialreçoit l’autorité d’E/S COM1 uniquementdrv-i8042reçoit l’autorité d’E/S i8042 uniquementdrv-acpireçoit l’autorité de lecture ACPIprobedreçoit l’autorité de lecture de configuration PCIdrv-virtio-blockreçoit un frame de périphérique validé plutôt qu’une autorité mémoire générale
deviced gère la politique des pilotes, mais il ne donne pas simplement à
chaque pilote une capacité générique de contrôle de périphérique. Il utilise
l’installation explicite de capacités de démarrage et des surfaces d’autorité
spécifiques au rôle.
Cela limite l’impact des bogues de pilotes sur la TCB.
7. La mémoire de périphérique est un type de capacité séparé⌗
La mémoire de périphérique est dangereuse parce qu’elle peut affecter directement l’état matériel.
EriX distingue la mémoire de périphérique de la RAM ordinaire avec
CAP_TYPE_DEVICE_FRAME.
Le chemin de stockage peut dériver un frame MMIO adossé à un BAR pour deviced,
et deviced peut installer uniquement ce frame de périphérique dérivé dans le
paquet de démarrage d’un pilote.
Ce frame de périphérique n’est pas traité comme de la mémoire allouable ordinaire.
C’est important parce que confondre mémoire de périphérique et frames normaux élargirait le modèle d’autorité et rendrait le raisonnement sur la sûreté mémoire plus difficile.
8. Dépendances en salle blanche⌗
Les dépendances peuvent agrandir la TCB silencieusement.
Si le code de confiance dépend d’une bibliothèque externe généraliste, le système peut hériter :
- de chemins de code dont il n’a pas besoin
- d’hypothèses qui ne correspondent pas à l’OS
- d’un comportement de parsing trop permissif
- d’un risque de mise à jour et de chaîne d’approvisionnement
EriX évite les crates tiers et implémente ses bibliothèques critiques dans le projet.
Cela augmente le travail d’implémentation, mais garde visible la frontière de confiance. Quand un parseur, un crate d’ABI ou un helper cryptographique fait partie du chemin de démarrage ou d’autorité, il fait partie de la surface de revue du système.
9. Comportement fail-closed⌗
Une petite TCB ne suffit pas si les échecs sont ambigus.
EriX essaie de rendre les échecs sensibles pour la sécurité explicites et terminaux :
- un handoff de démarrage malformé arrête le démarrage
- les versions non prises en charge sont rejetées
- une autorité de démarrage invalide empêche la disponibilité
- les mauvais types d’endpoints échouent à la validation
- l’échec d’un service requis arrête la progression
- les échecs après démarrage déclenchent un démontage fail-closed
Échouer de manière fermée est important parce que les heuristiques de récupération deviennent souvent de la politique cachée.
La politique cachée élargit le comportement de confiance du système.
Plus petit ne veut pas dire trivial⌗
Réduire la TCB ne rend pas la conception du système facile.
Cela la rend souvent plus explicite.
Au lieu de placer toute la logique dans un seul espace d’adressage privilégié, le système doit définir :
- quel composant possède chaque décision
- quelle autorité reçoit chaque composant
- comment l’autorité est transférée
- comment les échecs sont rapportés
- comment un démarrage partiel est nettoyé
- comment les capacités périmées sont invalidées ou supprimées
C’est plus de travail au départ.
Mais cela produit un système dont l’argument de sécurité est plus facile à inspecter.
La question devient :
Quel composant doit être fiable pour cette propriété précise ?
C’est une meilleure question que :
Est-ce que tout le kernel est correct ?
Une vue pratique de la TCB d’EriX⌗
Une vue pratique de la TCB d’EriX ressemble à des couches.
À la base se trouve la chaîne de démarrage :
- bootloader
- vérification de l’image de démarrage
- chemin lecture/vérification de
lib-bootimg - parsing ELF
- validation du handoff
Puis le kernel :
- objets de capacités
- application de CSpace et VSpace
- IPC et droits d’endpoints
- planification et gestion des traps
- livraison des interruptions
Puis l’espace utilisateur fiable précoce :
rootdpour la politique de démarrage et la distribution de capacitésprocdpour le cycle de vie des processusdevicedpour la politique des pilotes- bibliothèques d’ABI et de validation sélectionnées, partagées par les services
Puis des services fiables plus étroits :
- médiation de l’espace de noms des systèmes de fichiers dans
vfsd - fournisseurs backend privés
- services d’entrée, console, journalisation, bloc, temps et interruptions
Tous ces composants ne sont pas également privilégiés.
C’est précisément le but.
EriX essaie d’éviter un seul monde de confiance plat. Chaque composant devrait être fiable uniquement pour son rôle documenté et uniquement avec les capacités qui lui ont été explicitement données.
Et ensuite⌗
La discussion sur la TCB mène naturellement au langage d’implémentation.
Si le code de confiance doit être petit, explicite et auditable, alors la sûreté
mémoire compte. Les frontières unsafe, la correction des parseurs, le layout
des données et la discipline requise près du matériel comptent aussi.
Le prochain article examinera pourquoi EriX est écrit principalement en Rust, ce que Rust résout et ne résout pas pour le développement de kernels, et comment il se compare à l’approche traditionnelle en C pour la programmation système.