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-028 — Soundness et téardown du module io_uring (S1/S2/S3)

Statut : Accepté. Complète l’ADR-022 sans en modifier les 10 décisions.

Catégorie : Architecture (couche 0).

Contexte

L’ADR-022 fixe l’architecture du module air-sys-syscall::io_uring (niveau d’abstraction 2, coexistence syscalls/io_uring, trois mécanismes de buffers, etc.). La spécification détaillée du module (document maître ../specs/layer-0/io-uring-0-inventaire.md et Temps 1 ../specs/layer-0/io-uring-1-core.md) a fait émerger trois décisions de soundness qui ne figuraient pas dans ADR-022 et qui déterminent toutes les signatures de la façade. Elles doivent être gravées comme décisions d’architecture à part entière.

io_uring est asynchrone et concurrent avec le kernel : un buffer soumis peut être écrit par le kernel après le retour de l’appel de soumission, et jusqu’à la complétion. Deux classes de défauts en découlent si l’API est mal conçue :

  1. Coût d’allocation : retrouver, à la complétion, le buffer et les métadonnées d’une opération via son user_data tente d’imposer une table dynamique (allocation par opération) — en conflit avec la règle « pas d’allocation heap dans le happy path » (CLAUDE.md, conventions couche 0).
  2. Usage après libération : libérer le ring (ou un buffer) alors qu’une opération est encore en vol laisse le kernel écrire dans de la mémoire libérée — le défaut de soundness majeur d’io_uring en Rust.

Par ailleurs, io_uring peut exécuter des opérations qui contournent les filtres seccomp (l’opération n’est pas un syscall), ce qui pose un problème de confinement pour un système à capabilities (ADR-001 AirCom, ADR-010 entitlements).

Décisions

S1 — État des opérations en vol : slab pré-alloué

L’IoUring possède un slab pré-alloué dimensionné (par défaut) à la capacité de la file de complétion (cq_entries). Chaque opération en vol occupe un slot ; le buffer transféré (modèle d’ownership, ADR-022 Décision 3) y est déplacé (move), sans copie ni réallocation. Le SubmissionToken encapsule un index de slot + génération (compteur de génération anti-réutilisation) ; le user_data kernel encode cet index. Le slab plein provoque un refus (EBUSY) avant tout appel kernel, fournissant une back-pressure structurelle. Conséquence : zéro allocation heap par opération dans le happy path.

S2 — Téardown sûr : Drop quiescent + shutdown() explicite

  • shutdown(self) est la voie propre : annulation globale (REGISTER_SYNC_CANCEL), drainage des complétions restantes, puis fermeture du FD et libération de la mémoire ; bornée par un timeout.
  • Drop est un filet de sécurité : s’il reste des opérations en vol, il quiesce (annule + draine, de façon bloquante et best-effort) avant de rendre la mémoire. Le coût d’un Drop potentiellement bloquant est assumé et documenté ; l’usage soucieux de performance appelle shutdown().
  • Invariant : tant qu’une opération est en vol, son buffer vit dans son slot (S1) et n’est pas libérable par l’appelant ; le ring ne peut être détruit sans quiescence. Ces invariants garantissent qu’aucune écriture kernel ne tombe sur de la mémoire libérée.

Choix conforme au Principe d’ingénierie 5 (« sur-sécuriser puis dégraisser après mesure, jamais l’inverse ») : un Drop non sûr en couche 0 est inacceptable.

S3 — Confinement comme primitive de capability

Le triptyque SETUP_R_DISABLED + REGISTER_RESTRICTIONS + REGISTER_ENABLE_RINGS est traité en première classe (Temps 3f). Un ring peut être créé désactivé, restreint à une liste blanche d’opcodes / register-ops / drapeaux SQE (default-deny), puis activé — les restrictions devenant immuables, imposées par le kernel. La couche 0 fournit le mécanisme (RestrictionSet, restrict, enable) ; la politique (traduction des entitlements signés d’ADR-010 en restrictions) vit en couche supérieure. C’est la brique io_uring du modèle de capabilities d’Air, en défense en profondeur avec seccomp/Landlock (family-security), fermant la voie de contournement des filtres syscall.

Articulation avec les autres ADRs

  • ADR-022 : ADR-028 le complète. Les 10 décisions d’ADR-022 restent inchangées ; S1/S2/S3 s’y ajoutent comme décisions de soundness du même module.
  • ADR-021 (conventions couche 0) : S1 met en œuvre « pas d’allocation heap dans le happy path » et l’usage de Option/newtypes (token typé) ; le slab respecte la non-allocation.
  • ADR-001 / ADR-010 : S3 matérialise au niveau kernel le modèle de capabilities AirCom et les entitlements signés.
  • ADR-023 (runtime async) : le runtime de couche 1 s’appuie sur ces invariants (slab, téardown sûr) pour offrir des primitives async sûres.

Conséquences

Bénéfices

  • Sûreté mémoire par construction (pas d’usage après libération, pas de buffer accessible pendant qu’il est détenu par le kernel).
  • Happy path sans allocation, back-pressure naturelle.
  • Confinement kernel-enforced aligné sur le modèle de sécurité d’Air.

Coûts

  • Un Drop potentiellement bloquant (quiescence) : assumé, documenté, contournable par shutdown().
  • Le slab impose une borne d’opérations en vol (cq_entries par défaut, ajustable) : c’est une limite explicite, pas un défaut.

Risques et mitigations

  • Risque : la quiescence en Drop masque un oubli de shutdown() et coûte en latence à la destruction. Mitigation : documentation + lint/conseil d’appeler shutdown() sur chemin chaud.
  • Risque : sous-dimensionnement du slab → EBUSY fréquents. Mitigation : max_inflight() explicite au builder ; métrique in_flight().

Statut futur

ADR complémentaire d’ADR-022, immuable dans ses trois décisions (S1/S2/S3) sauf RFC. Le filtrage fin par programme BPF (IORING_REGISTER_BPF_FILTER), postérieur au kernel 6.12, pourra enrichir S3 ultérieurement, par détection runtime, sans amendement.


Licence du document : MPL 2.0 Statut : Document d’architecture, complément d’ADR-022.