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-046 — La libc Air (Rust pur) : périmètre, layering couche-1-only, et doctrine d’exécution

Statut : Accepté (2026-06-27, validé par le BDFL). Document fondateur de la future libc. Companion d’ADR-045 (erreurs ABI C), d’ADR-019 (modèle d’erreurs hybride), d’ADR-021 (conventions couche 0 — EINTR, Option<T>, variantes modernes), d’ADR-004 (Linux tier-1), d’ADR-012 (stabilité ABI) et d’ADR-016 (i18n/icu4x). Réalise la vision « libc Rust Air ».

⚠️ PRÉCISIONS STRUCTURANTES (2026-06-30, ADR-053). Le ctype/locale/i18n de la libc s’appuie sur icu4x (caractères, casse, collation, normalisation, segmentation, calendriers). Deux points du § D7 ci-dessous deviennent OBSOLÈTES : « libm exclu de la v1 » (icu4x exige un libm no_std — calendriers + LSTM) et « hors wide-char lourd » (le wcs*/towctrans i18n-aware EST la surface adossée à icu4x). Voir ADR-053 (fait autorité sur le i18n de la libc) + l’audit docs/notes/audit-icu4x-rust-pur-fr.md.

Catégorie : Architecture (fondateur, transverse). Toute décision structurante ultérieure sur la libc passe par un RFC amendant cet ADR (ADR-015).

Contexte

Air veut fournir une libc en Rust pur pour combler les trous userland que le C ne peut pas couvrir automatiquement : une libc prévisible, sans surprise, qui « fait ce qu’elle dit ». L’objectif fonctionnel est de pouvoir compiler et exécuter des userlands C/C++ existants sur Air (cible pilote : OpenSSH), tout en apportant une sécurité accrue par construction (Rust) là où la libc C historique laisse des angles morts.

Il faut distinguer deux artefacts que le langage courant tend à confondre :

  • libair-base.so (crate air-base-capi, ADR-027/045, tranches T1→T5 mergées) : l’export en ABI C des types de la couche 1 (AirString, AirPath, AirLog, AirInstant, AirDateTime, identifiants…). C’est « rendre notre bibliothèque utilisable depuis C ». Ce n’est pas la libc.
  • La libc Air (le présent ADR) : l’ensemble de crates fournissant la surface POSIX-ish (open/read/printf/malloc/getaddrinfo…) en ABI C, contre laquelle on linke un programme C. Artefact distinct et bien plus vaste.

POSIX/glibc reposent sur des choix qu’Air refuse : errno globale implicite, conformité comme fin en soi, extensions tentaculaires. La couche 0 (attaque directe du kernel, ADR-021) et la couche 1 ont justement été bâties pour fonder cette libc sur nos propres mécanismes, pas sur glibc/musl.

Décision

D1 — Artefact distinct, en Rust pur

La libc Air est un nouvel ensemble de crates (hors air-base-capi), exposant la surface POSIX-ish en extern "C". Pas de C non maîtrisé dans son cœur : Rust comble les trous que le C ne couvre pas automatiquement (bornes, encodages, ownership).

D2 — Layering strict : la libc ne repose QUE sur la couche 1 (impératif)

La libc (et libair-base.so) ne dépendent QUE de la couche 1. JAMAIS d’accès direct à la couche 0 (air-sys-*). Aucun saut de couche.

Conséquence directe et contraignante pour la couche 1 : c’est à elle d’exposer tout ce dont la libc a besoin, y compris des passthroughs fidèles au kernel. Exemple canonique : un read qui remonte EINTR sans retry automatique (sémantique POSIX attendue) doit être atteignable via la couche 1 — et non en court-circuitant vers la couche 0. La couche 1 offre donc deux tiers : un tier kernel-faithful (fidèle, sans politique) et des helpers ergonomiques (retry, types possédés). La libc se lie au tier fidèle. Gardé en CI (la libc ne doit jamais lister air-sys-* en dépendance).

D3 — POSIX = objectif fonctionnel, pas conformité

On vise « les userlands cibles tournent », pas la certification POSIX. Les SHOULD de POSIX deviennent des MUST chez Air (comportement défini, jamais « implementation- defined » laissé au hasard). Là où la conformité exigerait une concession à la prévisibilité, la prévisibilité gagne.

D4 — errno thread-local, jamais global ; plomberie interne in-band

