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-bootimg parse et vérifie la structure, les hachages et les signatures de boot.img
  • lib-elf valide les binaires ELF64 avant que le bootloader fasse confiance aux segments de chargement
  • lib-handoff valide les structures de handoff versionnées entre étapes de démarrage
  • lib-ipc définit et valide les layouts des messages IPC
  • lib-capabi dé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 :

  • procd est fiable pour l’orchestration du cycle de vie des processus
  • deviced est fiable pour la politique des pilotes et la livraison de leurs capacités
  • vfsd est 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 :

  1. créer un enfant en étape
  2. recevoir un grant d’installation et un alias d’endpoint de l’enfant
  3. installer le paquet déclaré de capacités de démarrage
  4. 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-serial reçoit l’autorité d’E/S COM1 uniquement
  • drv-i8042 reçoit l’autorité d’E/S i8042 uniquement
  • drv-acpi reçoit l’autorité de lecture ACPI
  • probed reçoit l’autorité de lecture de configuration PCI
  • drv-virtio-block reç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 :

  • rootd pour la politique de démarrage et la distribution de capacités
  • procd pour le cycle de vie des processus
  • deviced pour 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.