Note de conception — la table d’empreintes du seuil d’exécution
Statut : tranchée par le BDFL le 2026-09-28 (§6 : les six questions). Réalisé depuis :
ADR-190 (programmes générés, lecteur de BTF, chargeur air-execution-threshold). Reste à
faire : faire poser le seuil par le lanceur (§7).
Met en œuvre, si elle est retenue : l’amendement du 2026-09-28 d’ADR-188 D3 et la voie (ii)
d’ADR-189 D4 — un programme BPF LSM compare l’empreinte de ce qui s’exécute à une table tenue
par Air.
Rédigée le 2026-09-28, après la mesure consignée dans
mesure-mecanisme-noyau-seuil-fr.md.
1. Pourquoi cette note
La voie (ii) déplace la confiance : le noyau ne vérifie plus une signature, il croit une table. Celui qui peut y écrire décide de ce qui s’exécute. La table est donc désormais ce qu’il faut protéger, et sa conception décide de ce que vaut le seuil.
2. Ce qui est établi, et qui oriente tout
- Les empreintes coïncident. IMA calcule un SHA-256 du contenu du fichier : mesuré, il
est identique à
sha256sum. C’est exactement ce que porte un manifeste pour chacun de ses exécutables (artifact_sha256,crates/air-bundle/src/signature.rs). La table peut se remplir des empreintes déjà signées, sans rien recalculer ni rien signer de nouveau. - Le lanceur vérifie déjà ces signatures au démarrage :
air-launchdparcourt les racines de bundles et arbitre chacun (signature de projet du manifeste, octroi signé par la machine) avant de le lancer (crates/air-launchd/src/discovery.rs, au plus 64 services). - La couche 0 sait déjà faire le nécessaire : charger un programme de type
Lsm, l’attacher (LsmMac), créer des cartes, figer une carte (bpf_map_freeze: plus aucune écriture depuis l’espace utilisateur), épingler ou non (crates/air-sys-syscall/src/ebpf.rs). - Le coût est négligeable si une politique IMA de mesure tient le cache des empreintes (0,3 à 2,7 µs par empreinte, sur aarch64 et x86_64), et prohibitif sans elle (3,4 à 7,4 ms).
- Le programme mesuré ne refusait rien. Le refus réel n’a jamais été éprouvé.
3. La proposition
3.1 La table n’est pas un fichier : elle est dérivée, à chaque démarrage
Il n’y a pas de table sur disque. La source de vérité, ce sont les attestations signées déjà présentes : les manifestes des bundles aujourd’hui, les attestations hors bundle demain (ADR-189 D2). Au démarrage, le lanceur, qui les vérifie de toute façon, en déduit la table en mémoire du noyau. Rien de nouveau n’est à protéger sur disque : falsifier la table demanderait de falsifier une signature de projet.
3.2 La forme
- Une carte BPF de type table de hachage : clé = empreinte SHA-256 (32 octets), valeur = un
petit enregistrement qui dit d’où vient l’entrée (exécutable de bundle, attestation hors
bundle, exemption transitoire nommée — ADR-188 D4). L’origine sert au journal et à
l’inventaire (
air-bundle exemptions). - Bornée : un nombre maximal d’entrées fixé à la création, que la conception doit chiffrer.
3.3 Qui l’écrit : le lanceur, et lui seul
air-launchd est déjà root, hors de toute cage, et vérifie déjà les signatures. Il crée la
table, la remplit et charge le programme. Aucun autre processus ne reçoit le descripteur de la
carte. Un nouvel octroi (air-bundle grant) ne l’écrit pas lui-même : il demande au lanceur
de reprendre ce bundle, que le lanceur vérifie avant d’ajouter son empreinte.
3.4 La protection
Trois couches, de la plus simple à la plus forte :
- Pas d’épinglage. La carte n’existe que par les descripteurs du lanceur ; elle n’apparaît
pas dans
/sys/fs/bpf, où un fichier serait à la portée de tout processus root. - Des générations figées. Chaque table remplie est figée (
bpf_map_freeze) ; une mise à jour construit une nouvelle génération, la fige, et la substitue d’un coup (carte de cartes). Aucune table active n’est jamais modifiable. - Le programme se protège. Le même programme BPF LSM surveille les opérations
bpfet refuse qu’un autre processus que le lanceur touche à ses cartes ou détache ses programmes. C’est à mesurer : ce que le point d’accrochebpfdu noyau permet réellement d’identifier et de refuser.
La limite, dite franchement : contre root, aucune de ces couches n’est une garantie
tant que le noyau n’est pas verrouillé (lockdown) et le démarrage vérifié. Un root peut
charger son propre programme, recharger un noyau, ou modifier le lanceur sur le disque. Le seuil
protège contre les utilisateurs et les services confinés, pas contre une machine dont
root est compromis. C’est la même frontière qu’aujourd’hui, et c’est ADR-150 D5 (le démarrage
vérifié) qui la déplacera.
3.5 Le démarrage
- La politique IMA de mesure doit être posée tôt. systemd charge
/etc/ima/ima-policyau démarrage d’après sa documentation ; c’est à vérifier sur nos machines. - Le programme et la table sont posés par le lanceur avant le premier service et la première session. Tout ce qui s’exécute avant (le noyau, l’initramfs, systemd, udev) échappe au seuil. C’est le périmètre « hors arbitrage » d’ADR-150 D5, que le démarrage vérifié doit couvrir.
3.6 La portée du refus
bpf_get_current_ancestor_cgroup_id permet au programme de savoir si le processus qui exécute
est sous l’arbre de cgroups du lanceur (ses services, et ce qu’ils lancent ; où tombent
exactement les sessions d’air-sshd est à vérifier, et la cage par session le fixera). La
proposition : le refus ne s’applique qu’à cet arbre. C’est exactement le périmètre B d’ADR-188
D1 (« tout ce qui s’exécute sous une cage Air »), et le reste de la machine Ubuntu n’est pas
touché pendant la transition.
4. Le passage au refus, par étapes
- Journal partout : le programme compare et journalise, il ne refuse rien. On mesure ce qui serait refusé.
- Refus pour les exécutables des bundles : un service dont le binaire n’est pas dans la table ne démarre pas. C’est le cas le plus sûr : ces binaires sont tous attestés.
- Refus sous l’arbre du lanceur, exemptions nommées comprises : les sessions n’exécutent que ce qui est attesté ou exempté (ADR-188 D4).
- v1 : les exemptions sont vidées.
Chaque étape attend que la précédente ait tourné sans refus inattendu sur carbon, neon, puis le parc.
5. Ce que cela coûte, et ce qui manque aujourd’hui
- Écrire un programme eBPF dans le dépôt. La mesure l’a écrit en C, compilé par
clang. Air n’a aujourd’hui aucune source eBPF ni chaîne pour en produire, et c’est une décision de chaîne d’outils (Q6). - Les exemptions transitoires ne sont pas des empreintes.
/usr,/binet les shells glibc d’Ubuntu changent à chaque mise à jour. À l’étape 3, il faudra soit les exempter par chemin (comme Landlock le fait déjà), soit accepter qu’ils soient journalisés sans être refusés. C’est une question de conception à trancher à ce moment-là. - La taille du journal IMA : une entrée par version de fichier exécuté, en mémoire.
6. Les décisions du BDFL (2026-09-28)
Chaque question est suivie de ce qui a été décidé, puis de ce qui a été constaté pour le décider.
- Q1 — Qui écrit la table ? →
air-launchd, seul. Il pose le seuil, remplit la table et en détient seul les descripteurs. La table est remplie des exécutables des bundles qui passent l’arbitrage — manifeste signé par une clé de projet et octroi signé par la machine ; un bundle seulement installé n’y entre pas. Un nouvel octroi prend effet au redémarrage du lanceur, qui relit tout ; un canal directair-bundle grant→ lanceur pourra venir plus tard. Constaté : le chargeur rend un objet qui vit aussi longtemps que son processus ; le seuil appartient donc à un processus, et le lanceur est le seul qui vérifie déjà les signatures. - Q2 — La protection → pas d’épinglage, table figée à chaque démarrage, auto-protection plus
tard. Le lanceur remplit la table puis la fige (
BPF_MAP_FREEZE) : plus aucun processus ne peut la modifier depuis l’espace utilisateur, pas même lui. Les « générations » du §3.4 ne deviennent nécessaires que le jour d’un canal d’octroi à chaud. L’auto-protection par le programme lui-même viendra après avoir été mesurée. La limite face à root est acceptée jusqu’au démarrage vérifié. - Q3 — Si le seuil n’est pas en place → ouverture avec alerte pendant la transition,
fermeture en v1 ; la mort du lanceur emporte son arbre. Si le programme ne se charge pas au
démarrage, le lanceur démarre quand même pendant la transition, et alerte (journal, et
inventaire des exemptions : « seuil non posé ») ; en v1, il refuse de démarrer services et
sessions. Si le lanceur meurt, systemd tue son cgroup — services et sessions compris — : rien de
l’arbre d’Air ne s’exécute sans seuil, et il n’est pas besoin d’épingler les programmes.
Constaté : l’unité
air-launchd.servicene fixe pasKillMode(défautcontrol-group) et n’a pas deRestart=. Éprouvé sur helium le 2026-09-28 : une unité aux réglages exacts d’air-launchd.service, dont le processus principal se range dans un sous-cgroupsuperviseuret lance un « service » dans un autre sous-cgroup, qui lance une « session » — la disposition relevée sur carbon. Aprèskill -9du processus principal, l’unité passefailed(causesignal) et aucun processus du service ni de la session ne survit. Ce n’était pas le binaireair-launchdlui-même : c’est systemd qui décide ici, et c’est lui qui a été éprouvé. - Q4 — La portée du refus → l’arbre du lanceur pendant la transition, toute la machine en
v1. Le refus ne s’applique qu’à
air-launchd.serviceet à ce qui est dessous ; le reste d’Ubuntu n’est pas touché. En v1, quand le système lui-même est Air, la portée devient toute la machine. En journal seul, le programme compte à part l’arbre du lanceur et le reste, pour mesurer dès maintenant ce que la portée v1 refuserait. Constaté sur carbon : le lanceur,air-sshdet une session SSH sont sous/system.slice/air-launchd.service/; le terminal du bureau est sous/user.slice/. - Q5 — Le passage au refus → les quatre étapes du §4, chaque passage décidé par le BDFL. Critère : une semaine sur carbon et neon, puis sur le parc, sans refus inattendu dans l’arbre du lanceur. Pour en juger, le programme, en journal seul, émet un événement par empreinte absente (l’empreinte, le cgroup, le nom du processus), que le lanceur relaie à journald : on lit ce qui aurait été refusé, pas seulement combien. Le mode du seuil (journal, bundles, arbre) est un réglage de la machine, lu par le lanceur au démarrage : revenir en arrière, c’est changer ce réglage et redémarrer le lanceur. Les exemptions transitoires d’Ubuntu se traitent à l’étape 3.
- Q6 — Comment Air écrit un programme eBPF → tranchée par ADR-190 : généré en Rust par un
générateur écrit par nous, chargé grâce à un lecteur de BTF d’Air ; licence déclarée au noyau
Dual MPL/GPL(ADR-190 D5). Fait :air-btf,air-ebpf-programs,air-execution-threshold.
7. Les incréments qui en découlent
Éprouver Q3 sur helium— fait le 2026-09-28 : la mort du lanceur emporte ses services et ses sessions (§6, Q3).Faire évoluer le programme— fait le 2026-09-28 : la portée du processus (ancêtre de son cgroup au niveau de l’arbre du lanceur), deux jeux de compteurs (Q4), et un événement par empreinte absente écrit dans un tampon circulaire (Q5), avec son décodeur. Éprouvé sur helium,bpfactif : cinq exécutions dans l’arbre donnent 20 examens, tous absents (table vide), séparés des 13 examens du reste de la machine. La lecture du tampon — projeter la carte et suivre le protocole du noyau — revient à l’incrément 3, avec le relais vers journald.- (préalable fait le 2026-09-28 : la lecture du tampon des événements — un consommateur
de tampon circulaire en couche 0,
BpfRingBufferConsumer, etdrain_eventsdansair-execution-threshold, éprouvés en root.)Le lanceur pose le seuil— fait le 2026-09-28 (air-launchd, étape 1 du §4) : au démarrage, il lit le mode dans/etc/air/launcher.aircfg(domaineair.launcher, lu sous la garde du chargeur :root, inscriptible par lui seul, pas de lien). Un réglage absent, illisible, non fiable ou refusé vaut « off » (décision du BDFL du 2026-09-28) : le seuil n’est pas posé, et le lanceur l’alerte. Enjournal, il pose le seuil sur l’arbre de son unité, remplit la table des bundles qui passent l’arbitrage, la fige, et relaie les événements à journald ; s’il ne peut pas le poser, il alerte et démarre quand même. Une étape non ouverte (bundles,launcher-tree,machine) est posée en journal, et dite telle. Écart assumé avec Q5 : le programme écrit un événement par examen — chaque projection de chaque bibliothèque par chaque processus de la machine —, que le lanceur journalise une fois par empreinte et par portée, en comptant les répétitions. La borne est par portée (2048) : un compte quelconque peut remplir celle d’« ailleurs » par desmmapexécutables de fichiers distincts, jamais celle de l’arbre ; son franchissement est alerté aussitôt. Un tampon plein perd des événements : le lanceur le déduit des compteurs du programme et l’alerte, sans pouvoir dire lesquels. Éprouvé sur helium (tryboot,bpfactif) : table figée à une empreinte ; les exécutions du service comptées présentes dans l’arbre ; 26 examens absents ailleurs sur la machine, 25 empreintes distinctes journalisées. Écrire le réglage — fait le 2026-09-28 :air-bundle thresholdle montre,air-bundle threshold set <mode>l’écrit sous mot de passe administrateur (étapes ouvertes seulement :off,journal), puis le relit par la garde du lanceur et refuse de dire « écrit » si le lanceur ne le lirait pas ainsi ; l’inventaire des exemptions dit l’état du réglage en tête (« seuil non posé » compris). Ce qu’aucun des deux ne peut dire : si le lanceur a réussi à poser le seuil — son journal le dit à chaque démarrage. - La politique de mesure d’IMA sur les machines, sans laquelle le coût est prohibitif.
- Les étapes suivantes, une par une, chacune après sa semaine d’observation et sur décision du BDFL.
8. Ce que cette note ne décide pas
- La forme de l’attestation hors bundle (ADR-189 D2).
- Le binaire compilé par le développeur (ADR-189 D5, renvoyé à plus tard).
- La cage par session (
conception-cage-par-session-fr.md), qui décidera de l’arbre de cgroups sous lequel tournent les sessions.