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

air-signal — face signal générique

Couche : 1. Statut : implémentée (brique mince, fidèle). Périmètre : roadmap couche 1 (libc + PAL) §5.3 P0.4. Doctrine : ADR-064 (signalfd primaire, helper « bloquer + drainer », zéro unsafe exposé — pas de handler de faute en couche 1 Rust), companion d’ADR-020 (signalfd par défaut). Companion couche 0 : family-signal.md. Conventions : ADR-021 (EINTR remonté, pas de SA_RESTART) ; ADR-029 (nommage sans abréviation). Erreurs : ADR-019. no_std : ADR-048 / ADR-055.

1. Raison d’être

Les signaux sont un mécanisme entièrement piloté par le noyau (ensembles pending, masque bloqué per-thread, dispositions per-process) : aucun état de signal n’est détenu en userland. La couche 0 fournit déjà tous les syscalls (air-sys-syscall::signal, ADR-020) ; air-signal les re-expose fidèlement, typés couche 1 (AirError/AirResult, EINTR remonté), et fournit le pattern signalfd correct clé-en-main — c’est la valeur ajoutée d’ADR-064.

Décision de placement — crate dédiée

La face signal n’est ni process- ni socket-spécifique : la greffer sur air-process reproduirait le couplage que l’audit face-libc reproche à wait_until. Conformément à la doctrine des crates couche 1 « minces et bornées » (air-poll, air-env, air-stdio, air-terminal), elle mérite sa crate dédiée, au-dessus de la couche 0, d’air-base-core et d’air-poll (sœur couche 1, consommée pour l’attente bornée) — sans cycle (air-poll ne dépend pas d’air-signal, il ré-exporte seulement le type couche 0 SignalMask). Ce placement résout l’arbitrage laissé ouvert par la spec air-poll §6 : l’ergonomie de construction/articulation du SignalMask (masquage per-thread, signalfd) vit ici, tandis qu’air-poll continue de ré-exporter le SignalMask couche 0 inchangé sur le fil (raffinement additif).

2. Périmètre — fidèle, cadré par ADR-064

ÉlémentContenu
signalfd (face primaire)AirSignalFd (RAII) : create/create_nonblocking/block_and_drain ; as_fd() -> BorrowedFd (→ air-poll) ; read() -> AirResult<SignalFdInfo> (bloquant, 1 événement) ; next_pending() -> AirResult<Option<SignalFdInfo>> (drainage non-bloquant) ; update_mask ; into_fd
Helper « bloquer + drainer »AirSignalFd::block_and_drain(&SignalMask) — bloque le masque (thread appelant) et crée un signalfd non-bloquant, avec la discipline documentée (bloquer avant de créer les threads, ou dans chaque thread)
Masque per-threadblock/unblock/replace_mask/current_mask (typés SignalMask, rendent l’ancien masque) ; SignalMaskGuard (RAII : bloque, restaure au Drop)
Envoisend_signal_to_process(Pid, Option<Signal>) (kill) ; send_signal_to_thread(Pid, Tid, Option<Signal>) (tgkill) ; raise_signal(Signal) (thread courant) ; queue_signal(Pid, Signal, SignalValue) (rt_sigqueueinfo)
Attentewait_for_signal(&SignalMask) -> AirResult<SignalFdInfo> (bloquant) ; wait_for_signal_until(&SignalMask, AirDuration) -> AirResult<Option<SignalFdInfo>> (borné via air-poll)
Types ré-exportésSignal, SignalMask, SignalFdInfo, SignalFdFlags, SignalValue, Pid, Tid, BorrowedFd, OwnedFd (depuis la couche 0, inchangés)

