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-165 — Les formats de fichiers système sont des codecs sans I/O

Statut : Proposé (2026-08-23). Décide que tout format de fichier système qu’Air lit ou écrit — /etc, /proc, /sys — vit dans un codec pur, sans entrée/sortie, couvert à 100 % et fuzzé. Gèle la liste des formats de la première tranche.

Catégorie : Architecture transverse, anneau 1. Généralise ADR-091 (motif réseau sans-IO) aux formats de fichiers. Décline le Principe 11 (entrée stricte) et le Principe 3 (zéro présomption).

Contexte

Ce n’est pas une idée neuve : c’est ADR-091 appliqué ailleurs

ADR-091 a rendu normatif le motif sans-IO pour le réseau : un cœur pur — framer, codec, machine à états — qu’on peut fuzzer et couvrir intégralement, et un pilote d’I/O mince par-dessus. L’argument tenait en une phrase : on ne teste exhaustivement que ce qui ne touche à rien.

Le même argument vaut mot pour mot pour un format de fichier. Un décodeur de /etc/passwd qui ouvre lui-même le fichier ne se teste qu’avec un système de fichiers ; séparé, il se teste avec un tableau d’octets — et se fuzze.

Cet ADR ne décide donc pas un motif : il constate que celui d’ADR-091 s’applique, et l’étend.

Ce que le dépôt fait aujourd’hui

La duplication est mesurée, au 2026-08-23 :

FormatCrates qui le touchent
passwd15
shadow9
group6
hosts6
/proc/…/stat5

Quinze crates autour de /etc/passwd. Toutes ne le parsent pas — certaines ne font que le nommer —, mais au moins sept en portent une lecture : air-account, air-account-cli, air-account-schema, air-admin-gate, air-config, air-libc-pwd, air-sshd.

Chaque duplication est un endroit où deux lecteurs peuvent diverger — et le Principe 11 dit précisément que c’est là qu’est la faille.

Décisions

D1 — Un format, un codec, sans I/O

Tout format de fichier système qu’Air lit ou écrit est un codec pur : il prend des octets et rend une valeur typée, ou l’inverse. Il n’ouvre rien, ne lit rien, n’écrit rien.

Corollaire non négociable, et c’est la raison d’être de la règle : 100 % de couverture (lignes et branches) et une cible de fuzzing par codec. Un codec qui ne peut pas être fuzzé n’a pas été correctement séparé de son I/O.

D2 — Un seul crate, un module par format — jamais par répertoire

Le regroupement se fait par grammaire, pas par emplacement.

La raison est concrète : /proc/mounts et /etc/fstab partagent la même grammaire. Découper par répertoire les mettrait dans deux crates et dupliquerait le codec — c’est- à-dire referait exactement le défaut que cet ADR corrige.

