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

Layer 0 spec — io_uring module, Stage 4: raw access (level 1)

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

Position. Stage 4 specifies level 1 access to the ring protocol: direct manipulation of raw SQE/CQE slots, for cases that the typed API (levels Stages 1–3f) does not cover. Sub-module air-sys-syscall::io_uring::raw. This is the safety valve of ADR-022 (Decision 1): almost everything goes through level 2; level 1 remains available, governed by an exhaustive # Safety contract.


1. When (not) to use the raw level

Legitimate use cases:

  1. Operations not yet wrapped: a kernel opcode introduced after the facade (reminder: scope is frozen at 6.12; a 6.13+ opcode detected by probe can be driven at the raw level while its level-2 wrapper is pending).
  2. Advanced optimisations: aggressive batching, SQE layouts that the typed API cannot express.
  3. Tooling: debugging, monitoring, ring state inspection.

Avoid otherwise. The raw level bypasses slab S1 (and therefore safe buffer ownership) and exposes fields where a mistake corrupts the ring. The Air rule: stay at level 2 unless there is a measured necessity (Principle 5).


2. Raw types

2.1 RawSubmissionQueueEntry / RawCompletionQueueEntry

#![allow(unused)]
fn main() {
/// Exact mirror of `struct io_uring_sqe` (64 bytes; 128 if SETUP_SQE128).
#[repr(C)]
pub struct RawSubmissionQueueEntry { /* opcode, flags, ioprio, fd, off/addr2, addr, len,
                      op_flags, user_data, buf_index, personality,
                      splice_fd_in/file_index, addr3/cmd… */ }

/// Exact mirror of `struct io_uring_cqe` (16 bytes; 32 if SETUP_CQE32).
#[repr(C)]
pub struct RawCompletionQueueEntry {
    pub user_data: u64,
    pub res: i32,
    pub flags: u32,
    // big_cqe[] (16 bytes) if CQE32
}

/// Raw opcode (also covers opcodes outside `IoUringOpcode`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RawOpcode(pub u8);
}
  • #[repr(C)] and layout bit-for-bit identical to the uapi v6.12 header; verified by size/offset assertions (static) — any drift breaks compilation.
  • SQE128/CQE32 variants are accounted for: accessors respect the configured ring size (a 64-byte RawSubmissionQueueEntry on a non-SQE128 ring; the 80-byte cmd[] area only exists under SQE128).
  • Typed constructors/accessors for filling an SQE without manual offset arithmetic: RawSubmissionQueueEntry::nop(), .set_fd(), .set_addr(), .set_len(), .set_user_data(), .set_flags(), etc.

3. Raw submission

#![allow(unused)]
fn main() {
impl IoUring {
    /// Reserves a free SQE slot and returns it for manual filling.
    /// `None` if the SQ is full.
    ///
    /// # Safety
    /// The caller must write a **valid** SQE: supported opcode, consistent
    /// `fd`/`addr`/`len`, and **every buffer referenced by `addr` must remain
    /// valid until completion** (slab S1 does NOT manage that buffer). The
    /// `user_data` must comply with the coexistence rule (§5).
    pub unsafe fn raw_get_submission_queue_entry(&mut self) -> Option<&mut RawSubmissionQueueEntry>;

    /// Total SQ capacity (effective entries).
    pub fn submission_queue_capacity(&self) -> u32;     // safe
    /// Available SQ slots.
    pub fn submission_queue_available(&self) -> u32;    // safe
    /// Total CQ capacity.
    pub fn completion_queue_capacity(&self) -> u32;     // safe
}
}
  • Publishing = submit() (Stage 1), which remains safe. The caller fills one or more RawSubmissionQueueEntry entries via raw_get_submission_queue_entry, then calls submit()/submit_and_wait(): it is the facade that performs the store release on the queue (Stage 1 §3.2). The ring protocol ordering is therefore managed by the facade; the unsafe covers only the content of the SQE and the memory validity of buffers, not the ring protocol.

4. Raw completion

#![allow(unused)]
fn main() {
impl IoUring {
    /// Inspects the next raw completion without consuming it. `None` if the CQ
    /// is empty. (Safe: read-only via internal load acquire.)
    pub fn raw_peek_completion_queue_entry(&self) -> Option<&RawCompletionQueueEntry>;

    /// Consumes `n` completions from the CQ (advances the head, store release).
    pub fn raw_advance_completion_queue(&mut self, n: u32);
}
}
  • Raw read/advance for monitoring tools and operations submitted at the raw level. raw_peek_completion_queue_entry is safe (read-only); raw_advance_completion_queue is safe (the facade handles the ordering), but interpreting res/flags is the caller’s responsibility (based on the opcode it submitted).