HORS PÉRIMÈTRE

  • Handler de faute synchrone (sigaction). Décision BDFL (ADR-064) : la couche 1 Rust reste 100 % SAFE, zéro unsafe exposé ; elle n’installe aucun handler. Les fautes synchrones — SIGSEGV/SIGFPE/SIGBUS/SIGILL — prennent l’action par défaut du noyau (terminate + core dump). Un éventuel sigaction (crash reporter, sigaltstack) pour du code C vivra dans la face libc C-ABI au-dessus de cette brique (dual-face, ADR-064 §5) — jamais ici.
  • API sigaction générale. Les signaux normaux passent par signalfd. La face libc C sigaction/signal POSIX fidèle (OpenSSH…) se construira au-dessus, en face-C.
  • Boucle de reprise EINTR. EINTR est remonté (ADR-021 conv.2/ADR-064 §4) ; l’event loop appelante décide de reboucler (helpers de retry au-dessus).
  • Décodage du signalfd_siginfo. Il vit en couche 0 (déjà testé/couvrable là-bas) ; air-signal consomme le SignalFdInfo déjà décodé.
  • Élargissement des signaux RT au-delà de 64. Hérité du SignalMask (bitmask u64) couche 0 ; une représentation plus large relèverait d’un RFC couche 0.

3. Sémantique fidèle & doctrine

  • signalfd = face primaire (ADR-064 §1). Consommation synchrone, thread-safe, pollable : AirSignalFd::as_fd() rejoint un air_poll::poll. On bloque puis on lit — pas de handler réentrant.
  • Helper « bloquer + drainer » (ADR-064 §3) — le point de valeur. Le pattern correct est délicat : un signal process-directed part vers n’importe quel thread qui ne le bloque pas, court-circuitant le fd. block_and_drain bloque le masque dans le thread appelant et crée le fd non-bloquant ; la discipline (bloquer avant de créer les threads → héritage, ou bloquer dans chaque thread) est documentée, pas laissée à l’appelant nu. Le noyau ne permet pas de modifier le masque d’un thread déjà lancé — Air fournit le pattern correct, pas une magie impossible (choix fidèle-kernel signalé).
  • Fautes synchrones : action par défaut du noyau (ADR-064). SIGSEGV/ SIGBUS/SIGFPE/SIGILL sont des traps non différables qu’un signalfd ne peut pas couvrir. Décision BDFL : la couche 1 Rust n’installe aucun handler — ces fautes prennent l’action par défaut du noyau (terminate + core dump). Cela préserve le « zéro unsafe exposé » : Air ne peut pas prouver l’async-signal-safety d’un pointeur de fonction fourni par l’appelant, donc la face sigaction n’existe pas dans cette brique. Un sigaction légitime (crash reporter, sigaltstack) pour du code C relèvera de la face libc C-ABI au-dessus (ADR-064 §5).
  • Option<Signal> (convention 1 ADR-021). None = « signal 0 » = test d’existence, jamais un 0 magique.
  • raise_signal cible le thread courant (fidèle POSIX raise, via tgkill), jamais kill(getpid(), …) qui routerait vers un thread arbitraire.
  • Attente bornée via air-poll : wait_for_signal_until bloque le masque, crée un signalfd non-bloquant, poll avec échéance, draine si prêt. Comble la variante que la couche 0 laissait à EINVAL (faute de ppoll interne). Timeout typé AirDuration (jamais d’entier magique) — voir « Timeout typé » d’air-poll.
  • La couche 1 ne maintient AUCUN état de signal (ADR-064 §6). Elle expose celui du noyau. Le masque est per-thread, les dispositions per-process — le helper et la doc gèrent cette asymétrie pour l’appelant.

4. Sûreté & no_std

  • Zéro unsafe exposé (décision BDFL) : tout l’unsafe de bas niveau (l’asm! des syscalls) reste confiné en couche 0. Le fd est en RAII (OwnedFd). Aucune fonction publique de la crate n’est unsafe, et le code de test lui-même ne contient aucun bloc unsafe (plus de fork).
  • #![no_std], zéro allocation en code lib (le drainage lit dans un tampon fixe côté couche 0 ; les messages d’AirError allouent dans air-base-core). core seul (ADR-048/ADR-055), prouvé par cargo build --lib.
  • Zéro dépendance externe : seules air-sys-syscall, air-sys-types, air-base-core, air-poll. Aucune surface C (check-c-surface VERT).

