ADR-166 — La libc Air rend ses échecs : on corrige les prototypes que POSIX a laissés muets
Statut : Proposé (2026-08-24, règle posée par le BDFL). Décide que toute fonction de la libc Air qui peut échouer le dit, quitte à changer le prototype que POSIX déclare. Règle immuable de la libc.
Catégorie : Contrat de la libc, anneau 2. Prolonge ADR-046 D3 (POSIX fonctionnel, pas conforme) et D5 (kernel = bible) et applique à la face POSIX l’esprit qu’ADR-045 a fixé pour l’ABI propre d’Air.
Contexte
Ce qui était déjà décidé, et ce qui ne l’était pas
ADR-045 a tranché pour l’ABI C d’Air :
AirStatus rendu in-band, aucun errno, aucun canal hors-bande, les *out-params
écrits seulement en cas de succès. Il annonçait faire « précédent pour toute la future
libc Air ».
ADR-046 D3 a posé la posture : « POSIX = objectif fonctionnel,
pas conformité […] les SHOULD de POSIX deviennent des MUST chez Air (comportement
défini, jamais “implementation-defined”) ».
Mais rien n’a jamais dit ce qu’on fait d’un prototype POSIX qui ne peut pas exprimer
l’échec. Le précédent annoncé n’a pas été écrit, et le code s’en ressent : au 2026-08-24,
air-libc-capi::free est déclaré void free(void *ptr) et transmet à l’allocateur sans
rien rendre — l’appelant ne peut pas savoir si son bloc a été libéré.
Le défaut de POSIX que cet ADR corrige
void free(void *ptr) ne peut pas signaler un échec. Le développeur C qui écrit
free(p) n’a aucun moyen de savoir si son bloc a été rendu, si son pointeur était étranger
à l’arène, ou s’il venait de le libérer une seconde fois. POSIX répond « comportement
indéfini » — c’est-à-dire : ne demandez pas.
C’est exactement ce que le projet refuse ailleurs. Une API Rust rend un Result ; un
Option dit l’absence ; un type borné interdit l’état invalide. La libc Air doit tenir la
même promesse à un développeur C, sans quoi elle exporte vers le C une ambiguïté que le
reste du système a éliminée.
Décisions
D1 — Toute fonction faillible rend son échec
Une fonction de la libc Air qui peut échouer DOIT rendre un code d’échec, et permettre d’en connaître la cause. Là où le prototype POSIX ne le permet pas, Air change le prototype.
Le cas fondateur, décidé par le BDFL :
/* POSIX */ void free(void *ptr);
/* Air */ int free(void *ptr); /* 0 = libéré ; -1 = rien n'a été libéré */
D2 — Pourquoi cela ne casse pas le portage
C’est le point technique qui rend D1 tenable, et il mérite d’être écrit noir sur blanc :
En C, ignorer une valeur de retour est légal.
free(p);compile inchangé contreint free(void *).
L’écrasante majorité des sites d’appel d’un logiciel C existant ne voit aucune différence. Le portage — la raison d’être de cette libc — n’est pas entamé.
Là où cela casse, et on le dit :
- un pointeur de fonction typé sur la signature POSIX —
void (*f)(void*) = free;— ne compile plus. C’est réel, c’est étroit, et c’est un diagnostic de compilation franc, jamais un défaut silencieux ; - une redéclaration locale du prototype dans le source porté, qui entrera en conflit avec notre en-tête.
Dans les deux cas, le compilateur parle. C’est infiniment préférable au silence que POSIX impose aujourd’hui.
D3 — Trois régimes, et chaque fonction est classée
| Régime | Quand | Ce qu’Air fait |
|---|---|---|
| Corrigé | POSIX ne peut pas exprimer l’échec et Air sait le détecter | le prototype change ; 0 en succès, -1 en échec, cause dans l’errno thread-local |
| Fidèle | POSIX exprime déjà l’échec (-1, NULL, compte partiel) | inchangé — il n’y a rien à corriger |
| Impossible | la fonction ne peut structurellement pas revenir, ou l’échec n’est pas détectable | inchangé, et l’en-tête dit pourquoi |
Le régime Impossible couvre exit, _exit, abort — _Noreturn par définition — et
tout cas où l’implémentation ne peut pas honnêtement conclure.
D4 — Un code de retour qui ne vérifie rien est pire que void
C’est la garde de cette décision, et elle n’est pas rhétorique.
Une fonction ne passe au régime Corrigé que si son implémentation vérifie réellement. Un
intqui vaut toujours0promet un contrôle qui n’a pas lieu : il fait écrire à l’appelant unifqui ne protège de rien.
C’est le NO GO n°2 du dépôt — écrire qu’un sujet est traité sans qu’il le soit — appliqué à la frontière C.
Pour free, la vérification est possible : air-alloc porte des en-têtes de chunk et un
invariant d’arène, donc un pointeur étranger ou déjà libéré est détectable. C’est ce
qui autorise le régime Corrigé — et non l’inverse.
D5 — La cause voyage par l’errno thread-local, sur la face POSIX
La face POSIX conserve errno — thread-local, jamais global (ADR-046
D4. Une fonction corrigée le pose comme toute autre fonction POSIX faillible.
Ce n’est pas en contradiction avec ADR-045 : cet
ADR-là régit l’ABI propre d’Air (AirStatus in-band, zéro errno), celui-ci régit la
face POSIX. Deux faces, deux conventions, chacune cohérente avec ses appelants —
inventer une troisième convention pour les fonctions corrigées serait la vraie faute.
D6 — La correction se documente, dans l’en-tête et à la même place
Chaque fonction corrigée porte, dans l’en-tête C committé et dans sa documentation :
- le prototype POSIX d’origine ;
- le prototype Air ;
- ce que le code de retour distingue — non pas « échec », mais quels échecs ;
- ce que l’implémentation vérifie pour pouvoir le dire (D4).
Un développeur qui porte du C doit trouver l’écart là où il le rencontre, pas dans un document séparé.
Ce que cet ADR ne décide pas
- La liste des fonctions corrigées.
freeest le cas fondateur ; l’inventaire complet se constitue par amendement, fonction par fonction, chacune avec sa justification D4. - Le sort des écarts POSIX déjà pris —
EINTRremonté sans retry,errnothread-local,pthread_cancelen stub. Ils sont décidés en ordre dispersé et méritent un inventaire, qui relève d’un amendement d’ADR-046. - Le comportement des fonctions du régime Impossible au-delà de l’obligation de le dire.
D7 — L’inventaire : sept fonctions, pas deux cent quatre-vingt-huit
(Amendement du 2026-08-24 — mesuré, pas estimé.)
Cet ADR a été écrit sans savoir combien de fonctions il concernait. La roadmap parlait de « faire passer les 288 symboles », ce qui laissait croire à un chantier de plusieurs semaines. La mesure dit sept.
Le critère est mécanique : une fonction ne peut taire un échec que si son prototype ne
rend rien. Sur les 287 symboles extern "C" de la libc, 280 rendent déjà une
valeur — Air n’avait donc, presque partout, aucun échec à cacher. Restent sept :
| Fonction | Air sait-il ? | Régime D3 | Ce que le code fait aujourd’hui |
|---|---|---|---|
rewind | oui, et il jette | Corrigé | let _ = state.seek(AirSeekFrom::Start(0)) — air-libc-stdio/src/file.rs |
rewinddir | oui, et il jette | Corrigé | let _ = MANAGER.seek(…), sous un commentaire disant « l’erreur (rare) est ignorée (contrat void) » — air-libc-fileio/src/dir.rs |
setbuf | oui, et il jette | Corrigé | let _ = setvbuf_impl(…) — alors que setvbuf rend int |
clearerr | oui, et il jette | Corrigé | let _ = with_state(file, …) — l’échec est un FILE* invalide |
free | pas aujourd’hui | Corrigé, après instrumentation | Fait confiance à l’appelant (// SAFETY: contrat) ; ne teste que NULL |
freeaddrinfo | rien à savoir | Conservé | Parcourt la liste et rend les Box ; aucune opération faillible |
air_libc_stdio_flush_all | oui, et il jette délibérément | Conservé | Vidage d’arrêt de processus : on termine juste après, il n’y a plus personne pour lire le statut |
Les quatre premières sont le cas exact que D1 décrit : Air connaît l’échec, la
ligne qui le connaît s’appelle let _ = …, et le prototype POSIX l’empêche de le dire.
rewinddir va plus loin — un commentaire justifie l’oubli par le contrat void
lui-même. C’est le raisonnement circulaire que cet ADR casse : le type de retour n’est
pas une raison de ne pas savoir, c’est la conséquence d’un défaut de POSIX.
free est à part, et c’est la plus importante. Elle ne jette pas un échec connu :
elle ne le connaît pas. free_impl ne vérifie que la nullité, puis fait confiance. Un
double-free, ou un free d’un pointeur étranger, est aujourd’hui un comportement
indéfini silencieux. La corriger demande donc d’abord de la rendre capable de savoir
— l’en-tête d’allocation d’Air précède déjà chaque bloc et pourrait porter un témoin.
C’est un travail réel, mais borné, et D3
l’exige : on ne classe « Corrigé » qu’une fonction qu’Air sait vérifier — sans quoi le
code de retour ne vérifierait rien, ce que D4 interdit expressément.
Les deux « Conservé » portent leur raison, pas leur commodité. freeaddrinfo n’a
aucune opération faillible à rapporter. air_libc_stdio_flush_all vide les tampons à
l’arrêt du processus : le statut n’aurait personne à qui être rendu.
Ce que cet inventaire change pour la roadmap : l’étape 7 (« la libc — révision intégrale ») n’est pas un chantier de 288 fonctions. C’est quatre corrections mécaniques, une instrumentation d’allocateur, et deux justifications à écrire. Le gros du travail annoncé n’existait pas — il tenait à une estimation jamais mesurée.
Conséquences
- Un développeur C obtient d’Air ce qu’un développeur Rust obtient partout ailleurs : une fonction qui échoue le dit, et dit pourquoi.
- Les en-têtes C committés divergent de POSIX sur les fonctions corrigées — divergence
visible, documentée et vérifiée par
check-abi, jamais silencieuse. - Une obligation d’implémentation : passer une fonction en régime Corrigé exige d’écrire la vérification qui la justifie. D4 interdit le contraire.
- Le portage reste possible : D2 borne exactement ce qui casse, et le compilateur le dit.
Alternatives rejetées
- Rester fidèle à POSIX et ne rien corriger. Exporte vers le C l’ambiguïté que le reste
du système a éliminée, et contredit ADR-046 D3 — les
SHOULDde POSIX deviennent desMUST. - Doubler chaque fonction : un
free()fidèle et unair_free()qui rend le statut. Le développeur qui porte du C appellefree()— donc emprunte la face muette, et la face corrigée ne sert personne. On ne corrige pas un défaut en offrant une porte à côté. - Rendre l’erreur par un
AirStatussur la face POSIX. Introduirait une troisième convention là où les appelants C en attendent une qu’ils connaissent. - Le signaler seulement dans la documentation. Un commentaire énonce une règle ; il ne la
tient pas. C’est précisément la faute qu’
air-cryptoportait sur ses graines, et qu’un manager a dû venir corriger.
Licence du document : MPL 2.0