Notre plomberie interne reste in-band (erreurs dans la valeur de retour, cf. ADR-045) : zéro globale inventée par la libc. Mais POSIX exige errno (les programmes C le lisent) : la libc le fournit donc comme un errno thread-local — un shim de compatibilité POSIX posé en bordure, jamais un canal interne. errno global de la libc C historique = erreur de conception (effet de bord implicite, écrasable, non composable), refusée.

D5 — Kernel = bible, pas POSIX

Fidélité à la sémantique et à l’information du kernel Linux, re-présentées en types sûrs côté Rust. On ne cache rien, on ne remappe pas arbitrairement. Là où POSIX diverge de ce que fait le kernel, le kernel fait foi ; POSIX est l’objectif fonctionnel, pas l’autorité sémantique.

D6 — Exécution par provenance (bases en place ; activation post-v1)

⚠️ AMENDÉ le 2026-08-03 — voir la section « Amendement — D6 : la provenance décide la largeur d’enveloppe, pas l’exemption », en fin de document. Les deux premières puces ci-dessous sont caduques : il n’existe plus d’exécution non confinée, et un binaire sans octroi n’est pas confiné mais refusé. Le reste de D6 (analyse à l’exec, report de l’application complète) tient.

  • Un binaire compilé par rustc, signé, ne tirant NI glibc NI musl (donc reposant sur la libc Air) et s’appuyant sur les API Airexécution NON confinée (natif). CADUC (2026-08-03).
  • Tout autre binaire (compilé autrement, ou tirant une autre libc) → exécution CONFINÉE : d’abord via LXC, puis via nos conteneurs maison (mappings couche 0 cgroups/seccomp/landlock). CADUC (2026-08-03).
  • Chaque appel exec* déclenche, côté Rust et AVANT le spawn, une analyse de l’exécutable (ELF : DT_NEEDED/dépendances + signature comme ancre de confiance) qui décide natif vs confiné.
  • Périmètre v1 : seules les bases sont requises (couche 0 : cgroups/seccomp/landlock ; vérification de signature ; analyse ELF). L’enforcement complet à l’exec est différé post-v1 et fera l’objet d’un ADR sécurité dédié — c’est ADR-150 (« Manifeste, octroi et arbitrage de lancement »).

D7 — Périmètre et ordre de réalisation

Référence : libc classe musl (propre, sans le bloat glibc), libm exclu de la v1 ⚠️ OBSOLÈTE (ADR-053) : libm no_std est un PRÉREQUIS (icu4x : calendriers + LSTM).

  • Ordre de grandeur : ~25-30 familles, ~600-800 fonctions pour un cœur utile (POSIX base + C11, hors libm [libm requis, ADR-053], hors SysV IPC, hors wide-char lourd [le wcs* i18n-aware EST la surface icu4x, ADR-053]) ; une libc musl-complète va à ~1 400.
  • Cible pilote = compiler/linker OpenSSH : ~250-350 fonctions sur ~15 familles (I/O fichier, string/mem, réseau, process/signal, stdio, stdlib/malloc, credentials pwd/grp, termios/PTY, time, ctype, poll/select, dirent, mman, divers). « OpenSSH + ses deps » (OpenSSL rend pthread requis, zlib) ≈ 300-400.
  • Le vrai travail userland se concentre sur ~6-7 familles : stdio (FILE* bufferisé), malloc, printf/scanf (moteur de format), getaddrinfo/résolveur, termios/PTY, locale/ctype (→ adossé à icu4x, ADR-053). Le reste = wrappers fins sur la couche 1.
  • Déjà partiellement couvert : la majorité des syscall-wrappers (couche 0 exposée par couche 1) ; pwd/grp via les backends air.accounts.

D8 — Toolchain *-linux-air (quand la libc ≈ couvre la surface std)

  • Cible Rust *-linux-air : std reposant sur la libc Air (et non glibc/musl). Tant que la libc ne couvre pas assez de surface std, la cible n’a pas de sens à produire.
  • Toolchain clang C *-linux-air : on ne peut pas exclure des userlands C → un clang ciblant notre sysroot est nécessaire (un cargo/rustc seul ne compile pas du C). Un seul sysroot, deux front-ends (rustc + clang).
  • C++ séparable : supporter des userlands C++ n’impose pas un nouveau compilateur, mais une runtime C++ (libc++/libc++abi/libunwind) au-dessus de la libc Air. Décision différée, non bloquante pour le C.
  • Détails et prérequis : docs/notes/rustc-cible-linux-air-fr.md.

