Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mesure — le mécanisme noyau du seuil d’exécution (ADR-188 D3)

Statut : deux phases mesurées le 2026-09-28 — la première sur carbon et neon, sans redémarrage ; la seconde sur helium (aarch64, Raspberry Pi 4) puis sur hydrogen (x86_64), les deux machines que le BDFL a autorisées pour un redémarrage (§5, §5.5). Décision du BDFL le même jour : voie (ii) — amendements d’ADR-188 D3 et d’ADR-189 D4. Sert à : trancher ADR-188 D3 (quel mécanisme applique le seuil à chaque execve) et ADR-189 D4 (le noyau vérifie-t-il une signature, ou une empreinte dans une liste tenue par Air).


1. Ce qui a été mesuré, et comment

Sur carbon (x86_64, Ubuntu, noyau 7.0.0-34-generic) et neon (aarch64, Raspberry Pi, noyau 7.0.0-1020-raspi), en lecture seule : configuration du noyau (/boot/config-*), modules de sécurité actifs (/sys/kernel/security/lsm), paramètre de démarrage (/proc/cmdline), état d’IMA, trousseaux du noyau (/proc/keys), fonctions noyau disponibles aux programmes eBPF (/proc/kallsyms), système de fichiers racine (tune2fs -l), mode de démarrage.

Puis, sur carbon seulement (neon n’a pas bpftrace), la couverture des points d’accroche : bpftrace observe security_bprm_check, security_mmap_file (avec PROT_EXEC) et security_file_mprotect pendant quatre gestes — un binaire, un script #!, un dlopen depuis Python, le chargeur dynamique. C’est une observation : aucun module n’est activé, rien n’est bloqué.

2. Les trois mécanismes, sur nos deux machines

carbonneon
Modules actifslockdown,capability,landlock,yama,apparmor,ima,evmidem
IPE compiléoui — non actifoui — non actif
IPE : propriétésfs-verity (+ signature), dm-verity (+ signature)dm-verity seulement
fs-verity dans le noyauoui (signatures intégrées)non
Fonction verity de l’ext4 racinenonnon
IMAactif, aucune politique (1 seule mesure : le démarrage)idem
IMA : évaluationcompilée, réglable au démarrage (ima_appraise=)idem
IMA : politiquefichier en écriture seule, illisibleidem
BPF LSM compiléoui — non actifoui — non actif
eBPF : bpf_ima_file_hashouioui
eBPF : bpf_get_fsverity_digestouinon
eBPF : bpf_verify_pkcs7_signature, bpf_lookup_system_key, bpf_get_file_xattrouioui
Signatures que le noyau sait vérifierRSA (intégré), ECDSA (module) — pas Ed25519idem
DémarrageUEFI + GRUB (/etc/default/grub)firmware du Pi (/boot/firmware/cmdline.txt)
Accès de secoursOpenSSH actif (port 52222)OpenSSH actif (port 22)

IPE et BPF LSM ne sont pas dans la liste par défaut du noyau (CONFIG_LSM) : les activer demande d’ajouter lsm=…,ipe,bpf au paramètre de démarrage, donc un redémarrage.

3. La couverture des points d’accroche (carbon)

Gestebprm_checkmmap_file exécutable
execve d’un binaire (/usr/bin/true)une fois, le binairele binaire, ld-linux, libc
script #!/bin/shdeux fois : le script, puis l’interprète /bin/shdash, ld-linux, libc
dlopen depuis Python (libz, _ctypes)jamaisoui : chaque bibliothèque chargée
chargeur dynamique ld-linuxjamaisoui

Deux conséquences :

  1. Un script #! fait vérifier son interprète : la règle du BDFL — « un Python Air, jamais un Python venu de nulle part » — est applicable au point bprm_check.
  2. Un contrôle posé au seul execve laisserait passer toute bibliothèque chargée par dlopen, et le chargeur lui-même. Le seuil doit couvrir aussi mmap_file avec PROT_EXEC (et file_mprotect). IMA le fait (MMAP_CHECK), IPE aussi (op=EXECUTE), un programme BPF LSM le peut (lsm/mmap_file).

