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

Spec layer 0 — Module io_uring, Stage 3f: confinement (sandbox)

Technical specification — Version 1.0 (target kernel: Linux 6.12 LTS)

Position. Stage 3f makes soundness decision S3 from the master document operational: confine a ring so that it can only submit operations that have been explicitly authorized. Sub-module air-sys-syscall::io_uring::sandbox. Builds on the IORING_SETUP_R_DISABLED flag (Stage 1), and the register opcodes REGISTER_RESTRICTIONS (11) and ENABLE_RINGS (12). This is the io_uring brick of Air’s capabilities model (ADR-001 AirCom, ADR-010 signed entitlements), in defense in depth with family-security (seccomp/Landlock).


1. Why confine a ring?

io_uring executes operations (read, openat2, connect…) that, when submitted through the ring, do not go through the classic syscall interface. A seccomp filter that blocks the openat2 syscall does not prevent an IORING_OP_OPENAT2 submitted via the ring. Historically, this has been a bypass path for sandboxes. The proper response: restrict the ring itself.

A confined ring can only submit the opcodes, register-ops, and SQE flags explicitly placed on the allowlist, and the kernel enforces this. Even a compromised service cannot expand what its ring permits.

This is the materialization, at the io_uring level, of the entitlements declared in the signed manifest of an .airservice/.airapp (ADR-010), and the kernel-side counterpart of AirCom capabilities (ADR-001).


2. The mechanism in three stages

  1. Create disabled: the ring is created with IORING_SETUP_R_DISABLED — it exists but cannot submit anything.
  2. Restrict: REGISTER_RESTRICTIONS applies a list of restrictions (allowlist). Possible only while the ring is disabled.
  3. Enable: REGISTER_ENABLE_RINGS activates the ring. After activation, restrictions are immutable — they can no longer be relaxed.

The order is kernel-enforced and reflected by the API: a ring that is already active cannot be restricted, and submission is not possible before activation.


3. API

3.1 RestrictionSet

#![allow(unused)]
fn main() {
/// Allowlist of what a confined ring will be permitted to do. Default-deny: anything
/// not explicitly authorized is refused by the kernel.
#[derive(Debug, Clone, Default)]
pub struct RestrictionSet { /* ... */ }

impl RestrictionSet {
    pub fn new() -> Self;
    /// Authorizes a submission opcode (RESTRICTION_SQE_OP).
    pub fn allow_op(self, op: IoUringOpcode) -> Self;
    /// Authorizes a register opcode (RESTRICTION_REGISTER_OP).
    pub fn allow_register(self, op: RegisterOp) -> Self;
    /// Authorized SQE flags (RESTRICTION_SQE_FLAGS_ALLOWED).
    pub fn allow_sqe_flags(self, flags: SqeFlagSet) -> Self;
    /// SQE flags required on EVERY submission (RESTRICTION_SQE_FLAGS_REQUIRED).
    pub fn require_sqe_flags(self, flags: SqeFlagSet) -> Self;

    /// Conversion from an entitlement set (upper layer).
    /// Provided as an integration point; policy lives in layer 5.
    pub fn from_entitlements(/* … */) -> Self;
}
}

3.2 Integration with the builder (Stage 1)

#![allow(unused)]
fn main() {
// IoUringBuilder::restrict(&[Restriction]) → R_DISABLED + REGISTER_RESTRICTIONS.
let mut ring = IoUringBuilder::new(entries)
    .restrict(restrictions.as_slice())   // ring created disabled
    .build()?;
ring.enable()?;                           // REGISTER_ENABLE_RINGS; immutable thereafter
// From this point, any submission outside the allowlist fails (-EACCES/-EINVAL).
}
  • build() with a non-empty restrict leaves the ring disabled; enable() (Stage 1 §5.2) activates it. Any attempt to call submit_* before enable() ⇒ error. Any attempt to re-restrict after enable() ⇒ error.
  • Default-deny semantics: if RestrictionSet contains at least one allow_op, only the listed opcodes are permitted (the rest are refused by the kernel). Same for register-ops.

4. The four restriction types

