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-051 — Descellement couche-0-v1.7 : 7 syscalls pour le runtime et la libc

Statut : Accepté (2026-06-28, ratifié par le BDFL — périmètre v1.7 validé ; livraison en une seule PR pour minimiser la CI ARM). RFC d’extension d’une couche scellée (ADR-015). Extension d’une couche scellée (couche-0-v1.6), donc RFC, comme ADR-044 (ppoll, re-sceau v1.5) et ADR-048 (std-free, re-sceau v1.6). Companion d’ADR-049 (runtime, fondation *-linux-air) et d’ADR-046/ADR-047 (libc). Conventions : ADR-021. Couverture/CI : ADR-031/ADR-037.

Catégorie : Couche 0 (extension du socle / re-sceau).

Contexte

Le programme *-linux-air (ADR-049) bute, en phase 2, sur un syscall absent de la couche 0 : programmer le registre TLS du thread principal sur x86_64 exige arch_prctl(ARCH_SET_FS, …). Le descellement de la couche scellée étant une opération rare et coûteuse (re-sceau, 100 % de couverture, barrière 2 arches + CI ARM), le BDFL a décidé (2026-06-28) de regrouper en un seul descellement ce besoin immédiat et les primitives de bas niveau déjà connues comme manquantes pour l’objectif libc (ADR-046), afin d’éviter les cycles répétés scellage/descellage.

