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 :
- Coût d’allocation : retrouver, à la complétion, le buffer et les
métadonnées d’une opération via son
user_datatente 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). - 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.Dropest 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’unDroppotentiellement bloquant est assumé et documenté ; l’usage soucieux de performance appelleshutdown().- 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
Droppotentiellement bloquant (quiescence) : assumé, documenté, contournable parshutdown(). - Le slab impose une borne d’opérations en vol (
cq_entriespar défaut, ajustable) : c’est une limite explicite, pas un défaut.
Risques et mitigations
- Risque : la quiescence en
Dropmasque un oubli deshutdown()et coûte en latence à la destruction. Mitigation : documentation + lint/conseil d’appelershutdown()sur chemin chaud. - Risque : sous-dimensionnement du slab →
EBUSYfréquents. Mitigation :max_inflight()explicite au builder ; métriquein_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.