D9 — libm exclu de la v1 ⚠️ OBSOLÈTE (ADR-053)

Les fonctions mathématiques (libm) sont différées : hors périmètre de la première libc.

OBSOLÈTE depuis ADR-053 : la libc Air s’appuyant sur icu4x pour ctype/locale/i18n, un libm no_std devient un PRÉREQUISicu_calendar/calendrical_calculations (math calendaire, incontournable) et le LSTM d’icu_segmenter en dépendent. libm n’est donc plus exclu ; il est un maillon de base de la i18n Air (réécrit ou vendoré — question ouverte d’ADR-053). Cf. l’audit docs/notes/audit-icu4x-rust-pur-fr.md.

Conséquences

  • La couche 1 doit grandir pour exposer des passthroughs fidèles au kernel (D2) là où elle n’offre aujourd’hui que des helpers ergonomiques. Chaque famille libc à venir révélera les primitives manquantes côté couche 1.
  • Sécurité par construction : Rust rend structurelle la garantie « la libc fait ce qu’elle dit » (vs. garantie manuelle en C). La documentation (ADR-027 : pourquoi, alternatives, pièges) en est le pendant obligatoire — ne rien laisser au hasard, ne rien survendre.
  • Dépendances : règle des 80 % (ADR-024) et licences (deny.toml) s’appliquent ; tout code C tiers nécessaire est intégré/maîtrisé, pas tiré aveuglément.
  • Reproductibilité (ADR-025) et ABI (ADR-012) s’appliquent à la libc comme au reste.

Alternatives rejetées

  • Porter glibc ou musl : rejeté. C non maîtrisé au cœur, errno global, conformité-as-fin ; contraire à la raison d’être (sécurité userland par Rust).
  • errno global (modèle libc C historique) : rejeté (D4) — effet de bord implicite, écrasable, non composable.
  • Faire reposer les wrappers fins directement sur la couche 0 (proposé puis rejeté par le BDFL, D2) : casse le layering ; la fidélité kernel passe désormais par un tier dédié de la couche 1.
  • Viser la conformité POSIX certifiée : rejeté (D3) — coût/concessions sans bénéfice pour les cibles d’Air.
  • relibc / eyra / c-scape (libc Rust existantes) : veille maintenue, mais le créneau « libc Rust pur, mécanismes Air, exécution par provenance » reste vacant ; on construit le nôtre.

Questions ouvertes (différées)

  1. Enforcement exact à l’exec (format de signature, politique natif/confiné fine, conteneurs maison) → ADR sécurité post-v1. (Reformulée par l’amendement du 2026-08-03 : la « politique natif/confiné » n’existe plus ; ce qui reste à trancher est la largeur des enveloppes et la forme de l’arbitre.)
  2. libm : quand et comment (port vs. Rust pur).
  3. Périmètre fonction-par-fonction : à figer famille par famille au fil de la réalisation (un sous-ADR ou une annexe vivante par famille).
  4. Runtime C++ (libc++ & co) : si/quand on supporte les userlands C++.

Amendement — D6 : la provenance décide la largeur d’enveloppe, pas l’exemption (2026-08-03)

Décidé par le BDFL le 2026-08-03, au cours de l’étude du chemin vers un système Air amorçable (docs/notes/etude-chemin-vers-systeme-air-fr.md). Deux décisions de doctrine et deux corrections de fait. Cet amendement ne déplace pas la frontière fixée par D6 lui-même : l’application complète reste du ressort de l’ADR sécurité dédié que D6 annonçait : ADR-150, qui fixe le manifeste, l’octroi, la procédure d’arbitrage et les deux portes.

A1 — Il n’existe plus d’exécution non confinée

D6 exemptait de confinement le binaire de bonne provenance. Cette exemption est supprimée.

Invariant. La provenance ne décide plus si un exécutable est confiné, mais quelle largeur d’enveloppe il reçoit. Aucun chemin d’exécution ne se fait sans profil — y compris pour un binaire signé par Air, y compris pour un service du système.

Ce que la doctrine perd — « bonne provenance donc libre » — elle le regagne en cohérence : un mécanisme de sécurité qui s’efface devant les siens ne protège pas le système, il protège les autres du système. Le confinement par défaut est minimal et peu permissif ; l’enveloppe s’élargit sur octroi, jamais par appartenance.