Un inventaire vérifié de la couche 0 (lecture des implémentations, pas un simple grep : zéro todo!()/stub, dispatch asm réel, garde-fous 100 % couverture + xtask check_syscalls) a établi que la couche 0 est déjà un layer syscall quasi-complet : I/O fichier synchrones (read/write/pread/ pwrite/*v/lseek/openat/close/statx/getdents64…), mmap/munmap/mprotect, clone3/ clone_thread (avec CLONE_SETTLS), futex, waitid, execve, signaux (signalfd), sockets, ppoll, getrandom, etc. sont réellement implémentés. Les seuls manques pertinents pour la libc se réduisent à deux lots ; ce v1.7 ne prend que le premier (justifié maintenant), le second (v1.8) étant reporté à la phase 5 (cf. § Périmètre reporté).

Décision

Ré-ouvrir la couche 0 une foisre-sceau couche-0-v1.7, en ajoutant exactement 7 syscalls, chacun en fonction(s) dédiée(s) typée(s) (ADR-021 §3 : pas de wrapper multiplexé), Result<_, Errno> ou type infaillible, Option<T> au lieu des sentinelles, sans allocation sur le happy path (ADR-021 §4), newtypes d’identifiants (Pid/Tid).

#Syscallnr x86_64 / aarch64API couche 0 (esquisse)Arches
1arch_prctl158 / —set_fs(base) -> Result<(), Errno> + get_fs() -> Result<usize, Errno> (ARCH_SET_FS=0x1002/ARCH_GET_FS=0x1003)x86_64 uniquement
2set_tid_address218 / 96set_tid_address(clear_tid: Option<&AtomicU32>) -> Tid (rend le TID appelant ; infaillible)2 arches
3getcwd79 / 17getcwd(buf: &mut [u8]) -> Result<&[u8], Errno> (octets, pas UTF-8 présumé — Principe 3 ; buffer fourni, zéro alloc)2 arches
4umask95 / 166umask(new: FileMode) -> FileMode (rend l’ancien masque ; infaillible)2 arches
5getppid110 / 173getppid() -> Pid (infaillible)2 arches
6sched_yield24 / 124sched_yield() (infaillible sous Linux)2 arches
7getrusage98 / 165getrusage(who: RusageWho) -> Result<Rusage, Errno> (Rusage/RusageWho = nouveaux types #[repr(C)] air-sys-types : SELF/CHILDREN/THREAD)2 arches

Asymétrie d’arche assumée (1). arch_prctl n’existe que sur x86_64. Sur aarch64, le registre TLS (tpidr_el0) est inscriptible directement en EL0 par une instruction msr — c’est l’affaire de l’objet ThreadLocalStorage (couche 1) (asm encapsulé dans cet objet ; cf. ADR-052), pas un syscall couche 0. Donc le thread principal x86 passe par set_fs (couche 0, appelé par l’objet TLS couche 1) ; le thread principal aarch64 par msr tpidr_el0 (objet TLS couche 1). Les threads spawnés (2 arches) reçoivent leur registre TLS du kernel via CLONE_SETTLS (déjà supporté par clone_thread).

⚠️ Note layering (ADR-052, 2026-06-29). Les 7 wrappers ajoutés ici restent en couche 0 (un wrapper de syscall y est à sa place). Ce qui est corrigé, c’est leur consommateur : ce ne sont pas air-rt / le runtime qui les appellent en direct, mais les objets couche 1 (ThreadLocalStorage pour set_fs, air-process pour set_tid_address/getppid/…). Partout où cet ADR écrit « air-rt » comme appelant d’un syscall, lire « l’objet couche 1 concerné ». set_fs/get_fs sont donc #[cfg(target_arch = "x86_64")] et déclarés tels quels (cohérent avec l’esprit UNSUPPORTED.md pour le « pas d’équivalent aarch64 »).

Emplacement. getcwd près de chdir/fchdir (process) ; set_tid_address/getppid/sched_yield/ getrusage/umask en process.rs ; set_fs/get_fs en module arch dédié ou process.rs (détail d’impl). Specs docs/specs/layer-0/ mises à jour ; xtask check_syscalls complété (numéros vs uapi, 2 arches).

Stratégie de test (100 %, sans gaming ADR-035)

Six des sept sont triviaux à couvrir (appel direct + assertions). Le cas délicat = arch_prctl(SET_FS) : le modifier dans le binaire de test (lié à glibc) corromprait le TLS de glibc-std (c’est l’unsoundness même qui motive *-linux-air, #151). Parade, sans exception de couverture :

  • get_fs : lecture pure, sûre.
  • set_fs succès : set_fs(get_fs()) — réécrit la base courante (no-op effectif) → exerce le chemin succès sans rien casser.
  • set_fs erreur : adresse non canonique → le kernel rejette (EINVAL/EPERM) sans changer %fs → exerce le bras d’erreur sûrement.

C’est une « stratégie de test complexe » bienvenue (CLAUDE.md) ; aucune entrée COVERAGE-EXCEPTIONS.md requise.

Critères de sortie (identiques aux re-sceaux précédents)

  • #![no_std] préservé (compile std-free), 100 % lignes + branches, couvrable-vide VIDE.
  • Tests unitaires + property-based + (le cas échéant) injection de fautes ; // SAFETY: sur chaque bloc asm.
  • Barrière speedy + carbon verte et CI ARM verte ; check_syscalls à jour (2 arches).
  • Vérification indépendante 2 arches de tout asm/numéro avant push (leçon c_char i8/u8, x19 LLVM).
  • Jalon re-scellé couche-0-v1.7 ; SUMMARY/registre ADRs/INDEX cohérents.

Périmètre reporté à couche-0-v1.8 (phase 5, avec OpenSSH comme consommateur)

Volontairement hors v1.7, car leur API typée (ADR-021 : une fonction dédiée par opération ioctl) ne peut être bien conçue qu’avec son consommateur réel, et écrire 100 % de couverture sur du code que rien n’appelle = travail spéculatif sujet au churn :

  • termios (ioctl typé : tcgetattr/tcsetattr/…, TIOCGWINSZ/TIOCSWINSZ) ;
  • PTY (/dev/ptmx, ptsname/unlockpt/grantpt via ioctl) ;
  • rt_sigaction/sigaltstack (handlers POSIX du C porté) — nécessite une décision vis-à-vis d’ADR-020 (signalfd par défaut) ;
  • au besoin : recvfrom/sendto, set_robust_list, membarrier.

Le voyage libc ne comporte ainsi que deux descellements (v1.7 maintenant, v1.8 à la phase 5), pas une multitude — l’objectif « éviter les cycles » est tenu sans pré-construire à l’aveugle.

Conséquences

  • Débloque la phase 2 (TLS du thread principal x86 via set_fs).
  • Pré-équipe la libc des primitives universelles manquantes (getcwd/umask/getppid/sched_yield/ getrusage/set_tid_address) — testables à 100 % immédiatement, sans churn.
  • Réintroduit de l’asm (dispatch des 7 syscalls, 2 arches sauf arch_prctl) — minimal, audité // SAFETY:.
  • Coût d’un re-sceau (gate complet) amorti sur 7 ajouts au lieu d’un.

Alternatives écartées

  • Micro-descellements successifs (un par besoin) : multiplie le coût du gate et les re-sceaux. Rejeté (motivation même de cet ADR).
  • Tout mettre en v1.7 (termios/PTY/sigaction inclus) : API spéculative sans consommateur → churn + 100 % de couverture sur du code mort. Reporté en v1.8 (conçu avec OpenSSH).
  • arch_prctl en syscall brut dans air-rt (hors couche 0) : viole le layering (tout syscall = wrapper typé couche 0). Rejeté.
  • wrfsbase (FSGSBASE) pour %fs au lieu d’arch_prctl : dépend d’une feature CPU/kernel non universelle → fragile. Rejeté ; arch_prctl est la voie portable et correcte.

Questions ouvertes

  1. Type FileMode/Rusage/RusageWho : réutiliser les types existants d’air-sys-types ou en introduire — tranché à l’implémentation (sans impact ABI couche 0, types internes).
  2. Découpage en PRs : tranché — une seule PR pour les 7 syscalls + re-sceau (décision BDFL : la CI ARM étant longue, chaque PR la redéclenche → on minimise à un unique cycle de gate).