5. Raw / level-2 coexistence: the user_data coexistence rule (essential)

Slab S1 (Stage 1 §4.2) encodes the user_data of level-2 operations as (generation << 32) | slot, with slot < capacity. A raw operation sets its user_data freely — creating a risk of collision with the slab encoding.

Frozen rule: a raw operation must set the most-significant bit (RAW_USER_DATA_TAG = 1 << 63) in its user_data. The facade’s completion loop then routes:

  • user_data & RAW_USER_DATA_TAG == 0managed completion: decoded via the slab, delivered as a Completion (Stage 1);
  • user_data & RAW_USER_DATA_TAG != 0raw completion: delivered as-is (RawCompletionQueueEntry) to the caller, without touching the slab.
#![allow(unused)]
fn main() {
pub const RAW_USER_DATA_TAG: u64 = 1 << 63;
}

Consequence: the slab never uses bit 63 (slot + generation fit in 63 bits — more than sufficient). Raw and level 2 coexist on the same ring without collision. The facade rejects (debug_assert + error) a raw_get_submission_queue_entry whose user_data does not carry the tag, in test builds.


6. # Safety contract (summary)

The caller of the raw level guarantees:

  1. Valid opcode, supported by the current kernel (verify via supports_op / probe).
  2. Field consistency of the SQE (fd, addr, len, flags) for the opcode.
  3. Memory validity: every buffer pointed to by addr/addr2/addr3 remains valid and unpinned until completion (slab S1 does not protect it).
  4. user_data tag (§5) set on every raw operation.
  5. No double consumption of a completion (raw_advance_completion_queue consistent with raw_peek_completion_queue_entry calls).

The facade guarantees in return: ring ordering (publish/consume store release / load acquire) and non-corruption of internal structures (the raw level does not expose naked mmap pointers, only SQE/CQE slots accessed through bounded offsets).


7. Added / shared types

New: RawSubmissionQueueEntry, RawCompletionQueueEntry, RawOpcode, constant RAW_USER_DATA_TAG. Reuses the ring and its ordering (Stage 1). The raw register argument structures (io_uring_rsrc_register, io_uring_buf_reg, etc.) are exposed as #[repr(C)] here for tooling purposes, but their normal usage goes through the typed types of Stages 3a/3b.


8. Test strategy

  • Layout: static size/offset assertions for RawSubmissionQueueEntry/RawCompletionQueueEntry against the v6.12 header (64/128 and 16/32 bytes); compilation failure on any drift.
  • Integration: submit a NOP at the raw level (tagged user_data), observe it returned via raw_peek_completion_queue_entry/raw_advance_completion_queue; implement a simple operation at the raw level and compare against the equivalent level-2 wrapper.
  • Coexistence: mix level-2 (slab) operations and raw (tagged) operations on the same ring; verify correct routing of completions, no collision.
  • Safety: debug_assert on missing tag; Miri on raw_get_submission_queue_entry/advance (slot bounds); fuzzing of RawCompletionQueueEntry decoding (external kernel data, Principle 3).
  • Coverage: the raw unsafe level is tested via NOP and simple opcodes; branches that cannot be provoked are documented in COVERAGE-EXCEPTIONS.md with justification.

9. Key decisions that emerged at Stage 4

  1. Ordering always managed by the facade — even at the raw level, submit()/raw_advance_completion_queue perform the store release / load acquire barriers; the unsafe covers only the content of the SQE and memory validity of buffers, not the ring protocol. Naked queue head/tail pointers are never exposed.
  2. user_data tag (bit 63) — raw / level-2 coexistence without collision on the same ring; the slab never uses this bit.
  3. No naked mmap pointers exposed — the raw level goes through bounded SQE/CQE slots, not through the naked rings; reduces the error surface.
  4. Exhaustive and localised # Safety — consistent with layer 0 conventions; every unsafe function documents its preconditions.
  5. Safety valve, not the main path — the raw level exists for the 5 % of cases that level 2 does not cover; the documentation discourages its use by default.

10. End of the io_uring module

With this Stage 4, the inventory of the master document is fully covered:

  • Stage 1 (core); 2a (fs); 2b (network); 2c (async); 2d (uring_cmd); 3a (registration); 3b (provided buffers); 3c (linked); 3d (multishot); 3e (multi-thread); 3f (confinement); 4 (raw).

All io_uring features for the 6.12 target are specified as a Rust facade, excluding obsolete interfaces (moved to UNSUPPORTED.md).

Next steps: global English translation of the module documents (after validation of the French versions), then hand off the body implementations (todo!()) from the validated rustdoc skeletons.


Document license: MPL 2.0 Status: Technical specification for Stage 4 (raw access) of the air-sys-syscall::io_uring module, kernel target 6.12 LTS. Closes the module specification.