ADR-159 — air-elf : un codec de couche 1, et le rôle qui le nomme
Statut : Accepté (2026-08-14, BDFL). Crée la crate air-elf en couche 1 et introduit
le rôle codec dans le vocabulaire d’architecture de code
(ADR-149 D5/D9).
Catégorie : Architecture de code + sécurité. Décide qui sait lire un ELF dans Air, et sous quelle discipline.
Contexte
Ce qui existe, et qui n’est pas ce qu’il faut
Deux endroits du dépôt connaissent le format ELF, et aucun ne répond au besoin :
xtask/src/elf.rs(169 lignes) — écrit le 2026-08-14 pour les gates ABI. C’est de l’outillage de développement : hôte uniquement, jamais livré, hors couches, hors paquets. Il lit l’en-tête, la table des sections et.dynsym, soit de l’ordre de 5 % du format ; il ignore les en-têtes de programme, la table dynamique, les relocations, les notes, le contenu des définitions de version, le 32 bits et le gros-boutiste — qu’il refuse plutôt que de deviner.air-runtime(couche 1,role = "runtime") —reloc.rsetthread_local_storage.rsmanipulent des structures ELF, mais pour un autre problème : appliquer ses propres relocationsR_*_RELATIVEen l’absence deld.so(static-PIE, ADR-050) et poser le TLS. Il lit sa propre image en mémoire, par les en-têtes de programme. Pas de fichier, pas de table des sections, et sous une contrainte de bootstrap absolue qui interdit à peu près tout.
Aucune dépendance ELF externe n’existe (ni goblin, ni object), ce qui est cohérent avec la
règle des 80 % (ADR-024) et le bannissement de toute
surface C.
Le besoin, et il est de sécurité
air-bundle n’inspecte pas la nature des exécutables qu’il empreinte. Il en prend
l’empreinte du contenu — ce qui atteste qu’un octet n’a pas bougé, jamais ce que cet octet
est. Rien ne vérifie qu’un exécutable octroyé est bien un static-PIE linux-air plutôt
qu’un binaire glibc étranger.
Or c’est précisément le seuil d’exécution que le projet se donne pour but : un binaire glibc étranger ne doit pas tourner sur un système Air. Le plafond d’octroi peut borner ce qu’un programme fait ; il ne dit rien de ce qu’un programme est. Tant que personne ne sait lire un ELF en production, cette moitié de la doctrine repose sur la confiance.
Il ne s’agit donc pas d’« un besoin qui viendra peut-être », argument que la doctrine d’API du projet refuse de toute façon : il s’agit d’un écart ouvert entre ce qu’Air annonce et ce qu’Air vérifie.
Décision
D1 — air-elf est une crate de couche 1
Couche 1, donc : pas d’appel système, pas d’accès au noyau, et layering strict — elle ne consomme pas la couche 0 pour lire des octets qu’on lui remet déjà.
Le placement se justifie par ce qu’elle est : une transformation pure d’octets en types,
au même rang que les autres codecs du socle. La couche 0 est réservée à l’isolement des appels
unsafe (ADR-149 D2), et air-elf n’en contient aucun.
D2 — Elle est cataloguée role = "codec", rôle créé par cet ADR
[package.metadata.air]
layer = 1
role = "codec"
Le vocabulaire d’ADR-149 D9 comptait sept rôles — manager, controleur, vocabulaire,
bridge, runtime, syscall, service. Aucun ne décrit ce qu’est un codec, et l’absence se
voyait déjà : plusieurs crates se décrivent « pur codec, sans I/O » en commentaire
(air-json d’ADR-114, les codecs de la base comptes, le codec sockaddr↔bytes
d’ADR-070) sans qu’aucun registre ne le sache. Une propriété écrite en prose n’est pas
vérifiable ; c’est exactement ce que la règle NO GO reproche.
D3 — Ce que codec veut dire, et ce qu’il interdit
Un codec transforme des octets en types, et des types en octets. Il reçoit un &[u8],
il écrit dans un &mut [u8]. Rien d’autre.
| Exigence | Pourquoi |
|---|---|
| Aucune E/S — ni fichier, ni socket, ni descripteur | C’est l’appelant qui apporte les octets. Un codec qui ouvre un fichier n’est plus testable sans système de fichiers, ni fuzzable, ni réutilisable par un appelant qui a déjà les octets en mémoire |
| Aucun appel système | Corollaire du précédent, et vérifiable : seule la couche 1 consomme les syscalls, un codec n’en consomme aucun |
#![no_std] | Une transformation d’octets n’a besoin de rien d’autre que core. La contrainte est prouvée par le compilateur, pas déclarée |
| Déterministe | Mêmes octets ⇒ même résultat. Ni horloge, ni aléa, ni état global |
| Total : jamais de panique sur une entrée quelconque | Un codec lit des données externes, donc potentiellement hostiles. Il rend Result/Option, y compris sur des offsets aberrants |
| Fuzzé | Exigence déjà posée par CLAUDE.md pour toute API acceptant des données externes. Pour un codec, elle n’est pas négociable : c’est sa seule surface |
| 100 % de couverture lignes + branches | Règle de couche 1, et particulièrement atteignable ici : pas d’E/S à simuler |
Ce n’est pas une nouveauté doctrinale : c’est le motif sans-IO déjà retenu pour la pile réseau (ADR-042, ADR-043, ADR-079) — cœur pur fuzzable d’un côté, pilote d’E/S mince de l’autre — enfin nommé et rendu vérifiable.
D4 — Le périmètre de air-elf v1
Ce que la première version doit savoir lire, et rien de plus :
- l’en-tête ELF64 little-endian (
x86_64,aarch64— ADR-004) ; - les en-têtes de programme :
PT_LOAD,PT_INTERP,PT_DYNAMIC,PT_GNU_STACK; - le type de l’objet (
ET_EXEC,ET_DYN) et la présence d’un interprète ; - la table des sections et
.dynsym(ce quextasklit aujourd’hui) ; - la table dynamique :
DT_NEEDED,DT_SONAME.
De quoi répondre aux questions qui ont motivé l’ADR : cet exécutable est-il un static-PIE sans interprète ? cette bibliothèque porte-t-elle des versions de symboles ? que déclare-t-elle avoir besoin ?
Hors périmètre v1, et dit pour ne pas laisser croire : relocations, notes, DWARF, sections compressées, 32 bits, gros-boutiste. Ces derniers sont refusés, jamais devinés.
Conséquences
check-controleursapprend le rôlecodecet la règle qui va avec : une cratecodecne consomme aucun syscall et ne dépend d’aucuncontroleur. Le rôle est ajouté au vocabulaire ; une valeur hors vocabulaire échoue déjà aujourd’hui, ce qui garantit qu’aucune crate ne se déclareracodecavant que le gate ne le connaisse.xtask/src/elf.rsdisparaîtra au profit d’air-elfune fois celle-ci écrite : deux lecteurs du même format finiraient par diverger. La migration n’est pas immédiate — les contraintes diffèrent (l’un est un outil hôte, l’autre duno_stdfuzzé à 100 %) — mais elle est due, et ne pas la faire laisserait précisément le doublon que le projet refuse.air-bundlegagne de quoi vérifier le seuil d’exécution. C’est le but ; ce n’est pas fait par cet ADR, qui livre le codec, pas la politique. Ce queair-bundleexigera d’un exécutable octroyé — et ce qu’il fera d’un binaire étranger — relève d’un ADR à part.- Les crates qui se disent « codec » en commentaire sont candidates au reclassement. Cet
ADR ne les reclasse pas : le faire à l’aveugle réintroduirait une affirmation non vérifiée.
Chacune devra prouver qu’elle tient D3 — en particulier
#![no_std]et l’absence d’E/S.
Alternatives rejetées
- Réutiliser
xtask/src/elf.rstel quel en le promouvant. Rejeté : ce module a été écrit pour répondre à deux questions de gate, avec desStringet desVecpartout. Laisser un outil de développement dicter le contrat d’une crate de couche 1 est le mauvais sens du flux. - Ajouter la lecture ELF à
air-runtime. Rejeté :air-runtimelit sa propre image sous une contrainte de bootstrap absolue (avant relocation, sans TLS). Y mêler un lecteur de fichiers généraliste alourdirait le chemin le plus critique du système pour un besoin qui n’est pas le sien. - Prendre une crate externe (
goblin,object). Rejeté par la règle des 80 % (ADR-024) : Air n’utiliserait qu’une fraction de leur surface, et cette surface est précisément celle qui lit des données hostiles — là où le projet veut son propre code, fuzzé selon ses règles. - Ne rien faire et garder le lecteur en outillage. Rejeté par le besoin de sécurité : le
seuil d’exécution restera une intention tant qu’aucun composant livré ne saura distinguer un
static-PIE
linux-aird’un binaire glibc étranger.
Licence du document : MPL 2.0