Type (io_uring_register_restriction_op)RestrictionSetEffect
RESTRICTION_SQE_OP (1)allow_opallowlist of submittable opcodes
RESTRICTION_REGISTER_OP (0)allow_registerallowlist of register-ops
RESTRICTION_SQE_FLAGS_ALLOWED (2)allow_sqe_flagsauthorized SQE flags
RESTRICTION_SQE_FLAGS_REQUIRED (3)require_sqe_flagsflags required on each SQE

Example — a “network only” AirCom service:

#![allow(unused)]
fn main() {
let r = RestrictionSet::new()
    .allow_op(IoUringOpcode::Socket)
    .allow_op(IoUringOpcode::Connect)
    .allow_op(IoUringOpcode::Send)
    .allow_op(IoUringOpcode::Receive)
    .allow_op(IoUringOpcode::SendZeroCopy)
    .allow_op(IoUringOpcode::Close);
// The ring can never emit openat2, unlinkat, etc. — kernel-guaranteed.
}

5. Relationship with Air’s security model

  • ADR-010 (signed entitlements): the manifest of an .airservice declares its entitlements; a layer 5 component (air-launchd/air-trust) translates those entitlements into a RestrictionSet and provides the service with an already-confined ring. Layer 0 provides the mechanism (RestrictionSet, restrict, enable); policy lives in the upper layer — a clean separation.
  • ADR-001 (AirCom): a service receives only the capabilities (FDs) from its manifest, and its ring can only submit the corresponding operations — a double lock (unforgeable FDs + restricted ring).
  • family-security (seccomp/Landlock): io_uring restrictions complement seccomp/Landlock; they do not replace them. seccomp filters syscalls, Landlock filters FS access, io_uring restrictions filter ring operations — defense in depth. This is precisely what closes the bypass path described in §1.
  • Personality (Stage 3a): combined with restrictions, allows ops to be executed under a controlled identity — least privilege.

Out-of-scope note. Even finer-grained filtering via a BPF program (IORING_REGISTER_BPF_FILTER) is post-6.12; it may enrich this confinement later, through runtime detection, without amendment.


6. Summary

ElementFacade
SETUP_R_DISABLEDIoUringBuilder::restrict (creates disabled)
REGISTER_RESTRICTIONS (11)IoUringBuilder::restrict (applies the list)
REGISTER_ENABLE_RINGS (12)IoUring::enable
4 restriction_opRestrictionSet::{allow_op, allow_register, allow_sqe_flags, require_sqe_flags}

7. Added / shared types

New: RestrictionSet, RegisterOp (enumeration of authorizable register-ops), SqeFlagSet. Reused: Restriction and IoUringOpcode (declared in Stage 1), IoUringBuilder::restrict, IoUring::enable. Integration point from_entitlements (entitlement type defined in the upper layer).


8. Test strategy

  • Integration: create an R_DISABLED ring, apply an allowlist, enable(), verify that an authorized opcode passes and that a non-authorized opcode fails (-EACCES/-EINVAL); same for register-ops; require_sqe_flags enforced (a submission without the required flag fails); submission before enable rejected; re-restriction after enable rejected (immutability).
  • Security: test demonstrating that an openat2 submitted via a “network only” ring is refused even when the process has the right to open the file (proof of defense in depth vs seccomp).
  • Property-based: for any subset of authorized opcodes, exactly those opcodes pass.
  • Coverage: 100% lines + branches.

9. Key decisions that emerged at Stage 3f

  1. Confinement = layer 0 mechanism, layer 5 policyRestrictionSet and the restrict → enable flow are neutral; the translation of signed entitlements (ADR-010) lives in the upper layer.
  2. Default-deny — as soon as an opcode allowlist is set, everything else is refused by the kernel; no half-measures.
  3. Immutability after enable — guaranteed by the kernel and reflected by the API; a confined ring cannot be relaxed.
  4. Explicit defense in depth — io_uring restrictions complement seccomp/Landlock and close the syscall filter bypass path.
  5. BPF filtering noted but out of scope (post-6.12).

10. Next steps

Next and final spec in the module: io-uring-4-raw-en.md (level-1 access to the ring protocol: RawSubmissionQueueEntry/RawCompletionQueueEntry, direct SQE/CQE manipulation, not-yet-wrapped operations, extensive # Safety). Global English translation after validation of the French documents.


Document license: MPL 2.0 Status: Technical specification for Stage 3f (confinement) of the air-sys-syscall::io_uring module, target kernel 6.12 LTS.