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# Safetycontract.
1. When (not) to use the raw level
Legitimate use cases:
- 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).
- Advanced optimisations: aggressive batching, SQE layouts that the typed API cannot express.
- 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/CQE32variants are accounted for: accessors respect the configured ring size (a 64-byteRawSubmissionQueueEntryon a non-SQE128ring; the 80-bytecmd[]area only exists underSQE128).- 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 moreRawSubmissionQueueEntryentries viaraw_get_submission_queue_entry, then callssubmit()/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; theunsafecovers 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_entryis safe (read-only);raw_advance_completion_queueis safe (the facade handles the ordering), but interpretingres/flagsis 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 == 0⇒ managed completion: decoded via the slab, delivered as aCompletion(Stage 1);user_data & RAW_USER_DATA_TAG != 0⇒ raw 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:
- Valid opcode, supported by the current kernel (verify via
supports_op/ probe). - Field consistency of the SQE (
fd,addr,len, flags) for the opcode. - Memory validity: every buffer pointed to by
addr/addr2/addr3remains valid and unpinned until completion (slab S1 does not protect it). user_datatag (§5) set on every raw operation.- No double consumption of a completion (
raw_advance_completion_queueconsistent withraw_peek_completion_queue_entrycalls).
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/RawCompletionQueueEntryagainst the v6.12 header (64/128 and 16/32 bytes); compilation failure on any drift. - Integration: submit a
NOPat the raw level (tagged user_data), observe it returned viaraw_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_asserton missing tag; Miri onraw_get_submission_queue_entry/advance (slot bounds); fuzzing ofRawCompletionQueueEntrydecoding (external kernel data, Principle 3). - Coverage: the raw
unsafelevel is tested via NOP and simple opcodes; branches that cannot be provoked are documented inCOVERAGE-EXCEPTIONS.mdwith justification.
9. Key decisions that emerged at Stage 4
- Ordering always managed by the facade — even at the raw level,
submit()/raw_advance_completion_queueperform the store release / load acquire barriers; theunsafecovers only the content of the SQE and memory validity of buffers, not the ring protocol. Naked queue head/tail pointers are never exposed. user_datatag (bit 63) — raw / level-2 coexistence without collision on the same ring; the slab never uses this bit.- No naked mmap pointers exposed — the raw level goes through bounded SQE/CQE slots, not through the naked rings; reduces the error surface.
- Exhaustive and localised
# Safety— consistent with layer 0 conventions; everyunsafefunction documents its preconditions. - 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.