Note d’étude — une std Rust sur Air safe, sans libc C (PAL sur la couche 1)
Statut : note d’étude, NON normative. Cadre le nouvel objectif premier — une toolchain Rust dont la
stdrepose uniquement sur Air, par un backendstd::sysbindant la couche 1 Rust safe, sans lib C ni bindings C. Elle instruit une décision qui amendera ADR-076 (« option A :stdsur la libc Clibair_c») ; les décisions structurantes qui en découleront passeront par un ADR dédié puis, le cas échéant, un RFC amont à la Rust Teams libs. Aucun code n’est engagé par cette note.Source disséquée (réelle, pas de mémoire) : le
rust-srcdu nightly piné du treert/—nightly-2026-06-13,library/std/src/sys/.
1. Pourquoi ce pivot
M5 a prouvé que std compile et tourne sur Air, mais par le chemin d’ADR-076 :
std réutilise le PAL unix et lie une libc C (libair_c, crates air-libc-*). Or 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,
futex/gettid appelés en C brut). On a donc retiré de l’unsafe au nom de la doctrine
(« l’unsafe vit en couche 0 uniquement », macro-architecture §1) pour le réintroduire
par une libc C sous std. Incohérent.
Cible de l’étude : un backend std::sys::pal::air + des modules sys/<domaine>/air.rs
qui bindent la couche 1 Rust safe (les Managers de domaine, ADR-077), zéro FFI.
libair_c survit — elle reste la libc pour les vrais développeurs C (porter des outils
Unix, doctrine « Linux-conforme, pas POSIX ») — mais std ne passe plus par elle.
2. Comment std::sys est structuré aujourd’hui (et pourquoi ça nous arrange)
Le std moderne a vidé le PAL (chantier amont « FIXME(117276) », visible dans
sys/mod.rs). Deux niveaux :
- Sous-systèmes transverses
sys/<domaine>/mod.rs(fs,net,thread,thread_local,time,process,alloc,stdio,random,sync,os_str,args,env,fd,pipe,exit…). Chacun contient uncfg_select!qui route vers un fichier par OS (fs/unix.rs,fs/hermit.rs…) viause <os> as imp;, en enveloppantimpd’une grosse couche générique réutilisable (read_dir, copy, each_addr…). - PAL résiduel
sys/pal/<os>/— ne garde plus que l’amorçage runtime (init,cleanup,abort_internal), le modulefutex,time(souvent réexporté),stack_overflow,weak(dlsym).sys/pal/mod.rsdispatche parcfg_select!.
Le précédent qui valide le pivot : des backends std d’OS Rust sans libc existent déjà
en amont — hermit (unikernel Rust), xous (micro-noyau Rust), motor, uefi, zkvm.
Le PAL de hermit fait ~171 lignes (2 fichiers) contre ~2000 pour pal/unix. Le
mécanisme est donc conçu pour accueillir un backend non-libc.
3. Le patron à copier — et ce qui distingue Air
Structurellement, Air suit le patron hermit : PAL ultra-mince + un fichier
sys/<domaine>/air.rs par sous-système, bindant l’interface de l’OS.
Mais tous les backends existants bindent une frontière unsafe — c’est là qu’Air innove :
| Backend | Interface bindée | Nature | unsafe |
|---|---|---|---|
unix | libc (glibc/musl) | C-ABI externe | partout à la frontière |
hermit | hermit_abi (extern "C", #[link_name="sys_*"]) | C-ABI (libc-en-Rust) | 25 (socket), 17 (fs), 13 (thread)… |
xous | asm!("ecall") inline | asm brut | tout appel noyau |
motor | moto-rt : façade Rust au-dessus d’une vDSO extern "C" | C-ABI cachée | à la frontière |
uefi | r-efi (extern "efiapi") | C-ABI firmware | à la frontière |
air (cible) | Managers couche 1 Rust safe (AirFile, AirTcpSocket, AirThreadBuilder…) → Result typés | API Rust safe, lossless | confiné en couche 0 |
Le modèle le plus proche est motor/moto-rt (une façade Rust fn futex_wait(&AtomicU32) -> bool
au-dessus d’un mécanisme brut). Air pousse plus loin : la façade elle-même est safe et
sans C-ABI intermédiaire — les sys/*/air.rs appellent la couche 1, dont l’unsafe est
déjà confiné tout en bas dans air-sys-syscall (couche 0). Air serait le premier backend
std safe de bout en bout. C’est précisément l’angle qui justifie un RFC (§7).
⚠️ Invariant de layering (règle absolue). Le PAL est un toit : il s’assoit sur la couche 1, quoi qu’il arrive, et JAMAIS sur la couche 0. La couche 0 n’est pas stable (elle suit le kernel — doctrine de stabilité) : aucun toit ne s’y accroche. Donc tout ce dont le PAL a besoin doit être exposé par le Manager couche 1 adéquat (ADR-077), lequel s’appuie en interne sur la couche 0. Le jeu de cette étude est exactement cela : identifier ce qui manque au PAL et le placer dans le bon Manager couche 1 — pas de raccourci vers la couche 0, jamais.
4. Le contrat exact d’un backend air
4.1 Bras cfg_select! à ajouter
Un bras target_os = "air" (ou target_env = "air") — voir la décision §6.a — à insérer
avant le bras unix dans le PAL et chaque sous-système migré :
| Fichier | Action | Piège |
|---|---|---|
sys/pal/mod.rs | target_os="air" => mod air avant unix => | sinon target_family="unix" capte Air → pal/unix → libc |
sys/alloc/mod.rs | mod air (GlobalAlloc) | — |
sys/args/mod.rs | mod air (ou réutilise common) | — |
sys/env/mod.rs | mod air | — |
sys/fd/mod.rs | mod air (FileDesc(OwnedFd)) | brique de fs/net/pipe/stdio/process |
sys/fs/mod.rs | mod air | le plus gros mais le plus « traduisible » |
sys/io/mod.rs + sys/io/error/* | mod air (is_terminal, decode_error_kind) | — |
sys/net/connection/{mod,socket/mod,hostname/mod}.rs | mod air | le plus couplé libc — réécriture complète |
sys/paths/mod.rs | mod air (getcwd, current_exe, temp_dir…) | — |
sys/pipe/mod.rs | mod air | — |
sys/process/mod.rs | mod air | le plus dur (fork/exec) |
sys/random/mod.rs | mod air (obligatoire) | dispatch par target_os → sinon _ => {} vide, ne compile pas |
sys/stdio/mod.rs | mod air | facile une fois fd |
sys/sync/{mutex,condvar,rwlock,thread_parking}/mod.rs | ajouter target_os="air" aux bras futex existants (PAS un module) | dispatch par target_os → sinon tombe sur pthread = libc |
sys/thread/mod.rs | mod air (Thread) | id() renvoie aujourd’hui libc::pthread_t → id Air |
sys/thread_local/mod.rs | TLS statique (native) + guard::enable, ou clés os | voir §6.c |
sys/exit.rs | bras air → exit via AirTaskManager (couche 1 ; 2 endroits : exit et unique_thread_exit) | sinon _ => intrinsics::abort() = abort au lieu d’exit (bug silencieux) |
sys/personality/mod.rs | bras gcc (déjà target_family="unix") → symboles _Unwind_* | dépendance de lien, pas un module à écrire |
Aucune action (générique, convient déjà) : sys/os_str (→ bytes, OsStr = [u8]),
les couches génériques de fs/process/net/connection/sync/once/backtrace, cmath
(→ symboles libm externes, fournis par air-libm).
4.2 Le PAL air mince (gabarit = pal/unsupported/common.rs + pal/hermit)
pub unsafe fn init(argc, argv, sigpipe)— appelleargs::init, protection stack-overflow, SIGPIPE.pub unsafe fn cleanup()pub fn abort_internal() -> !pub fn unsupported<T>() / unsupported_err()pub mod futex:type Futex = Atomic<u32>(+SmallFutex),futex_wait(&Atomic<u32>, u32, Option<Duration>) -> bool,futex_wake(&Atomic<u32>) -> bool,futex_wake_all(&Atomic<u32>). Binde la couche 1 (le futex exposé parAirTaskManager—air-threada déjàraw_futex), jamais la couche 0.pub mod time(souvent#[path="../unix/time.rs"], ou unair.rs).- point d’entrée runtime (déjà couvert par
air-rt-std/air-std-entrydu harnais M5).
Le meilleur ratio du projet : ces 3 fonctions futex suffisent — Mutex, Condvar,
RwLock, Once, Parker de std sont alors gratuits (impls génériques sync/*/futex.rs,
100 % Rust safe).
5. Cartographie couche 1 → sous-systèmes (la couche 1 couvre déjà tout)
La couche 1 expose partout des APIs AirResult<T> safe, sur couche 0 uniquement, jamais
de libc C. Correspondance :
Sous-système std | Brique couche 1 | État |
|---|---|---|
fs | air-filesystem (AirFile, AirFileManager, AirFileMetadata) | couvre |
fd | air-handle (AirHandle, OwnedFd) + air-filesystem | couvre |
net | air-socket (AirTcpSocket/Listener, AirUdpSocket, AirSocket, AirNameResolver) + air-handle | couvre |
thread | air-thread (AirThreadBuilder, yield_now, set_thread_name) + air-runtime (JoinHandle, current_tid) | couvre |
sync / parking | air-thread (raw_futex : futex_wait/futex_wake) | couvre (3 wrappers → toute la synchro) |
thread_local | air-runtime (TCB/TLS ELF natif #[thread_local], run_thread_local_destructors) | couvre (voir §6.c pour les clés dynamiques) |
time | air-base-core (AirInstant, AirSystemTime) | couvre |
process | air-process (AirCommand, AirProcess, spawn_process, credentials) + air-runtime/fork | couvre |
os_str / path | air-base-core (AirOsStr/String, AirPath) | couvre |
args / env | air-runtime/args + air-env (get/vars/set/unset, environ_pointer) | couvre |
alloc (GlobalAlloc) | air-alloc (GlobalAllocator, reset_after_fork) | couvre |
stdio | air-stdio (stdin/stdout/stderr, is_terminal) | couvre |
random | air-crypto (AirRandom::fill, sur getrandom couche 0) | couvre |
| signaux (SIGPIPE/SIGCHLD, auto-réveil) | air-signal (install_handler, AirSignalFd) + air-poll | couvre |
Manques (mineurs) à combler côté couche 1, additifs v1.x :
current_exe(/proc/self/exe) — non trouvé ; probable vrai manque pourstd::env::current_exe.DirEntrytypé —read_directory_entriesrend un buffer brut ; le PAL peut parsergetdents64lui-même, ou l’on ajoute unReadDir/DirEntryergonomique dansair-filesystem.park/unpark/park_timeouthelper dansair-thread(les primitives futex suffisent, mais éviter de dupliquer).- Options socket dédiées (
SO_RCVTIMEO/SO_LINGER/IP_TTL/peek) — faisables via l’APIset_socket_option_intgénérique ou à ajouter. - Clés TLS dynamiques — seul
#[thread_local]natif est fourni (suffisant pour lestdmoderne, voie « native ») ; une API de clés ne serait requise que par la voieos.
Placement Manager (ADR-077) — décision de l’étude. Le PAL binde des Managers, pas des fonctions libres éparses. Le tableau ci-dessus liste les crates ; il faut arrêter, pour chaque besoin du PAL, le Manager qui l’expose. Cas notables :
AirTaskManager(fidèle au kernel), PASAirProcessManagerniAirThreadManager(décision). « Kernel = bible » : le kernel ne connaît que latask(struct task_struct, l’unité ordonnançable) ; ce que POSIX appelle process est un thread group (tasks partageant untgid; le leader apid == tgid), et un thread est une task membre.forket création de thread sont le mêmeclone3(seuls les flagsCLONE_THREAD/CLONE_VM/… changent). Le couplage fork → thread unique est intrinsèque (aprèsclone, réinitialiser TID, TLS/TCB, handlersatfork,errno_location,air-alloc::reset_after_fork,air-stdio::reset_after_fork— déjà porté parair-runtime/fork.rs+process_context). ⇒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 POSIX : process = thread group (getpid→ tgid), thread = task (gettid→ pid de task). Il expose : spawn/join,getpid/gettid,yield,park/unpark,set_name. (Le Manager est une surface de médiation — aucune fusion de crates requise.) Faux ami à éviter : lestruct task_groupdu kernel = regroupement d’ordonnancement CPU / cgroups (CONFIG_CGROUP_SCHED), sans rapport avec le thread group ; il relèverait d’AirSystemManager.futex— à placer consciemment. Ce n’est pas purement « thread » : c’est une primitive de synchronisation sur un mot mémoire, utilisable inter-process (mémoire partagée). Dans le PAL il sert de parking de task (il backMutex/Condvar/Parker). Reco : l’exposer viaAirTaskManageraux côtés depark/unpark(c’est son usage PAL), en documentant sa nature inter-process. (air-thread::raw_futexfournit déjàfutex_wait/futex_wake.)- Les manques (1)-(5) ci-dessus sont à ajouter dans le Manager de domaine adéquat (additifs
couche-1
v1.x) :current_exe/DirEntry→AirFileManager; options socket →AirNetworkManager;park/unpark→AirTaskManager.
Règle du chantier : pour chaque item du contrat PAL (§4), la question n’est jamais « quel syscall » mais « quel Manager couche 1 l’expose, et que faut-il y ajouter ». Zéro accroche couche 0 depuis le PAL.
6. Décisions de conception à trancher (pour l’ADR)
a. La cible : target_os="air" (à la hermit), PAS env=musl+target_family="unix".
ADR-076 a délibérément fait ressembler Air
à unix (env=musl+vendor=air) pour réutiliser le PAL unix + une libc. Le pivot
inverse ce choix : pour un PAL custom, Air doit ne pas être capté par les bras
target_family="unix" (sinon il retombe sur pal/unix→libc). hermit est target_os="hermit",
hors famille unix. ⇒ Air devrait être target_os="air" (cible JSON custom, comme en
phase 1), quitte à ajouter des bras air là où hermit a les siens. (Alternative discutée :
rester unix-family et insérer un bras air avant unix partout — plus fragile, plus de
patches, risque de capture accidentelle par un bras unix oublié.)
b. Allocateur : air-alloc derrière GlobalAlloc — et la dette unsafe. Le hook
GlobalAlloc est unsafe (le trait), mais zéro FFI : air-alloc est un allocateur Rust
possédé. C’est la dette GlobalAlloc-safe (macro-architecture §8) — à instruire dans ce
chantier (peut-elle devenir safe ? sujet potentiel d’RFC, §7).
c. TLS : voie « native » (statique ELF), pas les clés os. air-runtime supporte déjà
#[thread_local] natif ; __tls_get_addr n’est pas requis (TLS statique, acquis M5). Il
reste à fournir le hook de fin de thread (guard::enable) qui déclenche les destructeurs.
Évite de réimplémenter pthread_key_*.
d. Résorber raw_syscall (ADR-087). raw_syscall n’existait que parce que le PAL unix
appelait syscall/futex/gettid en C brut (via la libc). Avec le PAL custom, le PAL
binde la couche 1 (jamais la couche 0) : c’est le Manager couche 1 adéquat qui expose
futex et gettid (en s’appuyant en interne sur la couche 0). ⇒ l’escape hatch
raw_syscall disparaît — le pivot résorbe une dette au lieu d’en créer. futex et
gettid sont exposés par AirTaskManager (task + thread group, fidèle au kernel —
voir §5, note « placement Manager »).
e. id() du thread : un id Air, pas libc::pthread_t. sys/thread renvoie aujourd’hui un
pthread_t ; Air renvoie un Tid/u64. (Attention : bug connu du spawn natif aarch64,
cf. suivi — à traiter dans ce chantier.)
7. La frontière de l’RFC — ce qu’on fait seuls vs ce qu’on propose à Rust
Ce qu’on peut faire seuls, tout de suite (downstream) : exactement comme M5 — cible JSON
custom + -Z build-std + rust-src patché (bras air ajoutés). C’est éprouvé. Cela
n’exige aucun RFC pour prototyper et prouver un std safe sur Air.
Ce qui mérite un RFC amont (pour ne pas forker std::sys éternellement) : l’angle n’est
pas « rendre le mécanisme non-libc possible » (hermit le prouve déjà), mais :
- L’upstreaming Tier-3 de
*-linux-air(cible + mainteneur = projet Air) — planifié (ADR-050) : c’est le véhicule qui rend les brasairlégitimes en amont plutôt que patchés localement. - Un point d’extension propre pour qu’un « OS userland Rust safe » branche son backend
sans dupliquer les bras
cfg_select!de chaque sous-système — la question honnête à poser à la Teams libs : le contrat PAL actuel (pensé pour des frontières C-ABI) peut-il accueillir de première classe un backend safe dont l’unsafeest chez l’OS (couche 0) et non dansstd::sys? Y a-t-il un intérêt à formaliser un trait de backend partagé ? GlobalAllocsafe (§6.b) — la question la plus « recherche » : un hook d’allocateur global qui n’oblige pas àunsafe impl.
Honnêteté de cadrage : le gros de la valeur RFC est (1) — le reste ((2),(3)) est une conversation à ouvrir avec la Teams libs, pas un préalable au prototype. On avance downstream, on prouve, puis on propose.
8. Prochaines étapes (proposition)
- Cette note — revue BDFL (nous y sommes).
- ADR de direction — amende ADR-076 :
PAL custom safe sur couche 1,
target_os="air",libair_cdécouplée destd, résorption deraw_syscall. Tranche §6.a–e. - Reprofilage de la cible
*-linux-air(JSON :target_os="air", plusenv=musl) + les brasairdans unrust-srcpatché (fork de travail hors-arbrert/, carve-out ADR-030). - Implémentation par sous-système, du plus facile au plus dur, chacun bindant sa brique
couche 1 :
exit/random/time/os_str/args/env→fd/stdio→futex+sync→fs→thread+thread_local→process→net. Manques couche 1 (§5) comblés en additifsv1.xau fil de l’eau. - Jalon :
hello-stdre-tourne sur le PAL safe (2 arches), sanslibair_cliée — la preuve que le userland Rust est safe de bout en bout (unsafe confiné couche 0). - RFC amont (Tier-3 + conversation backend safe), une fois la preuve en main.
9. Conclusion
Le pivot est techniquement dégagé : (a) std::sys moderne accueille les backends non-libc
(hermit le prouve, 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é et
connu (les bras à ajouter, les ~15 modules air.rs, le module futex qui débloque toute la
synchro) ; (d) le pivot résorbe raw_syscall au lieu d’en créer, et confine enfin l’unsafe
là où la doctrine l’exige — la couche 0. La singularité d’Air — un backend std safe, non
une n-ième frontière C-ABI — est ce qui, à terme, mérite d’être porté à la Rust Teams libs.