4. Ce que la première phase permet déjà de dire

  • IPE n’est pas utilisable aujourd’hui sur les deux machines. Sur neon, faute de fs-verity, il ne peut s’appuyer que sur dm-verity, c’est-à-dire sur une image système en lecture seule qu’Air n’a pas encore. Sur carbon, il faudrait aussi activer la fonction verity de l’ext4. IPE redeviendra le bon candidat avec une image système Air vérifiée (dm-verity).
  • IMA (évaluation) est utilisable sur les deux, mais la voie est lourde et risquée à éprouver : une politique écrite à chaud ne se retire qu’au redémarrage ; une évaluation par empreinte n’est sûre que si EVM protège l’attribut étendu (clé ou TPM) ; une évaluation par signature demande une clé RSA ou ECDSA dans le trousseau .ima — la voie (i) d’ADR-189 D4.
  • BPF LSM est le seul mécanisme utilisable à l’identique sur carbon et neon aujourd’hui. bpf_ima_file_hash y donne l’empreinte d’un fichier exécuté ; un programme peut la chercher dans une table tenue par air-bundle — la voie (ii) d’ADR-189 D4, avec les seules clés Ed25519 d’Air. Il peut aussi vérifier une signature PKCS#7 portée par un attribut étendu — la voie (i). La couche 0 d’Air sait déjà charger un programme de type Lsm et l’attacher (crates/air-sys-types/src/ebpf.rs).

Rien de cela ne tranche encore : il manque ce qui ne se mesure qu’une fois le module actif.

5. La seconde phase — mesurée sur helium

La machine. helium : Raspberry Pi 4, Ubuntu 26.04, noyau 7.0.0-1020-raspi — la même configuration de noyau que neon. carbon a été exclu par le BDFL : une session de travail y tourne, et ce n’est pas une machine de test.

Le redémarrage, sans risque de perte. Le Pi démarre par tryboot_a_b : une copie de /boot/firmware/current/ en new/, dont seule la ligne de démarrage diffère (lsm=lockdown,capability,landlock,yama,apparmor,ima,evm,ipe,bpf), puis reboot "0 tryboot". Ce démarrage ne vaut qu’une fois : le suivant revient à current/, et panic=10 y ramènerait de lui-même une machine en panique. Résultat : les modules actifs deviennent lockdown,capability,landlock,yama,apparmor,ipe,bpf,ima,evm, et l’interface d’IPE apparaît, sans politique.

Le programme de mesure — un programme BPF LSM en journal seul, qui ne refuse jamais rien : sur bprm_check_security et mmap_file (avec PROT_EXEC), il calcule l’empreinte du fichier par bpf_ima_file_hash et compte les cas et le temps passé ; sur file_mprotect, il compte. Compilé sur helium (clang -target bpf), chargé par bpftool prog loadall … autoattach.

5.1 Sans cache, le coût est prohibitif

Temps par execve (boucle bash)sans programmeavec le programmeaprès retrait
/bin/true2,65 ms19,3 ms2,66 ms
air-bundle (binaire Air, static-PIE, marqué)2,16 ms70,9 ms2,23 ms

Chaque execve et chaque mmap exécutable a été vu, y compris un script #! et son interprète, et le dlopen de Python ; toutes les empreintes ont abouti. Mais 7,4 ms par fichier empreinté en moyenne : sans politique IMA, bpf_ima_file_hash recalcule tout le fichier à chaque appel. Aucun mprotect exécutable n’a été observé.

5.2 Avec la politique de mesure d’IMA, le coût disparaît

Une politique IMA de mesure seulement — elle calcule et journalise, elle ne refuse rien :

measure func=BPRM_CHECK
measure func=MMAP_CHECK mask=MAY_EXEC
Temps par execvesans rienIMA mesure seuleIMA mesure + programme BPF
/bin/true2,65 ms2,65 ms2,69 ms
air-bundle (Air)2,16 ms2,25 ms2,17 ms

L’empreinte lue par le programme coûte alors ≈ 2 µs à l’execve et ≈ 2,7 µs au mmap : IMA calcule chaque fichier une fois par version (28 mesures au total, une seule pour le binaire Air exécuté 500 fois), et bpf_ima_file_hash relit son cache.

5.3 Le cache suit les modifications