5. Tests & couverture

Cible 100 % lignes (couche 1, ADR-035). Tests sur signaux réels :

  • Masque : block/unblock round-trip, replace_mask total, current_mask ; SignalMaskGuard restaure au Drop. Chaque test restaure le masque du worker cargo (garde interne) pour ne pas contaminer les suivants.
  • signalfd : create bloquant + raise + read ; create_nonblocking + read sans pendant → WouldBlock ; block_and_drain + drainage (next_pending : None, puis Some, puis None) ; as_fd/into_fd ; update_mask (extension du set capturé, vérifiée par un raise de SIGCHLD).
  • Envoi : send_signal_to_process/_to_thread signal 0 (existence) et ESRCH (bras d’erreur, couvre map_errno) ; raise_signal auto-délivré et lu sur le signalfd ; queue_signal — bras d’erreur ESRCH et chemin de succès auto-dirigé en parent, sans fork (SIGCHLD, SIG_KERNEL_IGNORE → rejeté à la génération, sans effet de bord). Les deux tests s’exécutent dans le thread appelant : 100 % des lignes de queue_signal sont couvertes en parent.
  • Attente : wait_for_signal bloquant (signal pré-raise) ; wait_for_signal_until expiration (Ok(None)) et prêt (Ok(Some)).

Pas de fork, pas d’exception CHILD-EXIT. Tous les tests s’exercent sur des signaux réels dans le thread appelant (auto-délivrance thread-directed via raise/tgkill, ou envoi auto-dirigé d’un signal ignoré par le noyau). La face fault (handler sigaction, unsafe, jadis testée en enfant forké) a été supprimée (décision BDFL, ADR-064) ; le seul motif qui restait à fork — le round-trip de valeur d’un rt_sigqueueinfo process-directed — appartient à la couche 0 (family-signal, décodage signalfd_siginfo testé sur buffer et en enfant mono-thread), pas ici : un signal process-directed n’est pas lisible de façon fiable sur le signalfd du thread appelant en harnais multi-thread (le noyau le route vers le thread de groupe). air-signal transporte la SignalValue sans la re-décoder.

Exception de couverture (marquée in-code, ADR-035).

  1. STRUCTURAL (1). Le bras Err(errno) => autre qu’EAGAIN de AirSignalFd::next_pending : une erreur de read sur un signalfd valide autre qu’EAGAIN (p.ex. EINTR) n’est pas injectable de façon déterministe. EAGAIN (drainage à vide) est couvert ; la conversion Errno → AirError l’est via map_errno (ESRCH) et dans air-base-core.

Suivant la convention couche 1 (air-poll/air-process/air-terminal), cette exception est marquée in-code (// COVERAGE (STRUCTURAL, ADR-035)), non dans COVERAGE-EXCEPTIONS.md (registre orienté couche 0).

Note fuzz

Aucune cible fuzz. Le seul décodage d’octets kernel (le signalfd_siginfoSignalFdInfo) vit en couche 0 ; les entrées d’air-signal sont des types déjà validés (SignalMask, Signal borné, Pid/Tid, AirDuration, SignalValue). Ni parsing, ni désérialisation en couche 1. À l’instar d’air-poll/air-filesystem, l’absence de surface de fuzz est documentée plutôt que matérialisée par une cible vide.

6. Suite (hors périmètre P0.4)

  • Face libc C sigaction/signal POSIX fidèle, au-dessus de cette brique (dual-face ADR-064 §5), avec avertissements async-signal-safety. C’est que vivra tout handler unsafe (jamais dans la couche 1 Rust).
  • air-fault (crash reporter) installera son handler de faute via cette face C-ABI (ou un wrapper unsafe dédié, hors couche 1 Rust) — la couche 1 Rust reste SAFE (ADR-064 §Suite).
  • Élargissement RT > 64 si un besoin concret l’exige (RFC couche 0).