ADR-044 — Extension de la couche 0 : famille poll (ppoll) pour l’attente synchrone bornée et interruptible (re-sceau couche-0-v1.5)
Statut : Accepté (2026-06-26, validé par le BDFL). RFC d’extension d’une couche
scellée (ADR-015) : ajoute une nouvelle famille au socle syscall — ce n’est
pas un complément additif type Errno (qui, lui, ne requiert pas de RFC). S’appuie
sur le Principe 1 (rigueur des couches fondatrices), l’ADR-021 (conventions couche 0),
l’ADR-032 (zéro discard) et l’ADR-037 (validation x86 par barrière + CI ARM). Cadrage
détaillé : docs/draft/wait-timeout-design-fr.md.
Catégorie : Couche 0 (extension du socle syscall — RFC).
Contexte
La couche 0 a été scellée comme complète (11 familles + io_uring ; tags
couche-0-v1 → v1.4). Elle n’expose aucun wrapper poll/ppoll/epoll
synchrone.
L’incident fork/io_uring du 2026-06-25 (PR #128) a révélé un footgun de conception :
AirProcess::wait() fait un waitid bloquant non borné sur un pidfd ; si l’enfant
ne se termine jamais (deadlock), le parent hang indéfiniment. La correction de fond
exige, côté couche 1, une attente bornée et/ou interruptible (cf.
wait-timeout-design-fr.md : wait_timeout/wait_until/wait_forever).
Un pidfd devient lisible (POLLIN) à la terminaison de l’enfant ; mais waitid
n’a pas de timeout. Pour une attente synchrone bornée/interruptible, il faut
attendre la lisibilité d’un ou plusieurs fd avec une échéance — c’est exactement
ppoll(2). Sans lui, la seule alternative serait de tirer io_uring dans
air-process (allocation, lourd, conceptuellement couche 2) — rejeté.
Ajouter un syscall/une famille au socle scellé évolue le socle → relève de l’RFC (ADR-015), d’où le présent ADR (et non un simple jalon de re-sceau additif).
Décision
-
Nouvelle famille couche 0
poll(module dédié), extensible plus tard verspoll/select/epollsi un besoin réel apparaît. Chaque opération = une fonction dédiée typée (ADR-021 §3 — pas de wrapper multiplexé). -
Wrapper
ppoll(et nonpoll) :#![allow(unused)] fn main() { pub fn ppoll( fds: &mut [PollFd<'_>], timeout: Option<Duration>, // None → NULL (infini) ; ZERO → sondage sigmask: Option<&SignalMask>, // None → NULL (réservé usage futur) ) -> Result<usize, Errno>; // nb de fd à revents non vide ; 0 = expiration }EINTRremonté à l’appelant, jamais de retry (ADR-021 §2 ; la reprise vit en couche 1).- Conversion
Duration→timespecdéfensive :tv_secviai64::try_fromclampé ài64::MAXsi débordement (jamaisas; Principe 2) ;tv_nsec=subsec_nanos(u32 → i64sûr). - Choix
ppoll(paspoll) : précision ns (aligneDuration),timeoutnon muté au retour, sigmask atomique — la variante moderne (ADR-021 « variantes modernes préférées » ; doctrine « kernel = bible »).
-
Nouveau type
PollFd<'fd>:BorrowedFd<'fd>+eventsdemandés +reventsremplis par le kernel. Emprunté (+0), aucune prise d’ownership. Lesreventssont restitués intégralement à l’appelant (ADR-032 — l’information du kernel n’est pas masquée). -
Promotion de
PollEventshors du moduleio_uringvers le modulepoll, ré-exporté depuisio_uring: zéro rupture, type inchangé sur le fil (réorganisation interne d’une couche scellée — d’où le re-sceau). -
Re-sceau
couche-0-v1.5: jalons antérieurs (v1→v1.4) conservés.check-syscallsajoute le numéroppoll(x86_64 271 / aarch64 73), certifié sur les 2 arches. Couverture 100 % lignes+branches hors exceptions ADR-035 (EINTRprobable STRUCTURAL si non injectable de façon déterministe, cf. précédentwaitid). Fuzz non requis (pas de parsing d’entrée externe : entrées = fd + durée). -
Gate :
cargo xtask barriersur speedy ET carbon + CI ARM (raspi-srv-2), les trois verts (ADR-037), avant merge.
Conséquences
- + Débloque l’attente synchrone bornée/interruptible de
air-process(wait_timeout/wait_until/wait_forever) sans io_uring ni allocation — cohérent avec une couche 1 synchrone. - + Brique fidèle au kernel :
ppollest le mécanisme kernel d’attente multi-fd bornée ; on l’expose typé sans en distordre la sémantique (doctrine « kernel = bible », re-présentationResult/Option/PollFd). - + La supervision asynchrone de N pidfds (reactor io_uring) reste, comme prévu, en couche 2 ; cet ADR ne traite que le synchrone.
- − Évolution d’une couche scellée → re-sceau
v1.5, barrière 2 arches + CI ARM à repasser au vert (coût assumé, Principe 1). - −
PollEventsdéménage (réorg interne) ; atténué par le ré-export (zéro rupture d’appelant).
Alternatives écartées
- io_uring (
poll_add+link_timeout) pour l’attente : rejeté — tire un ring entier dansair-process(allocation, complexité), conceptuellement couche 2. poll(2)au lieu deppoll: rejeté — précision ms,timeoutmuté au retour, pas de sigmask ;ppollest la variante moderne.timerfd+ attente combinée : rejeté — davantage de syscalls, sans avantage surppoll, et nécessiterait aussi un wrapper couche 0.- Ne rien ajouter (rester sur
waitidbloquant) : rejeté — c’est précisément le footgun de hang qu’a exposé l’incident fork. - Re-sceau additif sans RFC (gabarit
Errno) : rejeté — ajouter une famille/syscall évolue le socle (≠ compléter des constantes) → RFC requis (ADR-015).