ADR-028 — Soundness and teardown of the io_uring module (S1/S2/S3)
Status: Accepted. Completes ADR-022 without modifying its 10 decisions.
Category: Architecture (layer 0).
Context
ADR-022 establishes the architecture of the air-sys-syscall::io_uring module (abstraction level 2, syscall/io_uring coexistence, three buffer mechanisms, etc.). The detailed specification of the module (master document ../specs/layer-0/io-uring-0-inventaire-en.md and Time 1 ../specs/layer-0/io-uring-1-core-en.md) surfaced three soundness decisions that were not present in ADR-022 and that determine all facade signatures. They must be enshrined as architecture decisions in their own right.
io_uring is asynchronous and concurrent with the kernel: a submitted buffer may be written by the kernel after the submission call returns, and until completion. Two classes of defects follow if the API is poorly designed:
- Allocation cost: finding, at completion, the buffer and metadata of an operation via its
user_datatends to impose a dynamic table (one allocation per operation) — in conflict with the rule “no heap allocation in the happy path” (CLAUDE.md, layer 0 conventions). - Use-after-free: freeing the ring (or a buffer) while an operation is still in flight leaves the kernel writing into freed memory — the major soundness defect of io_uring in Rust.
Furthermore, io_uring can execute operations that bypass seccomp filters (the operation is not a syscall), which raises a confinement issue for a capability-based system (ADR-001-fr.md AirCom, ADR-010-fr.md entitlements).
Decisions
S1 — In-flight operation state: pre-allocated slab
The IoUring owns a pre-allocated slab sized (by default) to the capacity of the completion queue (cq_entries). Each in-flight operation occupies a slot; the transferred buffer (ownership transfer model, ADR-022 Decision 3) is moved into it, without copy or reallocation. The SubmissionToken encapsulates a slot index + generation (compteur de génération anti-réutilisation); the kernel user_data encodes this index. A full slab causes a rejection (EBUSY) before any kernel call, providing structural back-pressure. Consequence: zero heap allocation per operation in the happy path.
S2 — Safe teardown: quiescing Drop + explicit shutdown()
shutdown(self)is the clean path: global cancellation (REGISTER_SYNC_CANCEL), draining remaining completions, then closing the FD and freeing memory; bounded by a timeout.Dropis a safety net: if in-flight operations remain, it quiesces (cancels + drains, in a blocking and best-effort manner) before releasing memory. The cost of a potentially blockingDropis acknowledged and documented; performance-sensitive usage callsshutdown().- Invariant: as long as an operation is in flight, its buffer lives in its slot (S1) and cannot be freed by the caller; the ring cannot be destroyed without quiescing. These invariants guarantee that no kernel write lands on freed memory.
Choice consistent with Engineering Principle 5 (“over-secure then trim after measurement, never the reverse”): an unsound Drop in layer 0 is unacceptable.
S3 — Confinement as a capability primitive
The triptych SETUP_R_DISABLED + REGISTER_RESTRICTIONS + REGISTER_ENABLE_RINGS is treated as a first-class citizen (Time 3f). A ring can be created disabled, restricted to an allowlist of opcodes / register-ops / SQE flags (default-deny), then enabled — the restrictions becoming immutable, enforced by the kernel. Layer 0 provides the mechanism (RestrictionSet, restrict, enable); the policy (translating the signed entitlements from ADR-010-fr.md into restrictions) lives in an upper layer. This is the io_uring building block of Air’s capability model, in defense in depth with seccomp/Landlock (family-security), closing the syscall-filter bypass path.
Relationship to other ADRs
- ADR-022: ADR-028 completes it. The 10 decisions of ADR-022 remain unchanged; S1/S2/S3 are added as soundness decisions for the same module.
- ADR-021-fr.md (layer 0 conventions): S1 implements “no heap allocation in the happy path” and the use of
Option/newtypes (typed token); the slab respects the no-allocation rule. - ADR-001-fr.md / ADR-010-fr.md: S3 materializes at the kernel level the AirCom capability model and signed entitlements.
- ADR-023-fr.md (async runtime): the layer 1 runtime relies on these invariants (slab, safe teardown) to provide safe async primitives.
Consequences
Benefits
- Memory safety by construction (no use-after-free, no buffer accessible while held by the kernel).
- Happy path without allocation, natural back-pressure.
- Kernel-enforced confinement aligned with Air’s security model.
Costs
- A potentially blocking
Drop(quiescing): acknowledged, documented, avoidable viashutdown(). - The slab imposes a bound on in-flight operations (
cq_entriesby default, adjustable): this is an explicit limit, not a defect.
Risks and mitigations
- Risk: quiescing in
Dropmasks a forgottenshutdown()call and incurs latency at destruction. Mitigation: documentation + lint/guidance to callshutdown()on the hot path. - Risk: undersized slab → frequent
EBUSY. Mitigation: explicitmax_inflight()on the builder;in_flight()metric.
Future status
ADR complementary to ADR-022, immutable in its three decisions (S1/S2/S3) unless subject to an RFC. Fine-grained filtering via BPF program (IORING_REGISTER_BPF_FILTER), available from kernel 6.12 onwards, may enrich S3 at a later stage via runtime detection, without amendment.
Document license: MPL 2.0 Status: Architecture document, complement to ADR-022.