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
libmno_std — calendriers + LSTM) et « hors wide-char lourd » (lewcs*/towctransi18n-aware EST la surface adossée à icu4x). Voir ADR-053 (fait autorité sur le i18n de la libc) + l’auditdocs/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(crateair-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)
- Un binaire compilé par rustc, signé, ne tirant NI glibc NI musl (donc reposant sur la libc Air) et s’appuyant sur les API Air → exécution NON confinée (natif).
- 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).
- 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’
execest différé post-v1 et fera l’objet d’un ADR sécurité dédié.
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[lewcs*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 rendpthreadrequis, 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/grpvia les backendsair.accounts.
D8 — Toolchain *-linux-air (quand la libc ≈ couvre la surface std)
- Cible Rust
*-linux-air:stdreposant sur la libc Air (et non glibc/musl). Tant que la libc ne couvre pas assez de surfacestd, 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 (uncargo/rustcseul 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)
libm exclu de la v1Les 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ÉREQUIS — icu_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 écartées
- Porter glibc ou musl : rejeté. C non maîtrisé au cœur,
errnoglobal, conformité-as-fin ; contraire à la raison d’être (sécurité userland par Rust). errnoglobal (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)
- Enforcement exact à l’
exec(format de signature, politique natif/confiné fine, conteneurs maison) → ADR sécurité post-v1. libm: quand et comment (port vs. Rust pur).- 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).
- Runtime C++ (
libc++& co) : si/quand on supporte les userlands C++.