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-085 — Descellement additif couche-0 cumulé : primitives manquantes pour la face libc (chantier B — std + OpenSSH)

Statut : Accepté (ratification BDFL 2026-07-08). Périmètre implémenté (100 % couverture, 2 arches) et re-scellé couche-0-v1.11 (2026-07-09, tag signé posé sous délégation ADR-065) : chroot, kill_process_group (→ killpg), sigaltstack (+ type AltStack), sendfile, fchmod/fchown/futimens, munmap_raw. pause sans additif (réutilise ppoll). Descellement additif de la couche 0 scellée couche-0-v1.10, sous la délégation des re-sceaux additifs du superviseur (ADR-065), cadre libc ADR-051, RFC d’extension de couches scellées (ADR-015). Sert le chantier B (lier std sur libair, ADR-076) et la campagne libc scopée OpenSSH (ADR-046/ADR-047).

Catégorie : Descellement additif de la couche 0. Aucun symbole existant modifié. Contrairement aux descellements par-symbole (ADR-070/ADR-071), celui-ci cumule en un seul acte tout le manque couche 0 restant de la campagne (directive BDFL 2026-07-08 : un ADR, un descellement, une production, un re-sceau). Tag de re-sceau couche-0-v1.11 DIFFÉRÉ jusqu’à ce que tout le périmètre ci-dessous soit implémenté (100 % couverture, 2 arches) et ratifié.

Contexte

La campagne libc (fichier, env, process de base) a épuisé les symboles bindables sur la couche 0 existante — 34/71 symboles std fournis sans toucher la couche 0. Les symboles restants se répartissent en trois classes :

  1. Bindables sur l’existant couche 0 (aucun additif) — livrés au fil de l’eau : readv/writev/preadv/pwritev/splice (I/O vectorisée), execve/execveat (execvp = recherche PATH + execve, couche 1), gethostname/sethostname, et killpg (= kill(2) avec un Pid négatif = groupe ; air_sys_syscall:: signal::kill transmet déjà pid.as_raw() brut au syscall, Pid::try_from_raw n’exclut que 0). setsid/waitid sont présents (leur difficulté est de couverture — enfant forké, cf. docs/COVERAGE-EXCEPTIONS.md — pas un manque).

  2. Manque en couche 0 → objet du présent ADR (§ Décision).

  3. Relève de la couche 1 (descellée, additifs libres — ADR-062) : getauxval (stockage de l’auxv capturé au _start par air-runtime + table de requête ; TARGET-ONLY — l’auxv n’existe qu’à l’entrée du programme cible), __xpg_strerror_r (strerror_r — mise en forme d’Errno), __tls_get_addr (TLS dynamique, runtime), et sysconf (agrégat : constantes + getauxval pour _SC_PAGESIZE/AT_PAGESZ + sched_getaffinity pour _SC_NPROCESSORS_ONLN). Non traités ici.

Multiplier les descellements par-symbole (7+ ADR distincts) alourdirait la gouvernance. La BDFL tranche : cumuler.

Décision

La couche 0 expose les fonctions publiques suivantes, toutes conformes aux conventions couche 0 (ADR-021) : Result<_, Errno>, Option<T> au lieu des sentinelles, FD emprunté (BorrowedFd), EINTR remonté, aucune allocation happy-path, une fonction dédiée typée par opération (jamais de wrapper multiplexé). Regroupées par famille :

1. Mémoire — libération par (adresse, longueur)

Le C fait munmap(void *addr, size_t len) ; la couche 0 n’offre que munmap(mapping: Mapping) RAII (champs privés addr/len, pas de constructeur public). La face libc possède l’adresse et la longueur, pas l’objet Mapping.

#![allow(unused)]
fn main() {
// crates/air-sys-syscall/src/mem.rs
/// `munmap(2)` par (adresse, longueur) brutes — pour la face libc, qui gère la vie du
/// mapping par (addr, len) (comme le C), non par l'objet RAII `Mapping`.
///
/// # Safety
/// `address`/`length` désignent une région **effectivement mappée** (contrat C).
pub unsafe fn munmap_raw(address: NonNull<u8>, length: usize) -> Result<(), Errno>;
}

(mmap et mprotect sont déjà bindables : mmap_anonymous/mmap_file rendent un Mapping dont on extrait l’adresse — la couche 1 air-memory::raw_mapping neutralise déjà le Drop ; mprotect(NonNull, len, prot) est déjà public.)

2. Métadonnées par descripteur

fchmodat/fchownat (par chemin) existent ; les variantes par fd (que fchmod/ fchown C exigent) manquent.

