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

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.rs et thread_local_storage.rs manipulent des structures ELF, mais pour un autre problème : appliquer ses propres relocations R_*_RELATIVE en l’absence de ld.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.

ExigencePourquoi
Aucune E/S — ni fichier, ni socket, ni descripteurC’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èmeCorollaire 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éterministeMêmes octets ⇒ même résultat. Ni horloge, ni aléa, ni état global
Total : jamais de panique sur une entrée quelconqueUn 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 + branchesRè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, aarch64ADR-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 que xtask lit 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-controleurs apprend le rôle codec et la règle qui va avec : une crate codec ne consomme aucun syscall et ne dépend d’aucun controleur. 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éclarera codec avant que le gate ne le connaisse.
  • xtask/src/elf.rs disparaîtra au profit d’air-elf une 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 du no_std fuzzé à 100 %) — mais elle est due, et ne pas la faire laisserait précisément le doublon que le projet refuse.
  • air-bundle gagne 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 que air-bundle exigera 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.rs tel quel en le promouvant. Rejeté : ce module a été écrit pour répondre à deux questions de gate, avec des String et des Vec partout. 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-runtime lit 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-air d’un binaire glibc étranger.

Licence du document : MPL 2.0