Une exception, et une seule, est reconnue : PID 1. Le confinement seccomp et Landlock est monotone — il s’hérite à travers execve et ne peut que se restreindre. Confiner PID 1 plafonnerait donc irrémédiablement tout ce que la machine lancera ensuite. PID 1 tire sa protection de ce qu’il est minimal et vérifié, non de ce qu’il serait mis en cage. C’est une propriété du noyau, pas un privilège accordé.

A2 — Refus par défaut, exécution confinée sur octroi explicite

D6 faisait tourner automatiquement tout binaire étranger, simplement mis en cage. C’est désormais l’inverse : l’absence d’octroi n’est pas une mise en cage, c’est un refus. Un exécutable sans octroi peut être présent sur le disque ; il n’est en aucun cas exécutable.

Trois états remplacent les deux de D6 :

ÉtatConditionEffet
RefuséAucun octroi valideexec refusé. L’artefact reste sur le disque, inerte
Confiné étroitOctroi accordé, provenance étrangèreExécution sous le profil le plus restrictif compatible avec l’octroi
Confiné selon enveloppeOctroi accordé, signature AirExécution sous l’enveloppe accordée — jamais sans profil (A1)

Le vocabulaire est celui déjà retenu ailleurs : l’application demande (manifeste, signé par le développeur), le système octroie (grant, signé par la machine). Ce sont deux artefacts distincts, et les confondre est l’erreur qu’Android a commise avant sa version 6.

Conséquence sur le chemin d’amorçage. C’est cette formulation — et non un refus pur — qui garde ouverte l’image hybride : un système Air qui démarre avec ses propres composants tout en s’appuyant transitoirement sur des binaires d’origine étrangère. Le mode « étranger, donc confiné » devient de première classe précisément parce qu’il n’est atteignable que par un octroi explicite, donc énumérable et auditable. Un refus pur fermerait ce chemin et rendrait l’amorçage impossible.

A3 — Correction de fait : LXC est rayé

D6 prévoyait le confinement « d’abord via LXC, puis via nos conteneurs maison ». Le mécanisme maison existe : ADR-112air-sandbox : confinement seccomp/Landlock sûr, compilateur BPF maison, profils fail-closed ») est mis en œuvre. LXC n’est pas déprécié, il est sans objet.

A4 — Correction de fait : l’édition de liens est une politique d’admission, pas un mécanisme d’application

D6 érige « ne tirant NI glibc NI musl » en critère de sécurité. Ce critère ne fait pas respecter ce que D6 lui demande. Lier la libc Air n’empêche rien : un binaire Rust peut émettre une instruction de syscall directement, et la libc n’est pas franchie. La libc n’est pas une frontière de sécurité. Ce qui contraint, c’est seccomp et Landlock ; ce qui ancre la confiance, c’est la signature.

L’exigence est donc conservée, et requalifiée : elle reste une politique d’admission — ce que le gestionnaire de paquets accepte d’installer, ce qu’une chaîne de compilation Air produit — et elle a sa raison d’être, qui est de ne pas faire entrer dans le système des programmes conçus pour atteindre le noyau sans passer par les surfaces d’Air. Mais elle n’applique rien par elle-même, et l’ADR ne doit pas laisser croire le contraire. Le standard de transparence du projet — « Air fait ce qu’il dit » — l’interdit.

Ce que cet amendement ne tranche pas

Il grave des invariants, pas un mécanisme. Restent ouverts, et relèvent de l’ADR sécurité dédié annoncé par D6 :

  • la forme de l’arbitre de lancement, et l’étage où il vit ;
  • le format du manifeste et celui de l’octroi, et leur cycle de vie — sachant que la mise à jour d’une application est vue par Air comme une désinstallation suivie d’une installation, donc une révocation de tout ce qui avait été octroyé, seules les données survivant ;
  • la largeur des enveloppes par défaut, qui est le vrai travail de conception : trop étroites, l’utilisateur se heurte à des murs ; trop larges, elles ne protègent rien ;
  • le statut du gestionnaire de paquets lui-même, qui installe des applications et des services non système, et doit rester cantonné à ce rôle — l’installation du système étant un processus distinct.

Cet ADR dédié bloque AirSandboxManager (ADR-112, ADR-077) : poser la surface médiatrice du confinement avant d’avoir tranché ces points reviendrait à les trancher par défaut.