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 fois → re-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).
| # | Syscall | nr x86_64 / aarch64 | API couche 0 (esquisse) | Arches |
|---|---|---|---|---|
| 1 | arch_prctl | 158 / — | set_fs(base) -> Result<(), Errno> + get_fs() -> Result<usize, Errno> (ARCH_SET_FS=0x1002/ARCH_GET_FS=0x1003) | x86_64 uniquement |
| 2 | set_tid_address | 218 / 96 | set_tid_address(clear_tid: Option<&AtomicU32>) -> Tid (rend le TID appelant ; infaillible) | 2 arches |
| 3 | getcwd | 79 / 17 | getcwd(buf: &mut [u8]) -> Result<&[u8], Errno> (octets, pas UTF-8 présumé — Principe 3 ; buffer fourni, zéro alloc) | 2 arches |
| 4 | umask | 95 / 166 | umask(new: FileMode) -> FileMode (rend l’ancien masque ; infaillible) | 2 arches |
| 5 | getppid | 110 / 173 | getppid() -> Pid (infaillible) | 2 arches |
| 6 | sched_yield | 24 / 124 | sched_yield() (infaillible sous Linux) | 2 arches |
| 7 | getrusage | 98 / 165 | getrusage(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 (ThreadLocalStoragepourset_fs,air-processpourset_tid_address/getppid/…). Partout où cet ADR écrit « air-rt » comme appelant d’un syscall, lire « l’objet couche 1 concerné ».set_fs/get_fssont donc#[cfg(target_arch = "x86_64")]et déclarés tels quels (cohérent avec l’espritUNSUPPORTED.mdpour 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_fssuccès :set_fs(get_fs())— réécrit la base courante (no-op effectif) → exerce le chemin succès sans rien casser.set_fserreur : 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-videVIDE.- 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_chari8/u8,x19LLVM). - 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/grantptvia 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_prctlen syscall brut dans air-rt (hors couche 0) : viole le layering (tout syscall = wrapper typé couche 0). Rejeté.wrfsbase(FSGSBASE) pour%fsau lieu d’arch_prctl: dépend d’une feature CPU/kernel non universelle → fragile. Rejeté ;arch_prctlest la voie portable et correcte.
Questions ouvertes
- 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). - 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).