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)

  • 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).
  • 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’exec est 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 [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 écarté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.
  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++.