ADR-047 — Principes de construction de la libc Air
Statut : Accepté (2026-06-27, validé par le BDFL). Companion d’ADR-046
(libc Air — fondateur) : ADR-046 pose le cadre (artefact, layering, doctrine), le présent
ADR pose la méthode de construction et les invariants non négociables. Décline les
Principes d’ingénierie 2 (arithmétique défensive), 3 (chaînes/buffers/encodages) et 5
(optimiser après mesure), la discipline // SAFETY: d’ADR-021,
et le standard de doc d’ADR-027.
Catégorie : Architecture / Méthode (transverse à toute la libc). Toute évolution passe par un RFC (ADR-015).
Contexte
La libc Air est du code userland critique : tout programme C/C++ exécuté sur Air en dépend. ADR-046 a décidé qu’on la bâtit en Rust pur, sur la couche 1 uniquement, en s’inspirant de musl. Cet ADR fixe comment on s’en inspire, sous quel regard, et les garanties qu’on ne transige jamais. Le regard est SÉCURITÉ / PROTECTION, jamais la performance d’abord.
Décision
P1 — musl = périmètre (surface), pas implémentation
On prend l’inventaire des symboles exportés de musl — épinglé à une version précise — comme liste cible de ce que la libc doit fournir. Implémentation en Rust pur, selon nos principes. Les signatures et la sémantique restent dictées par les standards (The Open Group / POSIX + ISO C + Linux man-pages section 2 + kernel/UAPI, « kernel = bible » d’ADR-046 D5). musl = le quoi, les standards = le contrat, nos principes = la manière.
P2 — Audit de musl par famille, à double tranchant, artefact vivant
Avant d’implémenter une famille, on audite la famille correspondante de musl. Double but : (i) repérer ce qui est bien fait → s’en inspirer ; (ii) être critique → noter les faiblesses. L’audit inventorie, par famille :
- les structures internes ;
- les variables globales publiques et privées/opaques ;
- comment l’accès aux ressources partagées est protégé en concurrence/multi-thread ;
- la proportion d’assembleur, où, et pourquoi.
L’audit est un artefact versionné (un document par famille, gabarit en annexe A), épinglé à la version de musl auditée (reproductibilité de la référence). On audite par famille, juste avant de l’implémenter (pas tout d’un bloc — l’audit vieillirait).
P3 — Regard sécurité, jamais performance d’abord
On audite et on code sous l’angle protection / sécurité / accès partagé, en permanence. On préfère du code supplémentaire — impact performance assumé — qui garantit qu’aucun cas critique ne survient, plutôt que l’optimisation. Aucune optimisation n’est entreprise avant une implémentation robuste de TOUTE la librairie, tout en tête (Principe 5). L’optimisation, si elle a lieu, vient après, mesurée et justifiée.
P4 — Les quatre modes de défaillance (invariants vérifiables)
« Ne jamais être pris en échec » se vérifie fonction par fonction. Un context switch pur (préemption) n’est pas un échec si le verrouillage est correct ; les vrais modes sont :
- Async-signal-safety — un signal interrompt le thread en plein milieu : le handler ne doit ni observer ni corrompre un état incohérent.
- Fork-safety —
fork()en multi-thread : l’enfant hérite de tous les locks mais d’un seul thread (cf. deadlock malloc/atfork) ; états cohérents viapthread_atfork/conception. - Cancellation-safety —
pthread_cancelaux points d’annulation : ni fuite ni corruption. - Réentrance / état mutable partagé non gardé — le péché capital de la libc C : buffers
statiques (
strtok,gmtime,getpwnam…),environ, la locale, le registre desFILE*, l’arène malloc, la liste atexit,TZ…
Chaque famille produit l’inventaire de son état mutable partagé, sa protection (lock ? TLS ? rien ?), et sa sûreté face aux 4 modes.
P5 — Grille sécurité élargie
Au-delà de la concurrence, l’audit et l’implémentation traquent aussi : overflow entier
(Principe 2 — ex. calloc(n,size)), bornes & format (%n, format-string, Principe 3),
TOCTOU sur les chemins, injection via environ/locale, effacement des secrets (memzero à
la libération des données sensibles).
P6 — Doctrine de l’assembleur : minimiser ; chaque bloc = trou de confiance
Notre architecture absorbe la plupart des raisons d’asm de musl : les syscalls sont en
couche 0, les atomics = core::sync::atomic, la TLS = la cible/std, les str/mem =
Rust sûr. On minimise donc l’asm. Résidu réellement incompressible attendu : setjmp/longjmp
(sauvegarde des registres callee-saved + adresse de retour), le crt0/_start (entrée avant que
le Rust ne tourne), éventuellement ucontext. Chaque bloc asm est : justifié (// SAFETY:,
ADR-021), minimal, audité face aux 4 modes, et ne touche aucun état partagé non gardé.
P7 — La règle de la fonction-sœur SAFE
Quand POSIX impose un prototype qui manipule une ressource mais retourne void / ne fournit
aucun canal d’erreur (ex. void rewind(FILE*) jette l’erreur de fseek ; void setbuf(...)
jette celle de setvbuf ; tzset, perror…) :
- (a) on implémente la version POSIX pure, fidèle au contrat — donc avec son DÉFAUT de reporting — mais robuste en dessous : elle est muette, pas fragile (elle ne se corrompt jamais elle-même) ;
- (b) on implémente une fonction-sœur SAFE qui remonte l’erreur (in-band, ADR-046 D4),
même hors-standard, nommée
air_<nom>(préfixeair_) ; - (c) on documente le défaut qu’on a identifié et le risque pour le développeur, et on suggère la version SAFE (doc ADR-027 : pourquoi + piège) ;
- (d) règle systématique et homogène (même modèle d’erreur in-band partout), pas ad hoc.
Le développeur C garde le choix : POSIX pur (compatible, avec le risque documenté) ou la version Air SAFE.
Conséquences
- Cycle par famille : audit (artefact) → implémentation Rust → doc (standard faisant autorité cité + défauts POSIX relevés + sœurs SAFE).
- Doublement partiel de la surface (les sœurs SAFE) assumé : la valeur est le choix informé et « la libc fait ce qu’elle dit ».
- Aucune optimisation avant complétude robuste (P3).
- La couche 1 grandit au besoin d’un tier kernel-faithful (ADR-046 D2) au fil des familles.
Alternatives écartées
- Copier l’implémentation de musl (C, asm, état global) : rejeté (P1 ; ADR-046 D1).
- Optimiser au fil de l’eau : rejeté (P3 / Principe 5).
- Se limiter à POSIX strict (pas de sœurs SAFE) : rejeté (P7 — laisserait le développeur sans recours sûr face aux défauts connus de POSIX).
- Auditer toute la surface puis tout implémenter : rejeté (P2 — l’audit vieillirait avant usage).
Annexe A — Gabarit d’audit par famille
Famille : <ex. stdio> Version musl auditée : <tag/commit>
Fonctions de la famille (surface cible) : <liste>
1. Structures internes : <…>
2. Globales publiques : <…> | Globales privées/opaques : <…>
3. État mutable partagé + protection :
| ressource | protection (lock/TLS/rien) | async-sig | fork | cancel | réentrance |
4. Assembleur : où / pourquoi / peut-on l'éviter chez nous (couche 0 / core / std) ?
5. Grille sécurité élargie : overflow | bornes/format | TOCTOU | injection | secrets
6. Fonctions `void`→ sœur SAFE `air_*` à créer : <liste + défaut POSIX constaté>
7. Bien fait (à reprendre) : <…>
8. Faiblesses (à ne pas reproduire) : <…>
9. Décisions Air pour cette famille : <…>