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-154 — Le shell d’Air : un interprète interactif, pas un langage — et pas POSIX

Statut : Accepté (2026-08-11, BDFL) sur le refus de POSIX ; le reste Proposé. Prolonge ADR-153 — qui fait de l’octroi le décideur du programme de session — et sert le seuil d’exécution d’ ADR-046 D6.

Catégorie : Fondateur produit + sécurité. Décide ce qu’Air apporte comme shell et, surtout, ce qu’il n’apporte pas.

Contexte

ADR-153 rend le programme de session octroyé au lieu d’être hérité du champ 7. Reste la question qu’elle laisse entière : quel programme y met-on ?

Aujourd’hui c’est /bin/bash ou /bin/sh de la distribution hôte — un binaire glibc que seul le profil TransitionalGlibc fait vivre. Le portage du shell est donc le dernier verrou du seuil d’exécution : tant qu’il n’est pas fait, la porte la plus empruntée d’Air est celle qui reste ouverte.

Écrire un shell soulève une objection que le BDFL a formulée d’emblée : ne serions-nous pas de fait une surface d’attaque pour notre propre système ? Cet ADR existe pour y répondre par une décision, pas par une intention.

Ce qu’est un shell, et où se loge exactement le danger

Un shell est deux choses :

  1. un interprète interactif : on nomme des programmes, on les enchaîne, on redirige ;
  2. un langage de programmation : variables, expansion, contrôle de flux, fonctions.

Toute la sinistralité historique vient de la seconde, et plus précisément d’une seule propriété : le shell ré-analyse des données comme du code.

MécanismeCe qu’il fait d’une donnée
word splittingune variable non protégée devient plusieurs arguments
globbing impliciteune donnée devient une liste de fichiers
substitution de commandeune donnée devient du code exécuté
evalune chaîne devient un programme
IFSles règles d’analyse se changent depuis les données
ShellShock (CVE-2014-6271)une fonction voyage dans une variable d’environnement

C’est mot pour mot ce que le Principe 11 nomme : la tolérance est la faille. Le shell POSIX est l’archétype du parseur tolérant placé sur un chemin de décision.

Décision

D1 — Le shell d’Air n’est pas compatible POSIX, et c’est la décision

Non pas « pas encore », ni « à l’exception de » : pas.

Le raisonnement tient en une phrase : l’insécurité est dans la spécification, pas dans les implémentations. Réécrire bash en Rust donnerait un bash mémoire-sûr portant exactement les mêmes injections, parce que le word splitting et eval sont ce que la spécification exige. On ne peut pas être compatible POSIX et sûr.

Air a déjà tranché cette famille de question : un binaire étranger ne s’exécute pas (ADR-046 D6). Dire « un script shell étranger ne s’exécute pas » est la même doctrine, un cran au-dessus — et elle est plus défendable que la compatibilité, parce qu’elle est énonçable, vérifiable et datable.

« POSIX : trop flou, trop ambigu, laissant trop de portes ouvertes » (BDFL, 2026-08-11).

D2 — Une expansion ne produit jamais de nouveaux mots

La règle structurante, et à elle seule elle supprime la classe d’injection. Une variable vaut un argument, toujours, quel que soit son contenu — espaces, retours à la ligne, métacaractères compris. Il n’existe pas d’IFS.

Conséquence directe : rm $fichier est sûr par construction. Il n’y a rien à protéger, donc rien à oublier de protéger — et c’est là le vrai gain, car l’échec des shells n’est pas qu’ils soient dangereux, c’est qu’ils exigent une vigilance parfaite à chaque usage.

Le globbing n’est pas implicite : il est une opération nommée, jamais un effet de bord de l’expansion.

D3 — Ni eval, ni substitution de commande en position de code

La substitution existe comme valeur : la sortie d’une commande peut devenir un argument. Elle n’est jamais réinjectée en texte dans la ligne avant analyse. Autrement dit, la substitution a lieu après l’analyse syntaxique, dans un emplacement de valeur — jamais avant, dans un emplacement de code.

