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-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.rsarch::set_fs, spawn.rsmem::mmap_anonymous/process::clone_thread/futex::futex_wait, process.rsexit_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’unsafe au safe Rust, 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)

  1. ThreadControlBlock (TCB) — objet couche 1 : self-pointer, errno thread-local, canari de pile (depuis AT_RANDOM), DTV, tid, mot futex de join ; offset C-ABI figé pour la libc.
  2. ThreadLocalStorage (TLS) — objet couche 1 : modèle ELF Variant I (aarch64) / Variant II (x86_64), lecture du gabarit PT_TLS, copie/zéro de l’image, programmation du registre TLS (x86_64 via air-sys-syscall::arch::set_fs appelé par cet objet couche 1, pas par le runtime ; aarch64 via msr tpidr_el0, asm encapsulé dans cet objet couche 1).
  3. Outils de relocation (PIE/static-PIE) — objet/module couche 1 : parcours auxvAT_PHDR, calcul du bias de chargement, parcours .dynamic (DT_RELA/DT_RELASZ), application des R_*_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.
  4. 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, le spawn std-free et la terminaison de processus — en s’appuyant sur les objets couche 1 existants (air-thread pour clone3/futex/join, air-memory pour mmap/munmap, air-process pour exit_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 couchesAirError/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 AirRuntime et les briques TCB/TLS/relocation en couche 1 ; rt/README.md et rt/DECISIONS.md corrigé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épendance air-sys-syscall du crate d’entrée, re-router via couche 1. Le Cargo.toml d’air-rt ne doit plus lister air-sys-syscall ; il peut garder air-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 _start asm 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-rt sur la couche 0 (statu quo ADR-049 D8) : viole la doctrine de layering ; fait d’air-rt un 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-rt une « 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)

  1. Découpage en crates couche 1 : un crate dédié air-runtime hébergeant AirRuntime + ThreadControlBlock + ThreadLocalStorage + relocation, ou répartition (TCB/TLS dans air-thread, relocation à part) ? Tranché à l’implémentation.
  2. Réentrée du _start dans AirRuntime : 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.
  3. no_std : ces objets couche 1 restent #![no_std] (cohérent ADR-048/049 D8 sur le no_std, seul point de D8 conservé).