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 :
-
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= recherchePATH+execve, couche 1),gethostname/sethostname, etkillpg(=kill(2)avec unPidnégatif = groupe ;air_sys_syscall:: signal::killtransmet déjàpid.as_raw()brut au syscall,Pid::try_from_rawn’exclut que0).setsid/waitidsont présents (leur difficulté est de couverture — enfant forké, cf.docs/COVERAGE-EXCEPTIONS.md— pas un manque). -
Manque en couche 0 → objet du présent ADR (§ Décision).
-
Relève de la couche 1 (descellée, additifs libres — ADR-062) :
getauxval(stockage de l’auxvcapturé au_startparair-runtime+ table de requête ; TARGET-ONLY — l’auxvn’existe qu’à l’entrée du programme cible),__xpg_strerror_r(strerror_r— mise en forme d’Errno),__tls_get_addr(TLS dynamique, runtime), etsysconf(agrégat : constantes +getauxvalpour_SC_PAGESIZE/AT_PAGESZ+sched_getaffinitypour_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. ioctl — pas 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) ;FIONBIO→set_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 unioctlbrut) ;requestinconnu →ENOTTYdé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(tableauxvdansair-runtime),sysconf(agrégat),strerror_r,__tls_get_addr. Traités par additifscouche-1-v1.xau 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/sigaltstackdivergent 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 specfamily-*concernée. - Débloque : le lot mémoire libc (
mmap/mprotect/munmap— critique pour questdlie : 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).
ioctlgénérique en couche 0 : viole ADR-021 conv. 3. Rejeté (dispatcher libc).munmapvia un registre couche 1 (addr →Mapping) : non fidèle (échoue sur unmunmappartiel ou d’une région étrangère), verrou global sur chaquemmap/munmap. Rejeté au profit dumunmap_rawfidèle au kernel.