eval n’existe pas. Un shell qui peut fabriquer du code depuis des données rend toute analyse statique impossible, y compris la nôtre.

D4 — Incrément 1 : un interprète sans langage

Ce qu’il porte : nommer et lancer des programmes, pipelines, redirections, contrôle de tâches, historique, complétion, un jeu de commandes internes, des variables comme valeurs.

Ce qu’il ne porte pas : fichiers de script, fonctions, eval, contrôle de flux.

Ce périmètre couvre l’administration, qui est le besoin réel et immédiat : ouvrir une session, regarder, lancer, corriger. Il permet de retirer le profil transitoire de la session avant d’avoir tranché la question du langage.

D5 — Le langage de script est différé, et devra être justifié par un reste mesuré

On ne construit pas le langage avant de savoir ce qu’il resterait à faire. Sur un Unix, le script shell sert principalement à l’orchestration — or Air la fait déjà autrement, et déclarativement : manifestes et octrois (ADR-010, ADR-150), services et dépendances (air-launchd).

La question à instruire n’est donc pas « quel langage ? » mais « que reste-t-il, une fois l’orchestration déclarative retirée ? ». Si le reste est de la glu utilisateur, il pourra appeler un langage ; si le reste est vide, on aura évité d’écrire une surface d’attaque pour rien.

D6 — Valeurs structurées à l’intérieur, texte à la frontière

Entre les commandes internes du shell, un pipeline transporte des valeurs typées, pas du texte à ré-analyser — c’est l’application directe d’« entrée stricte, sortie lisible » : on ne demande pas à awk de redécouper ce qu’un producteur savait déjà structuré.

Le texte reste le format d’interopérabilité à la frontière, avec les programmes qui n’en connaissent pas d’autre. La frontière est explicite, et c’est elle qu’on rend visible plutôt que de la laisser partout.

D7 — Le shell n’est pas privilégié, et c’est la vraie réponse à l’objection

Sur un Unix, un shell hérite de tout ce que l’utilisateur peut faire : son expressivité est dangereuse parce que rien ne la borne.

Sur Air, un shell est un programme sous octroi comme un autre (ADR-153). Même expressif, il ne peut exercer que ce que l’enveloppe de la session permet — pas un descripteur, pas un port, pas un chemin de plus.

D’où la reformulation de l’objection du BDFL : la question n’est pas « le langage nous ouvre-t-il une surface ? » mais « qu’accorde-t-on à une session ? ». La première n’a pas de réponse stable ; la seconde est exactement ce qu’ADR-153 vient de rendre explicite et signable. Le confinement ne vient pas de la pauvreté du langage : il vient de la capability.

C’est ce qu’aucun Unix traditionnel ne peut dire, et c’est ce qui rend l’entreprise raisonnable.

Conséquences

Les scripts shell existants ne s’exécutent pas. C’est un coût réel, et il faut le nommer : scripts de paquets, recettes d’intégration continue, procédures d’exploitation écrites depuis trente ans. Le refus de POSIX est une décision de rupture, pas une économie.

air-sshd lance aujourd’hui <shell> -c "commande". Cette forme suppose un shell qui interprète une chaîne — donc précisément ce que D2/D3 encadrent. Le contrat exact de -c (ou de son remplaçant) est à écrire avec l’incrément 1.

Le profil TransitionalGlibc a une date de sortie possible : le jour où la liste admise d’ADR-153 ne contient plus que le shell d’Air. Aujourd’hui cette date n’existe pas ; cet ADR la rend atteignable.

Il faudra un chemin de transition pour le reste de scripts légacy — un sh glibc restant admis, nommé et daté, comme le profil transitoire l’a été.

