ADR-048 — Descellement unique de la couche 0 : std-free certifié + famille futex(2) (re-sceau couche-0-v1.6)
Statut : Accepté (2026-06-28, validé par le BDFL). Companion d’ADR-044
(méthode de re-sceau de la couche 0), d’ADR-046 et d’ADR-047
(la libc — pourquoi on exige std-free + un futex(2)), et de l’ADR-021
(conventions couche 0). Décline le Principe 4 (no_std-future, errno sans alloc). S’appuie sur
l’audit docs/notes/audits/audit-couche0-std-free-fr.md.
Catégorie : Architecture — couche 0 (re-sceau). Évolution structurante de la couche scellée → RFC (ADR-015).
Contexte
Deux constats convergents :
- La couche 0 devait être
std-free (Principe 4) — elle ne l’est pas. L’audit montre queair-sys-typesetair-sys-syscallsontstdpar défaut (pas de#![no_std]) et dépendent destden code lib (std::os::fd,std::ffi::CString/OsString,std::sync::{Arc,Mutex}). - La libc Air (ADR-046/047) exige un substrat threading
std-free (air-threadréimplémenté sur la couche 0), ce qui requiert unfutex(2)classique (FUTEX_WAIT/WAKE sur&AtomicU32) absent de la couche 0 (le seul futex présent est via io_uring, trop lourd pour unMutexbasique). Or la couche 0 a elle-même besoin de cefutex(2)pour remplacer lestd::sync::Mutexde son état io_uring partagé.
Desceller une couche scellée a un coût (re-sceau, validation 2-arches, ABI). On regroupe donc
TOUT en un seul descellement pour ne re-sceller qu’une fois, certifié std-free.
Décision
D1 — Descellement unique, re-sceau couche-0-v1.6
Un seul descellement (depuis couche-0-v1.5) couvrant tout le périmètre ci-dessous. Le découpage
en tâches/PR est autorisé, mais le re-sceau couche-0-v1.6 ne se referme que lorsque TOUT est
fait et certifié std-free.
D2 — Conversion std-free (no_std + alloc)
#![no_std]+extern crate allocdans les deuxlib.rs.std::os::fd::*→core::os::fd::*(mécanique, ~22 modules).std::ffi::CString→alloc::ffi::CString;std::ffi::CStr→core::ffi::CStr.std::sync::Arc→alloc::sync::Arc.std::ffi::OsString/OsStr→ type octets (Unix : nom de fichier = octets, peut être non-UTF-8) — possédéalloc::vec::Vec<u8>/ emprunté&[u8], cohérent avec « chemins = octets ».allocest toléré pour les types possédés exigés (CString, nom deDirEntry…) ; le happy path reste sans alloc et errno resteNonZeroI32sans alloc (Principe 4 / ADR-021 inchangés).
D3 — Nouvelle famille futex(2)
Famille couche 0 dédiée : futex_wait / futex_wake (et requeue/cmp_requeue si justifiés), sur
&core::sync::atomic::AtomicU32. Une fonction typée par opération (pas de multiplexeur futex(op,…),
ADR-021 §3). EINTR remonté à l’appelant, sans retry (ADR-021 §2). Sert air-thread (couche 1,
libc) et le mutex de la couche 0 (D5).
D4 — clone3 : support du spawn de thread
Vérifier et, si besoin, étendre le wrapper clone3 (couche 0) pour exprimer les arguments de
création de thread : flags CLONE_VM|CLONE_THREAD|CLONE_SETTLS|CLONE_CHILD_CLEARTID|CLONE_PARENT_SETTID,
pile enfant, pointeur TLS, child_tid (pour le join via CHILD_CLEARTID/futex). (Le
bootstrap TLS + trampoline d’entrée du thread enfant relèvent de la couche 1 air-thread — hors de ce
re-sceau ; D4 garantit seulement que la couche 0 expose ce qu’il faut.)
D5 — Mutex io_uring sur futex(2)
Remplacer le std::sync::Mutex/PoisonError de io_uring/shared.rs par un mutex futex-maison
(au-dessus de D3). Supprime la dernière dépendance std::sync du code lib.
D6 — Critère d’acceptation du re-sceau (porte de sortie)
Le re-sceau couche-0-v1.6 n’est prononcé que si TOUT est vert :
cargo buildavec#![no_std]réussit (les deux crates) ⇒ preuve par le compilateur qu’il ne reste aucunstden code lib ;- couverture 100 % (lignes + branches) maintenue (couche 0) ;
- barrière 2-arches (x86_64 + aarch64) + CI ARM vertes ;
- les nouveaux symboles (
futex(2), éventuels ajoutsclone3) ont leurs tests + property/fuzz au standard couche 0, et leurs commentaires// SAFETY:.
Conséquences
air-thread(couche 1) pourra être réimplémentéstd-free sur la couche 0 (clone3 + futex), ce qui débloque le harnaisno_stdde la méthode libc (ADR-047 : thread Rust-pur-sur-couche-1 ∥ thread chemin-C, même behaviour).- La couche 0 redevient conforme au Principe 4, durablement (gardé par le critère
#![no_std]). - ABI : ajout de symboles (
futex(2)) ; la migrationstd::os::fd→core::os::fdest sans impact ABI (mêmes types, même layout —core::os::fdest la définition questdré-exporte).
Alternatives écartées
- Desceller plusieurs fois (std-free puis futex séparément) : rejeté — coût/risque de re-sceaux multiples ; D1 impose un descellement unique.
- Mutex via le futex io_uring existant : rejeté — un
Mutexbasique ne doit pas dépendre d’un anneau io_uring (poids, cycle de vie). - Spinlock au lieu de futex : rejeté — pas de blocage, gaspillage CPU, inversion de priorité.
- Garder
stden couche 0 : rejeté — viole le Principe 4 et contaminerait la libc (std tire la glibc), rendant impossible le harnaisno_std(ADR-047).
Questions ouvertes
- Opérations
futexexactes à exposer (WAIT/WAKE suffisent-ils, ou REQUEUE/CMP_REQUEUE/PI dès v1.6 ?). - Forme précise du type octets remplaçant
OsString(newtype dédié vsVec<u8>nu). - Le wrapper
clone3actuel exprime-t-il déjà tout D4, ou faut-il l’étendre (impact ABI à vérifier) ?
Amendement 1 — 2026-06-28 (ratifié BDFL) : core::os::fd n’existe pas → internalisation des types FD
Constat empirique (rustc 1.96 stable) : use core::os::fd::OwnedFd; produit error[E0432]: could not find fd in os. core::os::fd n’existe PAS en stable ; les types FD (OwnedFd/BorrowedFd/RawFd + traits AsFd/AsRawFd/FromRawFd/IntoRawFd) ne vivent que dans std::os::fd.
Correction de D2 et de la Conséquence « sans impact ABI » — les deux étaient FAUX pour les FD :
- La migration
std::os::fd→core::os::fdn’est ni possible ni mécanique. - Pour atteindre
#![no_std], Air doit internaliser ses propres types FD :RawFd,BorrowedFd<'fd>,OwnedFd(Drop → close(2)) + traits, comme la specair-sys-types.md(l.74-90) le prévoit déjà — lepub use std::os::fdactuel est un raccourci/dette qui dévie de la spec. - C’est une introduction de type air-stable (Principe 8, ADR-012, ABI 10 ans) → AVEC impact ABI (baseline
air-abi-checkà poser), PAS mécanique. ~88 sites lib + interop de tests à reprendre. - Point d’archi :
OwnedFd::dropexige unclose(2)minimal dans le crateair-sys-types(un seul syscall, conforme à la spec).
Tâche AJOUTÉE au cycle v1.6, AVANT le reste de D2 :
- Tâche 2a — Internalisation des types FD (air-stable) : définir
RawFd/BorrowedFd/OwnedFd+ traits +close(2), migrer les ~88 sites, interop tests, baselineair-abi-check. Miroir exact de la sémantiquestd(interop triviale viaRawFd+ layout ABI-stable). - Une fois 2a fait, le reste de D2 (CString→alloc, CStr→core, Arc→alloc, OsString→octets,
#![no_std]+alloc) redevient mécanique (piloté par le compilateur).
Déjà acquis : Tâche 1 futex(2) (#147, mergé) ; D5 mutex FutexMutex (5ca6fe8, livré).
Ordre v1.6 : futex(2) ✅ → FD (2a) → reste de D2 → #![no_std] → re-sceau v1.6 (D6 inchangé) → puis clone3 spawn-thread (D4 / Tâche 3).