#![allow(unused)]
fn main() {
// crates/air-sys-syscall/src/fs.rs
pub fn fchmod(fd: BorrowedFd<'_>, mode: Mode) -> Result<(), Errno>;
pub fn fchown(fd: BorrowedFd<'_>, uid: u32, gid: u32) -> Result<(), Errno>;
/// Horodatages **par fd** (`utimensat(fd, NULL, …)`) — socle de `futimens`. La variante
/// `utimensat` existante prend un `&CStr` (jamais `NULL`) ; celle-ci opère sur le fd.
pub fn futimens(fd: BorrowedFd<'_>, atime: UtimeValue, mtime: UtimeValue) -> Result<(), Errno>;
}

3. Confinement

#![allow(unused)]
fn main() {
// crates/air-sys-syscall/src/process.rs (ou fs.rs)
pub fn chroot(path: &CStr) -> Result<(), Errno>;
}

4. Ordonnancement / affinité CPU

#![allow(unused)]
fn main() {
// crates/air-sys-syscall/src/process.rs
/// Masque d'affinité CPU du processus `pid` (`None` = courant). `CpuSet` = newtype
/// couche 0 (bitset borné) — jamais un `cpu_set_t` brut. Socle de `sched_getaffinity`
/// et de `sysconf(_SC_NPROCESSORS_ONLN)`.
pub fn sched_getaffinity(pid: Option<Pid>, set: &mut CpuSet) -> Result<(), Errno>;
pub fn sched_setaffinity(pid: Option<Pid>, set: &CpuSet) -> Result<(), Errno>;
}

5. Attente d’un signal (pause)

pause(2) n’a pas de numéro de syscall sur aarch64 (seulement x86_64). Une fonction couche 0 portable l’implémente via rt_sigsuspend avec le masque courant (aarch64) / SYS_pause (x86_64).

#![allow(unused)]
fn main() {
// crates/air-sys-syscall/src/signal.rs
/// Suspend le thread jusqu'à la délivrance d'un signal (portable 2 arches). Rend
/// toujours `Err(EINTR)` en succès (POSIX `pause` rend `-1`/`EINTR`).
pub fn pause() -> Result<Infallible, Errno>;
}

6. Pile de signal alternative (sigaltstack)

#![allow(unused)]
fn main() {
// crates/air-sys-syscall/src/signal.rs
/// Installe/interroge la pile alternative de signal. `AltStack` = newtype couche 0
/// (base + taille + drapeaux typés), jamais un `stack_t` brut. Requis par `std`
/// (gestion du débordement de pile via `SIGSEGV` sur pile alternative).
pub fn sigaltstack(new: Option<&AltStack>, old: Option<&mut AltStack>) -> Result<(), Errno>;
}

7. Transfert zéro-copie (sendfile)

#![allow(unused)]
fn main() {
// crates/air-sys-syscall/src/fs.rs (ou un module io)
/// `sendfile(2)` : copie `count` octets de `in_fd` vers `out_fd` dans le kernel.
/// `offset` : `Some` = position explicite mise à jour (rendue) ; `None` = position
/// courante de `in_fd`. Rend le nombre d'octets transférés.
pub fn sendfile(
    out_fd: BorrowedFd<'_>,
    in_fd: BorrowedFd<'_>,
    offset: Option<&mut u64>,
    count: usize,
) -> Result<usize, Errno>;
}

8. ioctlpas de descellement couche 0 (résolution doctrinale)

Le C fournit ioctl(fd, request, …) variadique et multiplexé — exactement ce qu’ADR-021 conv. 3 interdit en couche 0 (« pas de wrapper générique pour syscalls multiplexés »). On préserve la doctrine : la couche 0 ne reçoit aucun ioctl générique. Le symbole libc ioctl est un dispatcher de la face libc (couche 1/toit) qui route chaque request connu vers l’opération typée correspondante déjà exposée :

  • TIOCGWINSZ/TIOCSWINSZ → famille termios/tty couche 0 (ADR-060) ;
  • FIONBIOset_status_flags/fcntl(O_NONBLOCK) ;
  • FIONREAD/SIOCGIFxxx… → op typée dédiée si requise par le périmètre OpenSSH (ajout ponctuel, typé, jamais un ioctl brut) ;
  • request inconnu → ENOTTY déterministe (jamais un comportement deviné).

Ainsi la couche 0 reste pure ; seule la libc porte la multiplexion (frontière C, unsafe localisé). Cette route est notée ici mais implémentée côté libc, hors du présent descellement.

Ce que ce descellement N’inclut PAS

  • Couche 1 (descellée, additifs libres) : getauxval (table auxv dans air-runtime), sysconf (agrégat), strerror_r, __tls_get_addr. Traités par additifs couche-1-v1.x au fil des lots libc, sans ADR de descellement (la couche 1 est descellée jusqu’au re-sceau groupé couche-1-v1.7).
  • ioctl : dispatcher libc, pas couche 0 (§ 8).
  • Toute opération hors périmètre std/OpenSSH : différée (UNSUPPORTED.md).

Conséquences

  • Tests : chaque additif = 100 % lignes + branches, property-based / fuzz si entrée externe, validé 2 arches (x86_64 + aarch64 — pause/sigaltstack divergent par arch). munmap_raw/sendfile/sched_*/sigaltstack/pause : harnais custom (mappings réels, enfants forkés, masques de signaux) comme le reste de la couche 0.
  • Re-sceau : après implémentation complète et validation 2 arches, le superviseur pose le tag couche-0-v1.11 (additif ; aucun symbole existant touché, ABI préservée). Mise à jour d’UNSUPPORTED.md (retrait des entrées désormais couvertes) et de la spec family-* concernée.
  • Débloque : le lot mémoire libc (mmap/mprotect/munmap — critique pour que std lie : allocateur, piles de threads, pile de signal) et les symboles fichier/ process/signal restants, sur les Managers de domaine (AirFileManager/ AirProcessManager/…, ADR-077).

Alternatives rejetées

  • Descellements par-symbole (façon ADR-070/071) : 7+ ADR, gouvernance lourde, re-sceaux multiples. Rejeté (directive BDFL de cumul).
  • ioctl générique en couche 0 : viole ADR-021 conv. 3. Rejeté (dispatcher libc).
  • munmap via un registre couche 1 (addr → Mapping) : non fidèle (échoue sur un munmap partiel ou d’une région étrangère), verrou global sur chaque mmap/munmap. Rejeté au profit du munmap_raw fidèle au kernel.