ADR-052 — air-rt est un objet couche 1 (AirRuntime), jamais un consommateur direct de la couche 0
Statut : Accepté (2026-06-29, ratifié par le BDFL). RFC de correction d’architecture
(ADR-015). Corrige et rend partiellement obsolète
ADR-049 (notamment D8 : « reposant sur la couche 0 ») et
reformule le cadrage « affaire d’air-rt » d’ADR-051.
Companion d’ADR-046/ADR-047 (libc)
et d’ADR-050 (cible *-linux-air).
Catégorie : Architecture (layering, fondateur). Toute évolution structurante = RFC.
Contexte
ADR-049 a posé le runtime userland d’Air (air-rt : TCB/TLS/crt0/environ/spawn) et a écrit,
en D8, qu’il « repose sur la couche 0 » (clone3, mmap, futex, arch_prctl, auxv…).
La phase 2 a été implémentée ainsi : le crate rt/crates/air-rt dépend de air-sys-syscall +
air-sys-types et appelle la couche 0 en direct (tls.rs → arch::set_fs, spawn.rs →
mem::mmap_anonymous/process::clone_thread/futex::futex_wait, process.rs → exit_group).
C’est une violation du layering d’Air, révélée à la relecture (2026-06-29) :
- La couche 0 (
air-sys-types+air-sys-syscall) est la surface bas-niveau au-dessus des syscalls : elle fait passer l’unsafeausafeRust, n’interprète rien, est fidèle à toutes les erreurs du kernel, est Linux (pas « POSIX »),no_std, et ne mappe volontairement que les syscalls récents (kernel ≥ 6.12). Elle ne consomme aucune autre couche. - La couche 1 héberge les objets Rust (purs ; asm quand justifié ; jamais de C) qui parlent au kernel de manière sûre via la couche 0. Doctrine non négociable : seule la couche 1 consomme la couche 0. Personne d’autre. (Cf. macro-architecture §1/§9, « consommation strictement descendante ».)
Or air-rt n’est rien d’autre qu’un objet userland de plus : un runtime. Un runtime de
programme (crt0) n’est pas que de la gestion de thread — c’est aussi la mise en place TLS, le
bloc de contrôle de thread, et, parce que la cible *-linux-air est PIE/static-PIE
(ADR-050), de la relocation (auto-application des
R_*_RELATIVE). Rien là-dedans n’exige de court-circuiter la couche 1 : ce sont des objets Rust
sûrs, exactement la nature de la couche 1.
Constat factuel (2026-06-29) : la couche 1 ne contient ni ThreadControlBlock, ni
ThreadLocalStorage, ni outil de relocation, ni objet AirRuntime. Ils manquent — il
faut les créer en couche 1, pas attaquer la couche 0 depuis le runtime.
Décision
D1 — air-rt (AirRuntime) est un objet couche 1
Le runtime userland d’Air est un objet de couche 1, nommé conceptuellement AirRuntime. Il
ne consomme que des contrats publics de couche 1 et ne dépend jamais de air-sys-syscall
(la surface kernel). Seule exception : air-sys-types — crate de types transverses (cf. D6)
— reste autorisé à toutes les couches. Sa place dans le stack :
libc C-ABI Air / std de *-linux-air ← reposent sur AirRuntime
─────────────────────────────────────────
AirRuntime (TCB + TLS + relocation + crt0 + environ + spawn) ← objet COUCHE 1
s'appuie sur ThreadControlBlock, ThreadLocalStorage, outils de relocation,
et sur air-thread / air-memory / air-process (tous couche 1)
─────────────────────────────────────────
couche 1 : air-thread, air-memory, air-process, … ← SEULES à parler couche 0
─────────────────────────────────────────
couche 0 (surface kernel) : air-sys-syscall ← SEULE la couche 1 la consomme
─────────────────────────────────────────
kernel Linux ≥ 6.12
air-sys-types (types transverses : AirError/Errno, Pid/Tid, repr(C)…)
↳ traverse TOUTES les couches — consommable partout, y compris par AirRuntime (cf. D6)
D2 — Nouveaux objets couche 1 à créer (briques d’AirRuntime)
ThreadControlBlock(TCB) — objet couche 1 : self-pointer,errnothread-local, canari de pile (depuisAT_RANDOM), DTV,tid, mot futex de join ; offset C-ABI figé pour la libc.ThreadLocalStorage(TLS) — objet couche 1 : modèle ELF Variant I (aarch64) / Variant II (x86_64), lecture du gabaritPT_TLS, copie/zéro de l’image, programmation du registre TLS (x86_64 viaair-sys-syscall::arch::set_fsappelé par cet objet couche 1, pas par le runtime ; aarch64 viamsr tpidr_el0, asm encapsulé dans cet objet couche 1).- Outils de relocation (PIE/static-PIE) — objet/module couche 1 : parcours
auxv→AT_PHDR, calcul du bias de chargement, parcours.dynamic(DT_RELA/DT_RELASZ), application desR_*_RELATIVE. Contrainte de bootstrap (aucune donnée relocalisée référencée, pas de panique) = propriété de l’objet couche 1, pas du runtime. AirRuntime— objet couche 1 qui compose (1)+(2)+(3) avec le décodage de la pile initiale (argc/argv/envp/auxv), l’exposition d’environ/args, lespawnstd-free et la terminaison de processus — en s’appuyant sur les objets couche 1 existants (air-threadpourclone3/futex/join,air-memorypourmmap/munmap,air-processpourexit_group…).
D3 — La couche 1 gagne ce qui manque, la couche 0 n’est jamais attaquée depuis le runtime
S’il manque une primitive pour qu’AirRuntime fonctionne, elle est ajoutée en couche 1 (nouvel
objet ou méthode). Interdiction absolue : appeler la couche 0 depuis le runtime / la libc / le
std cible. Les wrappers de syscalls (p. ex. set_fs ajouté par ADR-051) restent en couche 0,
mais leur consommateur est un objet couche 1 (ThreadLocalStorage), jamais le runtime.
D4 — Le crate rt/crates/air-rt : entrée ELF fine, le reste migre en couche 1
Le tree hors-arbre rt/ (ADR-050, nightly + build-std)
conserve uniquement ce qui est intrinsèquement lié à la cible : la spec JSON *-linux-air,
le point d’entrée ELF _start (trampoline global_asm! minimal : capture sp, saute) et la
plomberie build-std. _start appelle AirRuntime (couche 1). Toute la logique de runtime
(TCB/TLS/relocation/crt0/environ/spawn) migre dans le(s) crate(s) couche 1 du workspace stable.
D5 — Frontière avec air-thread
air-thread (couche 1) reste l’objet qui parle à la couche 0 pour clone3/futex/join.
AirRuntime consomme air-thread (et ThreadLocalStorage/ThreadControlBlock) pour le spawn :
le runtime orchestre, il ne syscall pas. La primitive de spawn bas-niveau demeure en
couche 1 (air-thread), conformément à « seule la couche 1 touche la couche 0 ».
D6 — air-sys-types est transverse (consommable par toutes les couches)
La restriction « seule la couche 1 consomme la couche 0 » vise air-sys-syscall (la surface
kernel). air-sys-types est d’une autre nature : il porte les types qui traversent toutes les
couches — AirError/Errno, identifiants (Pid/Tid/Uid/Gid…), structures #[repr(C)]
partagées. Une AirError est une AirError, en couche 0 comme en couche N. À ce titre,
air-sys-types est consommable par n’importe quelle couche (y compris AirRuntime, la libc et le
std cible), sans constituer un « accès couche 0 ». Plus on monte, plus on ajoute d’abstraction ;
mais le vocabulaire de types de base reste commun et partagé. Conséquence concrète : le crate
d’entrée et les objets de runtime peuvent dépendre de air-sys-types, jamais de
air-sys-syscall.
Conséquences
- Doc corrigée : ADR-049 D8 et le cadrage « affaire d’air-rt » d’ADR-051 sont tagués obsolètes
/ reformulés (bannières renvoyant ici) ; la macro-architecture nomme l’objet
AirRuntimeet les briques TCB/TLS/relocation en couche 1 ;rt/README.mdetrt/DECISIONS.mdcorrigés (« repose sur la couche 1 », plus « couche 0 »). - Code à refactorer (suivi séparé, non couvert par cet ADR de doctrine) : créer les objets
couche 1, déplacer la logique de
rt/crates/air-rt, retirer la dépendanceair-sys-syscalldu crate d’entrée, re-router via couche 1. LeCargo.tomld’air-rtne doit plus listerair-sys-syscall; il peut garderair-sys-types(types transverses, D6). - Couverture / barrière : les objets de runtime, devenus couche 1, retombent sous l’exigence
100 % (lignes + branches) et la barrière 2-arches — ils ne sont plus dans le carve-out
rt/. Seuls le_startasm et la plomberie cible restent hors-arbre (nightly). C’est un gain de rigueur : la gap-analysis (rt/DECISIONS.md, 2026-06-29) notait l’absence de gate comme un risque.
Alternatives écartées
- Garder
air-rtsur la couche 0 (statu quo ADR-049 D8) : viole la doctrine de layering ; fait d’air-rtun second consommateur direct du kernel à côté de la couche 1, exactement ce que l’on proscrit. Rejeté — motivation de cet ADR. - Faire d’
air-rtune « couche 0.5 » (entre couche 0 et couche 1) : invente un étage pour légitimer l’accès direct couche 0. Le runtime n’a aucune raison de court-circuiter les objets couche 1 ; il est un objet couche 1. Rejeté. - Déplacer les wrappers de syscalls du runtime dans le crate
air-rt(asm brut hors couche 0) : déjà rejeté par ADR-051 (« tout syscall = wrapper typé couche 0 ») — et ne résout pas le problème, qui est le consommateur, pas l’emplacement des wrappers. Rejeté.
Questions ouvertes (impl, sans impact sur la doctrine)
- Découpage en crates couche 1 : un crate dédié
air-runtimehébergeantAirRuntime+ThreadControlBlock+ThreadLocalStorage+ relocation, ou répartition (TCB/TLS dansair-thread, relocation à part) ? Tranché à l’implémentation. - Réentrée du
_startdansAirRuntime: l’appel d’entrée doit rester reloc-safe (la relocation s’exécute en tout premier) ; détail d’ABI d’appel à figer côté crate couche 1. no_std: ces objets couche 1 restent#.