Audit face-PAL — clôture pré-sceau couche-1-v1.0 (roadmap §5.2)
Statut : note d’audit (completeness critic), NON engageante. Préalable au sceau
couche-1-v1.0(roadmap §5.2). Lecture seule du code — aucune modification. Symboles réels, grepés (crates/), pas devinés. Rédigé le 2026-07-02.Objet. Le PAL Rust = le backend
std::sysqu’une toolchain*-linux-air(ADR-049/050/052) doit implémenter. Cet audit statue, primitivestd::syspar primitive, si la brique couche 1 (API Rust) existe pour porter la primitive. Le PAL est un toit au-dessus de la couche 1 (ADR-052 : seule la couche 1 consomme la couche 0) : il compose les briques couche 1 ; il ne re-syscall pas.Grille de statut. ✅ Brique présente · ◐ Partielle (manque précisé) · ❌ Manquante. Pour ◐/❌ : BLOQUANT (fondateur absent — non comblable sans refonte d’une API qu’on s’apprête à sceller) vs ADDITIF post-sceau (comblable en
v1.xpar descellement additif, sans toucher l’API scellée — modèle ADR-051).
1. Tableau — primitive std::sys → brique(s) couche 1 → statut
| # | Primitive std::sys | Brique(s) couche 1 (symboles réels) | Statut | Bloquant / Additif |
|---|---|---|---|---|
| 1 | thread — spawn/join | air_runtime::thread::spawn(entry: extern "C" fn(usize), arg: usize) -> Result<JoinHandle, Errno> + JoinHandle::{join, tid} (thread.rs:136/85) ; primitive : air_thread::runtime_primitives::create_thread(...) (runtime_primitives.rs:147) | ◐ | Additif — le spawn natif bas-niveau (pile mmap + TLS/TCB + clone3 + join futex) est présent et prouvé on-target. L’ergonomique boxé (FnOnce()->T) manque sur la cible → §2.B.2 ci-dessous. |
| 1b | thread — yield / sleep / name / id | yield : couche 0 air_sys_syscall::process::sched_yield() (process.rs:3494) ; sleep : air_base_core::time::{sleep, sleep_until} (time.rs:331/359) ; id : air_thread::runtime_primitives::current_tid() (:264), air_runtime::AirRuntime::current_tid() ; name (PR_SET_NAME) : cible absent (seul le wrapper hôte std::thread) | ◐ | Additif — sleep/yield/id ✅ ; yield_now = passthrough trivial sched_yield (couche 0) à surfacer ; nommage de thread sur cible = wrapper prctl(PR_SET_NAME) à ajouter. |
| 2 | sync — Mutex / RwLock | air_thread::{AirMutex, AirRwLock} (+ guards) sur futex(2) maison (sync.rs:135/341), sans std::sync, sans poisoning, modélisés loom (ADR-058) | ✅ | — (fondateur présent) |
| 2b | sync — Condvar | (aucun type Condvar/AirCondvar) — briques futex : air_thread::runtime_primitives::{futex_wait_word, futex_wake_one, reset_futex_word} (:219/238/248) | ❌ | Additif — pas de brique Condvar, mais les primitives futex fondatrices sont exposées ⇒ constructible au-dessus sans toucher l’API scellée. |
| 2c | sync — Once / lazy-init | (aucun type Once/AirOnce) — briques : air_thread::atomic::AtomicU32 + futex ci-dessus | ❌ | Additif — Once = AtomicU32 + futex ; fondations présentes. |
| 3 | thread_local (#[thread_local], TLS/TCB) | air_runtime::thread_control_block::ThreadControlBlock (errno, canari, DTV, tid ; :69) ; air_runtime::thread_local_storage::{ThreadLocalStorage, init_spawned_thread_tls} (:460/626) ; errno vrai #[thread_local] sur cible (thread_control_block.rs:289) — Variant I/II, ADR-049 D2/D3, prouvé on-target | ◐ | Additif — brique TLS/TCB fondatrice présente. Manque : destructeurs thread_local! (Drop fin de thread, ADR-049 D9) — champ DTV présent, pas de register_dtor. Mineur (panic=abort). |
| 4 | time — Instant / SystemTime | air_base_core::time::{AirInstant, AirSystemTime, AirDuration} (:49/76/55) — CLOCK_MONOTONIC/CLOCK_REALTIME, UNIX_EPOCH, duration_since -> AirResult (recul d’horloge honnête) | ✅ | — |
| 5 | fs — File / metadata / dir | air_filesystem::AirFileSystem = façade path-level : canonicalize/exists/write_atomic/read_to_bytes/copy/read_dir/glob (paths.rs, atomic.rs, search.rs) + AirTempDir + watcher inotify. Syscalls fichier bruts : couche 0 air_sys_syscall::fs::{openat2, read, write, pread/pwrite, lseek, statx, getdents64, mkdirat, unlinkat, renameat2, symlinkat, readlinkat, ftruncate, fsync} (fs.rs) | ◐ | Additif — pas d’objet File couche 1 (open/read/write/seek/metadata). Le socle couche 0 est scellé et complet ; il manque le handle AirFile dans air-filesystem (brique existante) → ajout additif, aucune refonte. (Voir Gaps additifs — c’est le plus substantiel.) |
| 6 | net — TCP / UDP / lookup | air_socket::{AirTcpSocket, AirTcpListener, AirUdpSocket, AirUnixStream/Listener/Datagram} (connect/bind/listen/accept/read/write/send_to/recv_from/shutdown/as_fd) + AirNameResolver::{resolve, resolve_socket} (résolveur DNS maison, hosts/resolv.conf) | ✅ | — |
| 7 | process — Command / Child / exit | air_process::{AirCommand (builder + spawn), AirProcess (wait_*/try_wait/signal/kill/pidfd/take_std*), AirExitStatus, AirStdio} + air_pipe + exit_process(code) -> ! (clone3/pidfd/exec internes) | ✅ | — |
| 8 | env — vars | air_env::{get, vars, Vars} (octets, lecture seule ; set_environ au démarrage) | ◐ | Additif — lecture ✅, mais set/unset (mutation setenv) absents. std::env::set_var en aura besoin → ajout additif à air-env. |
| 15 | args (argv) | air_runtime::args::{count, get, args, Args} (octets ; :27/46/66) | ✅ | — |
| 9 | random (seed HashMap / getrandom) | Couche 0 air_sys_syscall::system::getrandom surfacé par air_crypto::AirRandom::fill(&mut [u8]) -> AirResult<()> (random.rs:60) et air_base_core::id (usage privé UUIDv7). CSPRNG kernel, jamais PRNG userspace. | ✅ | — (le PAL peut lier AirRandom::fill ou directement couche-0 getrandom pour éviter de tirer les deps RustCrypto) |
| 10 | stdio — Stdin/out/err | air_stdio::{stdin, stdout, stderr} -> StandardStream : read/write bruts (fd 0/1/2), is_terminal, lock() -> StreamGuard, reset_after_fork (atfork). no_std, verrou par flux modélisé loom | ✅ | — |
| 11 | path — séparateurs / composants | air_base_core::path::{AirPath, ComponentIterator} : séparateur / (POSIX), components/parent/join/file_name/extension/is_absolute/normalize_lexically/to_c_string (path.rs) | ✅ | — |
| 12 | alloc (GlobalAlloc) | air_alloc::GlobalAllocator : unsafe impl core::alloc::GlobalAlloc — alloc/alloc_zeroed/dealloc/realloc (global.rs:75), const fn new, backing mmap réel (MmapBacking), OOM = null déterministe (jamais de hang). #[global_allocator] static câblé dans rt/air-rt. | ✅ | — |
| 13 | os_str (OsStr/OsString octets) | Substrat présent : AirPath (octets possédés) + conventions &[u8]/Vec<u8> (env/args/AirDirEntry). Pas de paire newtype OsStr/OsString dédiée (AirPath = owned-only, emprunts en &[u8] brut) | ◐ | Additif — le PAL os_str Unix EST un wrapper de [u8]/Vec<u8> : substrat trivialement présent ; newtype AirOsStr/AirOsString = ajout additif si l’on veut une paire nommée. |
| 14 | backtrace / unwind | panic = abort partout (ADR-045/ADR-049 D9) ⇒ pas d’unwind cross-thread ; sortie via air_process::exit_process / couche 0 exit_group(status) -> ! (process.rs:688) | ✅ | — (surface std::sys backtrace/unwind minimale par construction) |
2. Points durs tranchés
2.1 — sync : Condvar & Once (grep)
Confirmé par grep (air-thread/src/, arbre entier) : aucun Condvar/AirCondvar,
aucun Once/AirOnce. La surface sync exportée est exactement
AirMutex/AirRwLock/AirSemaphore (+ guards) et air_channel (MPSC).
Statut : ❌ absents, mais ADDITIFS. Les deux se construisent au-dessus des
primitives futex fondatrices déjà publiques (futex_wait_word/futex_wake_one/
reset_futex_word + AtomicU32). Les ajouter = nouveaux types dans air-thread
(brique existante), sans nouvelle surface couche 0 ni refonte d’API scellée →
descellement additif v1.x (ADR-051). Non bloquant.
2.2 — TLS (#[thread_local] / TCB) pour la face PAL
Statut : ◐ — brique fondatrice présente. air-runtime fournit le TCB
(ThreadControlBlock : errno, canari %fs:0x28/tpidr_el0, DTV, tid) et le TLS
ELF (ThreadLocalStorage, Variant I/II, init_spawned_thread_tls,
install_main), avec un vrai errno #[thread_local] sur *-linux-air
(ADR-049 D3), prouvé on-target (selftest rt/, 2 arches). C’est la brique
que std::sys::thread_local réclame. Manque additif : l’exécution des
destructeurs thread_local! en fin de thread (ADR-049 D9) — le champ DTV
existe mais aucun register_dtor n’est câblé. Impact mineur (panic=abort,
threads courts). Non bloquant.
2.3 — random : quelle brique ?
La seed HashMap / getrandom de std::sys se lie à air_crypto::AirRandom::fill
(random.rs:60), lui-même sur la couche 0 air_sys_syscall::system::getrandom.
Recommandation : pour le PAL, lier directement la couche-0 getrandom
(comme le fait déjà air_base_core::id) plutôt que de tirer toute la fermeture
RustCrypto d’air-crypto — cela évite aussi le point dur cpufeatures→libc
d’aarch64 (§2.5). ✅ présent.
2.4 — path / os_str : AirPath couvre-t-il la face PAL ?
path : ✅. AirPath couvre exactement la logique std::sys::path Unix :
séparateur /, itération de composants (ComponentIterator → &[u8]), parent,
join, is_absolute, file_name, extension, normalize_lexically,
to_c_string (FFI syscall). os_str : ◐ additif — AirPath est owned-only
(pas de paire empruntée &OsStr), et il n’existe pas de newtype OsStr/OsString
générique ; sur Unix, std::sys::os_str n’est qu’un wrapper de [u8]/Vec<u8>,
substrat déjà présent partout. Newtype dédié = additif.
2.5 — fermeture on-target (rappel + caveat aarch64)
§2.A a prouvé la fermeture no_std icu pour x86_64/aarch64-unknown-linux-air
(release, build-std=core,alloc, LSTM inclus). C’est une propriété de fermeture
de lien, distincte de la complétude d’API objet de cet audit. Caveat
(§2.E.2) : sur aarch64, la dép cpufeatures d’air-crypto lie libc
(getauxval) sans profil env=air → chemin air_auxval(AT_HWCAP) libc-free à
fournir. C’est une limitation de fermeture (chantier dédié), pas un manque
d’API PAL : le PAL n’a pas besoin d’air-crypto pour son random (il lie
couche-0 getrandom).
3. Gaps BLOQUANTS (fondateur absent)
Aucun.
Toutes les primitives std::sys disposent de leur brique fondatrice en couche
1 (ou de son socle couche-0 scellé + point d’ancrage couche-1) :
- l’allocateur global (
GlobalAlloccomplet, backing mmap réel) — présent ; - le spawn natif avec bootstrap TCB/TLS (la partie unsound-si-fausse, qui fige la disposition TLS qu’on scelle) — présent et prouvé on-target ;
- les verrous
Mutex/RwLocksur futex maison + les primitives futex exposées — présents ; - time, net, process, stdio, path, args, random, exit — présents ;
- le socle fs (syscalls fichier couche 0) — scellé et complet.
Rien de ce qui reste ne peut casser une API scellée : ce sont des ajouts au-dessus.
4. Gaps ADDITIFS post-sceau (comblables en v1.x, sans refonte)
Classés par importance décroissante. Aucun ne modifie une API scellée ; chacun s’ajoute à une brique existante (ou au toit PAL) au-dessus de fondations figées.
fs— handleAirFilecouche 1 (le plus substantiel).air-filesystemn’expose pas de type fichier (open/read/write/seek/metadata) ; seulement des ops path-level. Le socle couche-0 (openat2/read/write/lseek/statx/getdents64…) est scellé et complet. → ajouter un objetAirFile(RAIIOwnedFd) +metadata/DirEntrydansair-filesystem. Additif (brique existante, socle figé, cohérent avec la façon dontair-socket/air-processexposent déjà leurs handles I/O).thread— spawn ergonomique boxé (FnOnce()->T+ résultat au join) sur la cible. Voir §5 (recommandation dédiée). Additif.sync—CondvaretOnce— au-dessus des primitives futex publiques (§2.1). Additif.env— mutation (set/unset,std::env::set_var).air-envest lecture-seule aujourd’hui. Additif.os_str— newtypeAirOsStr/AirOsString(paire owned/emprunté octets). Substrat[u8]/Vec<u8>déjà présent. Additif.thread_local— destructeursthread_local!(ADR-049 D9) — DTV présent,register_dtorà câbler. Additif, mineur (panic=abort).thread— divers cible :yield_now(passthrough couche-0sched_yieldà surfacer en couche 1), nommage de thread sur cible (prctl(PR_SET_NAME)). Additifs, triviaux.
5. Recommandation argumentée — spawn ergonomique natif (§2.B.2)
Question ouverte §2.B.2. L’API std::thread::spawn a-t-elle sa brique
ergonomique sur la cible ? Le spawn natif bas-niveau existe
(air_runtime::thread::spawn(entry: extern "C" fn(usize), arg: usize) +
air_thread::runtime_primitives::create_thread), mais l’ergonomique boxé
(FnOnce()->T via air-alloc + trampoline extern "C" + résultat récupéré au
join) n’est pas câblé sur *-linux-air. Sur l’hôte, AirThreadBuilder::spawn
existe mais enrobe std::thread et est gardé #[cfg(not(target_env = "air"))]
— donc absent de la cible, qui est précisément celle du PAL.
Verdict : ADDITIF (non bloquant pour le sceau)
Argumentaire :
-
La partie FONDATRICE est déjà là. Le difficile — et le dangereux-si-faux — du spawn Air-natif, c’est le bootstrap TCB/TLS : allouer pile + image TLS,
clone3 + CLONE_SETTLS, faire résoudreerrno/#[thread_local]dans le bloc du thread, joindre parCHILD_CLEARTID/futex. C’est exactement ce qui fige la disposition TLS que le sceau grave — et c’est présent, testé on-target (air_runtime::thread::spawn+create_thread, selftestrt/). Si cette pièce manquait, ce serait bloquant (impossible à ajouter plus tard sans toucher la disposition scellée). Elle ne manque pas. -
Ce qui manque est de la pure composition, au-dessus de deux fondations déjà présentes et à sceller : (a) le spawn natif ci-dessus, (b) l’allocateur global
air_alloc::GlobalAllocator(✅). L’ergonomique = boxer la closure (Box<dyn FnOnce()>viaair-alloc), un trampolineextern "C" fn(usize)qui reconstitue leBoxet l’appelle, et une case tas où déposerT, lue aujoin. Zéro nouvelle surface couche 0, zéro modification d’API scellée. Avecpanic=abort(ADR-049 D9), la « capture de panique au join » est sans objet (un thread qui panique abort le process) : lejoinn’a qu’à rendreT. -
Le modèle
stdlui-même sépare ces deux étages :std::sysfournit leThread::new(stack, main: Box<dyn FnOnce()>)bas-niveau, etstd::threadpose au-dessus le plombage du résultat/handle. Notre découpe est donc alignée sur l’amont : couche 1 = le natif ; le boxage/résultat s’ajoute proprement au-dessus.
Où le poser (décision de couche)
- PAS dans
air-thread. Cycle : l’ergonomique a besoin du spawn natif, qui vit dansair-runtime, lequel dépend déjà d’air-thread(runtime_primitives::create_thread) →air-thread → air-runtimebouclerait. (C’est la contrainte notée en §2.B.2 de la roadmap.) - Recommandé : dans
air-runtime(nouveau point d’entrée, p. ex.air_runtime::thread::spawn_boxed<F, T>(f: F) -> Result<JoinHandle<T>, Errno>), qui : (i) possède déjà le spawn natif et laJoinHandle(cohésion) ; (ii) peut dépendre d’air-allocsans cycle — la dépendanceair-runtime → air-allocexiste déjà (air_runtime::forkappelleair_alloc::reset_after_fork). C’est l’emplacement que la roadmap §2.B.2 anticipe (« le spawn Air-natif viendra d’air-runtime+ allocateur réel »). - Variante acceptable : laisser le plombage
T-au-join dans le toit PAL (std::sys::threadde*-linux-air) et n’exiger de couche 1 que l’acceptation d’une closure boxée (Box<dyn FnOnce()>) — calqué surstd::sys::pal::unix. Dans les deux cas : additif, aucune API scellée touchée.
Conséquence pour le sceau : sceller maintenant le spawn natif + le
GlobalAlloc ; l’ergonomique se pose après en couche additive v1.x. Le sceau
n’a pas à attendre spawn_boxed.
6. VERDICT de sceau-readiness face-PAL
OUI — la couche 1 est brique-complète pour porter le PAL
std::sys.
Justification. Toutes les primitives std::sys fondatrices disposent de
leur brique couche 1, réelle et (pour le code target-only) prouvée on-target :
allocateur global (GlobalAlloc complet), spawn natif avec bootstrap
TCB/TLS (la pièce qui fige la disposition scellée), verrous Mutex/RwLock
sur futex maison + primitives futex exposées, temps monotone/mur + sleep,
réseau (TCP/UDP/Unix + DNS), process (Command/Child/exit), stdio brut (fd 0/1/2 +
isatty + verrou + atfork), chemins octets, argv/env (lecture), random (CSPRNG
kernel), et exit/panic=abort pour la face backtrace/unwind minimale.
Aucun gap BLOQUANT (§3). Les manques recensés (§4) — handle fs::File couche 1,
spawn ergonomique boxé (§5), Condvar/Once, mutation env, newtype os_str,
destructeurs TLS, yield_now/nommage cible — sont tous ADDITIFS : ils se
construisent au-dessus de fondations figées (socle couche-0 scellé + briques
couche-1 fondatrices), sans refonte d’aucune API que le sceau grave. Ils
relèvent du comblement additif §5.3 et/ou du descellement additif v1.x
(modèle ADR-051), pas d’un pré-requis au sceau.
Réserve de fermeture (non-API). Sur aarch64, la fermeture de lien reste
imparfaite tant que le chemin air_auxval(AT_HWCAP) libc-free n’est pas fourni
(§2.5, §2.E.2) — mais c’est une propriété de lien, pas de complétude d’API PAL,
et le random du PAL n’y est pas exposé (lien couche-0 getrandom direct).
7. Synthèse (mémo)
| Face PAL | Verdict |
|---|---|
| Primitives fondatrices | toutes présentes (alloc, spawn natif+TCB/TLS, Mutex/RwLock+futex, time, net, process, stdio, path, args, env-lecture, random, exit) |
| Gaps bloquants | 0 |
| Gaps additifs | fs::File (handle couche 1) · spawn ergonomique boxé · Condvar/Once · env mutation · os_str newtype · dtors TLS · yield/nommage cible |
| §2.B.2 (spawn ergo) | ADDITIF, à poser dans air-runtime (spawn_boxed, dépend d’air-alloc — pas de cycle) ; le natif+TCB/TLS, déjà scellable, suffit au sceau |
| Sceau-readiness face-PAL | OUI |