Alternatives rejetées

  • Porter un shell existant (dash, mksh) sur la libc d’Air. La moins chère, et la pire : elle importe la spécification avec le code. On aurait un parseur tolérant de plus, recompilé.
  • Réécrire bash fidèlement en Rust. Un bash mémoire-sûr reste un bash : les injections ne viennent pas de la mémoire.
  • Adopter nushell tel quel. Inspiration réelle et assumée — valeurs structurées, refus du re-découpage textuel —, mais dépendance externe massive, très loin de la règle des 80 % (ADR-024), et périmètre fonctionnel bien plus large que ce dont Air a besoin.
  • Pas de shell du tout. L’administration deviendrait impossible, et l’exploitant contournerait — ce qui est toujours pire que de fournir l’outil.
  • Un shell POSIX confiné très étroitement. Rejeté : la cage borne ce qu’un programme fait, pas ce que son interprète comprend. Une injection dans un shell confiné reste une injection, à l’intérieur de l’enveloppe.

Questions ouvertes

  1. Le nom. TRANCHÉE le 2026-08-11 (BDFL) : un nom propre.

    Le raisonnement : un nom en sh promet un sh. Qui tape airsh s’attend à IFS, à eval, au découpage — et conclura à un shell incomplet plutôt qu’à un shell différent. Or D1 n’est pas une incomplétude, c’est un refus. Un nom qui encode une propriété technique (airsafe, airq) vieillit mal et demande une explication ; un nom propre ne promet rien, donc ne ment pas. C’est ce qui a permis à fish et à nu de diverger sans être lus comme des sh ratés.

    Retenu : alize — l’alizé, le vent régulier qui a porté la navigation pendant des siècles. Il tient à « Air » sans jeu de mots, il évoque la constance plutôt que la puissance, et il n’entre en collision avec aucun outil connu du domaine.

    L’exécutable s’écrit alize, sans accent : un chemin non-ASCII dans un octroi, dans un /etc/passwd et dans un message d’erreur est une source d’ennuis sans contrepartie. La prose écrit « Alizé », le disque écrit alize.

    (Écartés en chemin : zephyr — collision avec Zephyr RTOS, dans le même domaine ; mistral — collision avec Mistral AI, dans le même pays ; brise — « brise » est aussi la troisième personne de « briser », mauvaise promesse pour un shell ; bora — un vent de chute violent, mauvaise évocation.)

  2. L’interprète interactif a-t-il besoin de contrôle de flux ? Un for tapé à la main est commode et n’est pas un fichier de script. La frontière D4 est peut-être à mettre là plutôt qu’à la porte du langage.

  3. Le contrat de -c pour air-sshd. TRANCHÉE le 2026-08-11 (BDFL) : une ligne interprétée (option B), pour l’interopérabilité.

    Le discriminant n’était pas la sûreté — D2 la fournit dans les deux cas, puisqu’aucune expansion ne produit de nouveaux mots. Il était ce que ssh hôte "…" doit signifier sur Air.

    Le fait qui emporte la décision : git, rsync et scp dépendent d’une ligne composée (cd /x && tar cf - .). Une commande déjà découpée les couperait — donc couperait l’interopérabilité SSH qu’air-sshd a passé une vague entière à construire. Le coût de l’option A était bien plus large que « quelques usages composés ».

    Ce que cela engage : il subsiste un interprète sur le chemin d’entrée d’une exécution distante. Il est borné par trois choses, et il faut qu’elles tiennent ensemble — D2 (une expansion ne produit jamais de mots), D3 (ni eval, ni substitution en position de code), et l’octroi de session d’ADR-153, qui décide par quel programme cette ligne est interprétée. Aucune des trois n’est facultative.

  4. Le reste de scripts légacy : combien, et pour quoi ? À mesurer avant de décider de la forme du chemin de transition — la même méthode que pour le profil transitoire, où strace -c a remplacé les suppositions.

  5. La complétion : peut-elle interroger les managers de domaine plutôt que d’analyser du texte de sortie ? Ce serait D6 appliqué à l’interface.


Licence du document : MPL 2.0