Deux autres constats achèvent d’écarter le découpage par répertoire :

  • /dev n’a aucun format texte. Ce sont des nœuds de périphérique et des structures d’ioctl, qui relèvent des types de couche 0. Le crate serait vide.

  • /sys est pour moitié un scalaire par fichier, et /proc est hétérogène — du scalaire (/proc/sys/*) à la ligne de cinquante-deux champs (/proc/self/stat).

    (Rectifié le 2026-08-24. Cette ligne disait « presque partout un scalaire par fichier ». La mesure — 27 848 attributs lus sur les 46 288 parcourus d’un /sys réel — dit 12 865 scalaires sur 26 188 attributs texte, contre 8 462 chaînes libres, 2 298 multi-lignes et 1 294 listes. La décision n’en est pas affaiblie : elle en est renforcée, puisque c’est justement l’hétérogénéité qui interdit un crate par répertoire. Mais la phrase était plus optimiste que le réel, et elle n’avait jamais été confrontée.)

Le crate porte donc, en tête de sa documentation, une table chemin → module : on garde l’évidence de « où chercher », sans payer la duplication.

D3 — La tolérance de chaque format est une décision, pas un détail d’implémentation

C’est le point qui mordra, et il ne se délègue pas.

Le Principe 11 proscrit le parseur tolérant. Mais un codec qui refuserait un /etc/fstab réel — avec ses commentaires, ses lignes vides, ses espaces de fin — serait inutilisable, et son refus ferait chercher une porte plus simple.

Pour chaque format, cet ADR (ou son amendement) dit ce que le codec accepte et ce qu’il refuse. L’implémenteur ne tranche pas : il applique.

La règle générale, dont chaque format peut déroger explicitement :

Posture par défaut
Commentaires et lignes videsacceptés là où le format les définit, ignorés
Espaces de tête ou de finacceptés là où le format les définit comme séparateurs
Champ manquantrefusé — jamais de valeur par défaut inventée
Champ surnumérairerefusé — c’est le signe d’une autre grammaire
Encodageoctets, jamais d’UTF-8 présumé (Principe 3)
Nombre hors bornesrefusé, jamais tronqué

D4 — Le codec rend des octets, pas des chaînes

Un nom d’utilisateur, un point de montage, un chemin de /proc peuvent contenir des octets non-UTF-8 : Unix l’autorise. Un codec qui présumerait de l’UTF-8 perdrait de la donnée valide, ou paniquerait sur une entrée légitime.

Les codecs travaillent donc sur &[u8] et rendent des vues d’octets ; la conversion en texte est une décision d’appelant, jamais du codec.

D5 — Ce qui existe déjà migre, il ne coexiste pas

Les lectures dispersées dans air-account, air-config, air-socket, air-device et air-system rejoignent le crate. Elles ne restent pas en route parallèle : deux codecs du même format sont deux lecteurs qui divergeront.

Le décodeur de scalaire /proc/sys écrit le 2026-08-23 dans air-system::parameters en fait partie — il est arrivé avant cet ADR, il en relève.

La première tranche — liste gelée

Onze formats. Pour chacun : la référence qui fait autorité — un codec qui invente sa grammaire est pire que pas de codec — et la posture de tolérance quand elle déroge au défaut de D3.

#FormatCheminsRéférenceDérogation à D3
1passwd/etc/passwdpasswd(5)champ gecos : contenu opaque, jamais découpé
2shadow/etc/shadowshadow(5)champs d’âge vides admis (sens : « non fixé »)
3group/etc/groupgroup(5)liste de membres vide admise ; membre vide dans une liste non vide accepté et ignoré (cf. amendement)
4fstab/etc/fstabfstab(5)commentaires #, lignes vides ; 4 champs admis (les deux derniers valent 0)
5mtab / mounts/etc/mtab, /proc/mounts, /proc/self/mountsfstab(5), proc(5)même codec que 4 — c’est la raison d’être de D2
6hosts/etc/hostshosts(5)commentaires, alias multiples
7resolv.conf/etc/resolv.confresolv.conf(5)directives inconnues ignorées — le format est extensible par conception
8scalaire sysctl/proc/sys/**proc(5), sysctl(8)aucune — entier décimal signé, un \n final au plus
9meminfo/proc/meminfoproc(5)clés inconnues ignorées ; unité kB obligatoire quand présente
10proc stat/proc/[pid]/statproc(5)le champ comm est entre parenthèses et peut en contenir — le piège classique
11attribut sysfs/sys/**sysfs(5)scalaire ou chaîne d’octets ; pas de grammaire propre

Le format 10 mérite son avertissement. /proc/[pid]/stat est le format qui casse naïvement tous les analyseurs : le second champ est le nom de l’exécutable entre parenthèses, et ce nom peut contenir des parenthèses et des espaces. Découper sur l’espace donne un décalage silencieux de tous les champs suivants. Le codec doit trouver la dernière parenthèse fermante, pas la première.

Le format 11 n’a pas de grammaire, et la mesure va plus loin que la page de manuel. sysfs(5) énonce des garanties que /sys ne tient pas. Mesuré le 2026-08-24 : 27 848 attributs lus sur les 46 288 parcourus d’une machine réelle.

Ce que la référence laisse croireCe que la mesure dit
« should be ASCII text files »487 binaires, dont 482 portant des NUL
tient dans une page (PAGE_SIZE)74 dépassent 4 096 o ; btf/vmlinux fait 7 114 264 o
se termine par \n66 attributs texte n’en ont pas
une valeur par fichier2 298 sont multi-lignes

PAGE_SIZE borne ce que show() écrit, pas ce qu’une lecture rend — la confusion est facile et coûteuse. Et la page se termine elle-même sur « This manual page is incomplete, possibly inaccurate ».

Trois exemples achèvent d’écarter toute devinette, tous relevés sur des fichiers réels :

  1. …/cpu0/topology/core_cpus vaut 5\n et son voisin immédiat core_cpus_list vaut 0,2\nle même ensemble {0, 2}, l’un en masque, l’autre en liste. Et 5\n est cinq dans des milliers d’autres attributs.
  2. \n seul est la liste vide dans …/cpu/offline et la chaîne vide dans uevent_helper.
  3. Le booléen a treize écritures mesurées, dont deux n’en sont pas : sched_ext/state = disabled est une énumération, et le contraire de power/control = on est auto, pas off.

Conséquence normative : le codec du format 11 n’est pas un décodeur. C’est un objet inerte plus des décodeurs typés que l’appelant choisit, chacun calqué sur la fonction du noyau qui a produit la valeur — deux producteurs de booléens donnent deux décodeurs. Aucun repli d’un décodeur sur un autre : un codec qui devine est exactement la faille que le Principe 11 proscrit.

Amendement — le membre vide d’une liste group(5) (2026-08-24)

Origine. La session déléquée aux trois premiers codecs s’est arrêtée devant un cas que la première rédaction ne tranchait pas, et l’a signalé plutôt que de décider seule — ce que D3 lui demandait explicitement :

a,,b et a, — un membre vide dans une liste de membres non vide. Refuser par lecture stricte de D3 (« champ manquant → refusé ») ? La glibc, elle, accepte.

D3-bis — un élément vide de liste est accepté et ignoré

Dans la liste de membres de group(5), un élément vide est ignoré. La ligne est acceptée ; le groupe rend les membres non vides.

Trois raisons, et la troisième est décisive.

  1. Une liste n’est pas un champ. D3 refuse le champ manquant parce qu’un champ absent oblige à inventer une valeur par défaut. Un élément vide dans une liste n’oblige à rien : c’est un artefact de séparateur, pas une donnée absente.
  2. Un nom d’utilisateur vide est irreprésentable. L’ignorer ne perd donc aucune information — au contraire, le conserver produirait un membre "" qui ne désigne personne et que chaque appelant devrait filtrer.
  3. Refuser rendrait tout le fichier illisible pour une virgule. Un /etc/group portant une virgule finale existe en vrai — un usermod bâclé, une édition à la main. Air refuserait alors une connexion à cause d’une faute de frappe qui ne cache aucune ambiguïté. C’est exactement le cas que D3 prévoit dans son propre exposé : « un codec qui refuserait un /etc/fstab réel serait inutilisable, et son refus ferait chercher une porte plus simple. »

Ce que cette dérogation n’ouvre pas. Elle vaut pour un élément de liste, et pour lui seul. Un champ vide de passwd(5) ou de group(5) — un gid absent, par exemple — reste refusé : là, il faudrait inventer.

Ce que cet ADR ne décide pas

  • Le nom du crate ni son découpage en modules — affaire d’implémentation, tant que D2 tient.
  • Les formats au-delà de la première tranche. cpuinfo, /proc/[pid]/maps, /proc/net/*, les unités systemd viendront par amendement, chacun avec sa référence et sa posture.
  • L’écriture de tous les formats. Certains ne sont lus que par Air (/proc, /sys en lecture) ; l’exigence d’un encodeur se décide format par format.
  • Le sort des lectures existantes au-delà de D5 : quelle crate migre quand relève de la feuille de route, pas d’une décision d’architecture.

Conséquences

  • Un travail délégable. Un codec sans I/O n’a aucune dépendance sortante : sa production n’entre en conflit avec aucun autre chantier, et se spécifie intégralement par cette liste.
  • Les managers en profitent immédiatement : AirAccountManager, AirConfigManager et air-device cessent chacun de porter leur lecture.
  • Une dette de migration, à faire une fois : sept crates au moins portent aujourd’hui une lecture de passwd.
  • Le crate est couche 1 et sans dépendance hors du cœur : c’est ce qui rend le 100 % et le fuzzing atteignables.

Alternatives rejetées

  • Un crate par répertoire (/proc, /sys, /dev, /etc). Sépare fstab de mounts qui partagent leur grammaire — donc duplique le codec, le défaut même qu’on corrige — et crée un crate vide pour /dev.
  • Laisser chaque crate parser ce dont elle a besoin. C’est l’état actuel : quinze crates autour de passwd, et autant d’endroits où deux lecteurs peuvent diverger.
  • Des codecs qui ouvrent leurs fichiers. Rend le 100 % et le fuzzing inatteignables, et contredit ADR-091 qu’on se contente ici d’étendre.
  • Rendre des String. Perd les noms non-UTF-8, que Unix autorise (D4).

Licence du document : MPL 2.0