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-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-v1v1.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

  1. Nouvelle famille couche 0 poll (module dédié), extensible plus tard vers poll/select/epoll si un besoin réel apparaît. Chaque opération = une fonction dédiée typée (ADR-021 §3 — pas de wrapper multiplexé).

  2. Wrapper ppoll (et non poll) :

    #![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
    }
    • EINTR remonté à l’appelant, jamais de retry (ADR-021 §2 ; la reprise vit en couche 1).
    • Conversion Durationtimespec défensive : tv_sec via i64::try_from clampé à i64::MAX si débordement (jamais as ; Principe 2) ; tv_nsec = subsec_nanos (u32 → i64 sûr).
    • Choix ppoll (pas poll) : précision ns (aligne Duration), timeout non muté au retour, sigmask atomique — la variante moderne (ADR-021 « variantes modernes préférées » ; doctrine « kernel = bible »).
  3. Nouveau type PollFd<'fd> : BorrowedFd<'fd> + events demandés + revents remplis par le kernel. Emprunté (+0), aucune prise d’ownership. Les revents sont restitués intégralement à l’appelant (ADR-032 — l’information du kernel n’est pas masquée).

  4. Promotion de PollEvents hors du module io_uring vers le module poll, ré-exporté depuis io_uring : zéro rupture, type inchangé sur le fil (réorganisation interne d’une couche scellée — d’où le re-sceau).

  5. Re-sceau couche-0-v1.5 : jalons antérieurs (v1v1.4) conservés. check-syscalls ajoute le numéro ppoll (x86_64 271 / aarch64 73), certifié sur les 2 arches. Couverture 100 % lignes+branches hors exceptions ADR-035 (EINTR probable STRUCTURAL si non injectable de façon déterministe, cf. précédent waitid). Fuzz non requis (pas de parsing d’entrée externe : entrées = fd + durée).

  6. Gate : cargo xtask barrier sur 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 : ppoll est 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ésentation Result/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).
  • PollEvents dé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 dans air-process (allocation, complexité), conceptuellement couche 2.
  • poll(2) au lieu de ppoll : rejeté — précision ms, timeout muté au retour, pas de sigmask ; ppoll est la variante moderne.
  • timerfd + attente combinée : rejeté — davantage de syscalls, sans avantage sur ppoll, et nécessiterait aussi un wrapper couche 0.
  • Ne rien ajouter (rester sur waitid bloquant) : 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).