ADR-088 — std Rust sur Air safe : PAL custom sur la couche 1, sans libc C
Statut : Accepté (2026-07-10, décision BDFL). Amende ADR-076
(abandonne l’« option A : std sur la libc C libair_c » comme état-cible). Re-amende
ADR-050 (identité de cible : Air n’est plus « déguisé »
en unix). Raffine ADR-077 (AirProcessManager →
AirTaskManager). Résorbe ADR-087
(l’escape hatch raw_syscall disparaît). Instruit par la note d’étude
etude-std-pal-air-safe-fr.md.
Catégorie : Décision de toolchain (toit std) + raffinement d’un Manager couche 1.
N’altère aucune API couche 0 scellée ; la couche 1 (interne, non contractuelle pour les
apps — ADR-077) évolue par re-sceau additif.
Contexte
Le jalon M5 (2026-07-10) a prouvé qu’un programme std compile et tourne sur Air, par
le chemin d’ADR-076 (« option A ») : std réutilise
le PAL unix et lie une libc C (libair_c, crates air-libc-*), zéro glibc.
Mais ce chemin contredit la doctrine fondatrice (« l’unsafe Rust vit en couche 0
uniquement », macro-architecture §1) : la frontière C (extern "C" / #[no_mangle] /
FFI / errno in-band) est unsafe par nature et a même forcé l’escape hatch raw_syscall
(ADR-087). On a donc retiré de l’unsafe
pour le réintroduire par une libc C sous std. Incohérent avec un « userland Rust safe ».
La note d’étude a disséqué le std::sys réel (nightly
piné) et établi que le pivot est techniquement dégagé : (a) std::sys moderne accueille
les backends non-libc (précédent hermit, PAL ~171 lignes) ; (b) la couche 1 d’Air couvre
déjà tous les sous-systèmes en Rust safe (à quelques additifs mineurs près) ; (c) le contrat
est borné (bras cfg_select à ajouter, ~15 modules air.rs, un module futex qui débloque
toute la synchro std) ; (d) tous les backends existants (hermit_abi, xous asm, uefi
r-efi) bindent une frontière unsafe — Air serait le premier backend std safe de bout en
bout.
Décision
std repose sur un PAL Air custom et safe, bindant la couche 1 Rust, sans lib C ni
bindings C.
-
PAL custom safe. On implémente
std::sys::pal::air+ les modulessys/<domaine>/air.rs(fs, fd, net, thread, alloc, stdio, time, random, env, args, exit, pipe…) qui bindent les Managers de la couche 1 en Rust safe (AirResult<T>), zéro FFI. Le modulefutexsuffit à rendreMutex/Condvar/RwLock/Once/Parkerdestdgratuits. -
Invariant de layering (absolu). Le PAL est un toit : il s’assoit sur la couche 1, JAMAIS la couche 0 (instable, ADR-077 + doctrine de stabilité). Tout besoin du PAL est exposé par le Manager adéquat, qui s’appuie en interne sur la couche 0. Les manques (
current_exe,DirEntrytypé,park/unpark, certaines options socket) sont ajoutés dans les Managers (additifs couche-1v1.x). -
Identité de cible :
target_os = "air", Air n’est plus déguisé en unix. Ceci revient sur le choixenv = "musl"d’ADR-076 (fait exprès pour réutiliser le PALunix+ une libc). Air ne doit pas être capté par les brastarget_family = "unix"destd::sys(sinon il retombe surpal/unix→ libc). Les champs précis du spec JSON (os/env/abi/family) relèvent de la tâche de reprofilage de la cible (premier lot du chantier).hermit/xoussont des PRÉCÉDENTS STRUCTURELS, pas un modèle à cloner.hermitest un unikernel (noyau Rust propre, cibles*-unknown-hermit),xousun micro-noyau — ni l’un ni l’autre n’est Linux. Air est un userland Linux : son PAL binde la couche 1 → couche 0 → kernel Linux. On n’emprunte àhermitque (a) le patron « PAL mince +sys/*/air.rs» et (b) le précédent prouvant questd::sysaccepte un backend non-libc. Pas son noyau, pas sa cible, pas sa frontière C-ABI unsafe (hermit_abi). -
libair_csurvit, mais découplée destd. La libc C (air-libc-*) reste le produit pour les vrais développeurs C (porter des outils Unix), Linux-conforme, pas POSIX (cf. ADR-046, doctrine « symbole déprécié/inerte pour une abomination typepthread_cancel»). Maisstdne passe plus par elle. Deux produits, deux publics. -
AirTaskManagerremplaceAirProcessManager(raffine ADR-077). « Kernel = bible » : le kernel ne connaît que latask(struct task_struct) ; le process POSIX est un thread group (tasks partageant untgid), le thread est une task.AirTaskManagermodéliseAirTask+AirThreadGroup(tgid), médie surair-process+air-thread+ les concerns tâche/fork/TLS d’air-runtime, et produit côté userland les notions process/thread (getpid= tgid,gettid= pid de task). Il expose spawn/join,getpid/gettid,yield,park/unpark,set_name, et lefutex(primitive de parking, nature inter-process documentée). Faux ami écarté : lestruct task_groupdu kernel = cgroup d’ordonnancement CPU, sans rapport — relève d’AirSystemManager. -
Résorption de dettes. Avec le PAL custom bindant la couche 1,
stdn’appelle plussyscall/futex/gettiden C brut →raw_syscall(ADR-087) disparaît. La detteGlobalAlloc-safe (macro-architecture §8) est traitée dans ce chantier (air-allocderrière le hook, zéro FFI ; le rendre safe est un sujet potentiel d’RFC amont). -
TLS statique natif. Voie « native » (
#[thread_local]ELF, déjà acquise M5 —__tls_get_addrnon requis), pas les clésosfaçonpthread_key. Reste à fournir le hook de fin de thread (guard::enable) pour les destructeurs. -
M5 (option A) = marchepied de faisabilité, conservé comme preuve que
stdcompile+tourne sur Air. Ce n’est plus l’état-cible.
Conséquences
- Stratégie de sceau couche 1 : desceller UNE fois, tout faire, resceller UNE fois (comme le
sceau
couche-1-v1.0: « combler/remodeler puis sceller », pas de churn au fil de l’eau). On descelle la couche 1 pour toute la durée du chantier PAL, on y réalise tout le nécessaire, et on re-scelle une seule fois quand le critère est atteint (voir jalon).- Lot 1 (fait en premier — seul changement cassant) : renommage
AirProcessManager→AirTaskManager(codeair-process/air-thread/air-runtimemédiés +resource_limitd’ADR-086) avec, dans le même lot, l’exposition defutexetgettidviaAirTaskManager(ils existent déjà —air-thread::raw_futex,current_tid— il s’agit de les surfacer). + propagation doc (macro-architecture, ADR-077/085/086, INDEX, suivi, registre, libc-conformance). - Lots suivants (tous additifs, ne cassent rien) : les manques révélés par le PAL
(
current_exe,DirEntrytypé,park/unpark, options socket…) ajoutés dans le Manager adéquat, au fil de l’implémentation. - Re-sceau final unique au jalon de succès. Couche 1 « en flux » seulement pendant le
chantier (host-first, exploratoire) ; les additifs ne cassent ni
libair_cni les consommateurs existants.
- Lot 1 (fait en premier — seul changement cassant) : renommage
- Perte de la compatibilité
unix-family « drop-in » pour les crates tierscfg(unix)** — **assumée** : (i) le **modèle d'exécution par provenance** d'Air confine/recompile déjà tout binaire étranger (la libc Air + rustc + signature étant l'ancre) — le « drop-in » maximal n'a jamais été l'objectif ; (ii) la charte privilégie un **userland safe et prévisible** sur la compatibilité de masse ; (iii)hermit/xous` prouvent la viabilité du modèle non-unix. - Développement host-first : prototypage seul, downstream, via cible JSON custom +
-Z build-std+rust-srcpatché (tree hors-arbrert/, carve-out ADR-030) — aucun RFC requis pour prouver. - Ordre d’implémentation (du plus facile au plus dur, chacun bindant son Manager) :
exit/random/time/os_str/args/env→fd/stdio→futex+sync→fs→thread+thread_local→process→net. Manques couche 1 comblés en additifsv1.x. - Jalon de succès :
hello-stdre-tourne sur le PAL safe, 2 arches, sanslibair_cliée — preuve que le userland Rust est safe de bout en bout (unsafeconfiné couche 0). - RFC amont après la preuve : upstreaming Tier-3 de
*-linux-air(ADR-050) ; conversation Rust Teams libs sur un backendstd::syssafe de première classe et unGlobalAllocsafe. - Réversibilité : le patchset
rust-srcet le spec de cible sont localisés (hors-arbrert/) ; aucun impact sur la couche 0 scellée. La couche 1 évolue par re-sceau additif.
Alternatives rejetées
- Rester en option A (
stdsur la libc Clibair_c, ADR-076) : rejeté — réintroduit l’unsafevia la frontière C sousstd(et imposeraw_syscall), en contradiction directe avec la doctrine « userland Rust safe ». C’est la raison même du pivot. - Rester
unix-family + insérer un brasairavantunixpartout : rejeté — fragile (un brasunixoublié capte Air → libc silencieusement), et ne règle pas le fond (l’objectif est ne pas réutiliser le palunix). Le modèlehermit(target_ospropre) est plus sûr. - Un
AirThreadManagerséparé, ou garderAirProcessManager: rejeté — le kernel ne sépare pas task et process (tout esttask, le process = thread group) ; un Manager « process » qui ignore les threads, ou un split thread/process artificiel, s’éloignerait du modèle noyau.AirTaskManager(task + thread group) est fidèle et produit process/thread. - Fork downstream permanent de
std::sys: rejeté comme état final — on prototype downstream, mais la cible est l’upstreaming Tier-3 pour ne pas rebaser éternellement.