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 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:

  1. Allocation cost: finding, at completion, the buffer and metadata of an operation via its user_data tends 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).
  2. 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.
  • Drop is 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 blocking Drop is acknowledged and documented; performance-sensitive usage calls shutdown().
  • 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 via shutdown().
  • The slab imposes a bound on in-flight operations (cq_entries by default, adjustable): this is an explicit limit, not a defect.

Risks and mitigations

  • Risk: quiescing in Drop masks a forgotten shutdown() call and incurs latency at destruction. Mitigation: documentation + lint/guidance to call shutdown() on the hot path.
  • Risk: undersized slab → frequent EBUSY. Mitigation: explicit max_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.