Un binaire copié, exécuté, puis modifié (un octet ajouté) et réexécuté : IMA consigne une seconde empreinte, identique à celle de sha256sum. Le cache ne valide pas une ancienne version.

5.4 La machine a été rendue

Redémarrage normal : retour à current/, liste de modules d’origine, aucune politique IMA (une seule mesure, celle du démarrage), copie new/ supprimée, ligne de démarrage d’origine inchangée. Restent installés sur helium : bpftool et libbpf-dev.

5.5 La même mesure sur x86_64 — hydrogen

hydrogen (x86_64, Ubuntu, noyau 7.0.0-34-generic, exécuteur de CI air-x86_64), autorisé par le BDFL à condition de le sortir des exécuteurs pendant l’essai : service de l’exécuteur arrêté et désactivé (GitHub l’a vu hors ligne), puis réactivé à la fin (GitHub l’a revu en ligne, étiquette air-x86_64). Aucun job n’était en cours.

Sans toucher à son démarrage : kexec -s du même noyau avec la ligne de démarrage d’origine plus lsm=…,ipe,bpf — rien n’est écrit dans GRUB ni sous /boot ; un redémarrage normal l’a ramené à l’identique. Le binaire Air mesuré est air-bundle, construit et marqué sur carbon.

Temps par execvesans rienprogramme, sans cacheIMA mesure seuleIMA mesure + programme
/bin/true0,91 ms11,0 ms0,83 ms0,89 ms
air-bundle (Air)0,59 ms32,9 ms0,69 ms0,63 ms

Sans cache : 3,4 ms par empreinte à l’execve, 4,2 ms au mmap. Avec la mesure IMA : ≈ 0,3 µs et ≈ 1,5 µs. Même couverture qu’en aarch64 ; un fichier modifié reçoit une nouvelle empreinte, identique à sha256sum. hydrogen rendu : ligne de démarrage d’origine, modules d’origine, une seule mesure IMA. Reste installé : libbpf-dev.

5.6 Ce que cela établit — et ce qui n’est pas établi

Établi, sur aarch64 et x86_64 : un seuil BPF LSM qui compare l’empreinte de chaque exécutable et de chaque bibliothèque projetée en mémoire exécutable à une table tenue par Air — la voie (ii) d’ADR-189 D4 — couvre execve, les interprètes de #! et dlopen, pour un surcoût négligeable, à condition qu’une politique IMA de mesure tienne le cache des empreintes. Il ne demande que les clés Ed25519 d’Air, ni fs-verity ni image vérifiée.

Pas établi :

  • le refus lui-même : le programme mesuré ne refuse rien. Un refus sur mmap_file touche le chargeur et la libc de tout programme glibc : le mode transitoire d’ADR-188 D4 devra être éprouvé avant tout refus réel ;
  • la table : comment air-bundle l’alimente, qui peut l’écrire, ce qui se passe au redémarrage (la table et le programme doivent être posés avant le premier programme d’utilisateur) ;
  • la taille du journal IMA : une entrée par version de fichier exécuté, en mémoire ;
  • le coût du premier calcul d’un gros fichier, qui se paie une fois par version.

6. Ce que cette note ne décide pas

Tranché depuis : le BDFL a retenu la voie (ii) — BPF LSM, politique de mesure d’IMA, table d’empreintes tenue par Air — le 2026-09-28 (amendements d’ADR-188 D3 et d’ADR-189 D4). Cette note ne décide ni de la forme de la table, ni du passage au refus réel.

7. Phase 2 (2026-10-13) — ce que la phase 1 laissait « pas établi »

Mesuré sur helium (aarch64, 7.0.0-1020-raspi, bpf actif), la politique de mesure posée à chaud puis la machine rendue par un redémarrage. Les coûts par empreinte de la phase 1 n’ont pas été remesurés : ils tiennent.

7.1 La taille du journal : elle croît par VERSION de fichier, jamais par exécution

ExpérienceRésultat
500 exécutions du même fichier+0 entrée
200 fichiers distincts, une exécution chacun+200 entrées, exactement
Un fichier modifié d’un octet, réexécuté+1 entrée (deux au total pour ce chemin)
Coût par entrée — journal binaire106 octets
Coût par entrée — mémoire noyau (SUnreclaim)389 octets

