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-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 (AirProcessManagerAirTaskManager). 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.

  1. PAL custom safe. On implémente std::sys::pal::air + les modules sys/<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 module futex suffit à rendre Mutex/Condvar/RwLock/Once/Parker de std gratuits.

  2. 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, DirEntry typé, park/unpark, certaines options socket) sont ajoutés dans les Managers (additifs couche-1 v1.x).

  3. Identité de cible : target_os = "air", Air n’est plus déguisé en unix. Ceci revient sur le choix env = "musl" d’ADR-076 (fait exprès pour réutiliser le PAL unix + une libc). Air ne doit pas être capté par les bras target_family = "unix" de std::sys (sinon il retombe sur pal/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/xous sont des PRÉCÉDENTS STRUCTURELS, pas un modèle à cloner. hermit est un unikernel (noyau Rust propre, cibles *-unknown-hermit), xous un 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 à hermit que (a) le patron « PAL mince + sys/*/air.rs » et (b) le précédent prouvant que std::sys accepte un backend non-libc. Pas son noyau, pas sa cible, pas sa frontière C-ABI unsafe (hermit_abi).

  4. libair_c survit, mais découplée de std. 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 type pthread_cancel »). Mais std ne passe plus par elle. Deux produits, deux publics.

  5. AirTaskManager remplace AirProcessManager (raffine ADR-077). « Kernel = bible » : le kernel ne connaît que la task (struct task_struct) ; le process POSIX est un thread group (tasks partageant un tgid), le thread est une task. AirTaskManager modélise AirTask + AirThreadGroup (tgid), médie sur air-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 le futex (primitive de parking, nature inter-process documentée). Faux ami écarté : le struct task_group du kernel = cgroup d’ordonnancement CPU, sans rapport — relève d’AirSystemManager.

  6. Résorption de dettes. Avec le PAL custom bindant la couche 1, std n’appelle plus syscall/futex/gettid en C brut → raw_syscall (ADR-087) disparaît. La dette GlobalAlloc-safe (macro-architecture §8) est traitée dans ce chantier (air-alloc derrière le hook, zéro FFI ; le rendre safe est un sujet potentiel d’RFC amont).

  7. TLS statique natif. Voie « native » (#[thread_local] ELF, déjà acquise M5 — __tls_get_addr non requis), pas les clés os façon pthread_key. Reste à fournir le hook de fin de thread (guard::enable) pour les destructeurs.

  8. M5 (option A) = marchepied de faisabilité, conservé comme preuve que std compile+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 AirProcessManagerAirTaskManager (code air-process/air-thread/air-runtime médiés + resource_limit d’ADR-086) avec, dans le même lot, l’exposition de futex et gettid via AirTaskManager (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, DirEntry typé, 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_c ni les consommateurs existants.
  • Perte de la compatibilité unix-family « drop-in » pour les crates tiers cfg(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-src patché (tree hors-arbre rt/, 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/envfd/stdiofutex+syncfsthread+thread_localprocessnet. Manques couche 1 comblés en additifs v1.x.
  • Jalon de succès : hello-std re-tourne sur le PAL safe, 2 arches, sans libair_c liée — preuve que le userland Rust est safe de bout en bout (unsafe confiné couche 0).
  • RFC amont après la preuve : upstreaming Tier-3 de *-linux-air (ADR-050) ; conversation Rust Teams libs sur un backend std::sys safe de première classe et un GlobalAlloc safe.
  • Réversibilité : le patchset rust-src et le spec de cible sont localisés (hors-arbre rt/) ; aucun impact sur la couche 0 scellée. La couche 1 évolue par re-sceau additif.

Alternatives rejetées

  • Rester en option A (std sur la libc C libair_c, ADR-076) : rejeté — réintroduit l’unsafe via la frontière C sous std (et impose raw_syscall), en contradiction directe avec la doctrine « userland Rust safe ». C’est la raison même du pivot.
  • Rester unix-family + insérer un bras air avant unix partout : rejeté — fragile (un bras unix oublié capte Air → libc silencieusement), et ne règle pas le fond (l’objectif est ne pas réutiliser le pal unix). Le modèle hermit (target_os propre) est plus sûr.
  • Un AirThreadManager séparé, ou garder AirProcessManager : rejeté — le kernel ne sépare pas task et process (tout est task, 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.