Le contrôle, sans lequel le second chiffre aurait été faux de 3×. Créer les 200 fichiers sans les exécuter donne 0 entrée et pourtant +48 Kio de SUnreclaim — le coût des inodes, pas celui d’IMA. Seule la seconde moitié de l’expérience (les exécuter) attribue à la mesure ses +76 Kio. Mesurer la charge d’un bout à l’autre aurait imputé les deux à IMA.

Conséquence de dimensionnement : 10 000 versions distinctes ≈ 1 Mio de journal et ≈ 3,9 Mio de mémoire noyau, et ce journal ne se purge pas avant le redémarrage. Le cas qui mord n’est donc pas la machine de bureau — elle réexécute les mêmes fichiers — mais l’exécuteur de CI, qui fabrique des binaires neufs à chaque build.

7.2 Le coût du premier calcul : ~7,5 ms par Mio (aarch64)

Taille du fichier1er execve2e execve
168 Kio< 10 ms< 10 ms
2,1 Mio0,01 s0,00 s
20 Mio0,15 s0,00 s

Une fois par version, puis plus rien. À titre d’ordre de grandeur, le lot de binaires de test du dépôt relevé le même jour sur speedy — 43 binaires, 0,8 Gio, médiane 8,4 Mio, maximum 71,2 Mio — coûterait ≈ 6 s d’empreintes à sa première exécution, sur des builds qui durent une demi-heure.

Pas mesuré : le même débit sur x86_64, où le SHA-256 est plus rapide — la phase 1 donnait un rapport d’environ deux entre les deux architectures, mais ce rapport-ci n’a pas été établi.

7.3 La persistance : la politique ne survit pas au redémarrage

CONFIG_IMA_READ_POLICY et CONFIG_IMA_WRITE_POLICY ne sont pas compilés sur ce noyau. Trois conséquences, toutes vérifiées :

  1. la politique ne s’écrit qu’une fois par amorçage ;
  2. elle est en écriture seule : rien ne peut relire ce qu’elle couvre ;
  3. le fichier /sys/kernel/security/ima/policy disparaît du répertoire dès qu’elle est posée — constaté, non supposé.

Le point 3 n’était pas attendu, et il est utile : il rend la pose idempotente par une simple ConditionPathExists, de sorte qu’un systemctl restart est sauté (« unmet condition check ») au lieu d’être tenté puis échoué.

Le geste retenu (décision du BDFL du 2026-10-13, voie « geste à l’amorçage ») est une unité oneshot ordonnée avant le lanceur, livrée en exemple comme les autres unités — crates/air-service/units/air-ima-policy.service et ima-policy — et posée à la main. Éprouvée sur helium par un redémarrage complet : unité active (exited), code 0, et le compteur passe de 1 à 358, dont 122 entrées de binaires (BPRM_CHECK) et 236 de bibliothèques (MMAP_CHECK mask=MAY_EXEC) — les deux règles mordent, et systemd-executor figure parmi les mesurés, donc la politique est posée assez tôt pour couvrir les services du démarrage.

Pas éprouvé : l’ordre Before=air-launchd.service, helium ne portant aucun paquet Air. Il le sera à la pose sur carbon et neon.

7.4 La garde : le seuil refuse de se poser sans cache

La note de phase 1 qualifiait l’absence de politique de prohibitive ; rien ne la constatait. AirExecutionThresholdManager::load la refuse désormais (ThresholdLoadError::ImaMeasurementsAbsent), comme il refusait déjà un bpf inactif, et le lanceur l’écrit au journal par le même chemin que ses autres refus.

Le témoin, et sa limite — dite ici parce qu’elle ne peut pas être levée. La garde lit /sys/kernel/security/ima/runtime_measurements_count : il vaut 1 quand aucune politique n’alimente le journal (le seul boot_aggregate), et davantage dès qu’une politique mesure. Cela établit qu’une politique mesure — pas ce qu’elle couvre, puisque la politique active est illisible (point 2 ci-dessus). Une politique qui ne mesurerait ni l’exécution ni la projection exécutable satisferait donc la garde en laissant le cache froid. Sur une machine Air, la politique posée est celle que livre ce dépôt, dont le contenu est versionné.