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 — Module io_uring, Stage 2a: filesystem operations

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

Position. Stage 2a specifies the 26 filesystem operations exposed via io_uring in 6.12 (see master document io-uring-0-inventaire-en.md, axis B). All of them reuse as-is the core from Stage 1 (io-uring-1-core-en.md): submission, slab S1, completion, teardown S2. This document does not re-specify the ring or the slab — it specifies the operations.


1. Cross-cutting conventions for Stage 2a

1.1 Cross-reference to synchronous families (ADR-022 Decision 2)

Each io_uring operation has a synchronous syscall equivalent already specified in family-fs.md / family-mem.md. To avoid duplication (and the risk of introducing divergences), this document:

  • identifies the op by its opcode IORING_OP_* and its number (source of truth: uapi header v6.12);
  • names the equivalent syscall and cross-references the family for its x86_64/ARM64 number, flags, and fine-grained semantics;
  • reuses the shared types (OpenHow, StatxFlags, StatxMask, Statx, RenameFlags, Mode, DirFd, Advice, SpliceFlags, FallocateMode, FsyncFlags, XattrFlags…) defined alongside the families — a developer familiar with the synchronous API will find the same types here.

The “real underlying syscall” of every io_uring op is io_uring_enter(2) (no. 426); the names below refer to the semantic equivalent.

1.2 Buffer model (recap ADR-022 Decision 3 + S1)

Three mechanisms, ownership transfer by default:

  1. Ownership (default): the buffer (Vec<u8>, Box<Statx>, CString…) is moved into slot S1 on submission, and returned on completion. Safe by construction: it cannot be touched while the kernel holds it.
  2. Registered buffer (READ_FIXED/WRITE_FIXED): references a pre-registered buffer by index → avoids address translation. Type RegisteredBufferSlice defined at Stage 3a; only the submit_read_fixed form is exposed here.
  3. Raw unsafe: raw pointer, validity guaranteed by the caller. Stage 4.

1.3 Offset: Option<u64> rather than a sentinel (ADR-021 conv. 1)

Operations that take an offset (read, write, readv, writev, read_fixed, write_fixed) accept offset: Option<u64>:

  • Some(n): absolute offset n;
  • None: “current position of the file” (transmitted as -1 to the kernel, requires IORING_FEAT_RW_CUR_POS, present in 6.12).

The magic -1 is never exposed: the typed None replaces it.

1.4 Directory descriptors

Operations using *at take dirfd: DirFd (newtype shared with family-fs), where DirFd::CWD materializes AT_FDCWD — yet another kernel sentinel replaced by a typed constructor.

1.5 Paths

Paths are CString values (NUL-terminated bytes, not necessarily UTF-8; consistent with Principle 3 and the use of Path/OsStr at layer 1). The path buffer is moved into the slot (the kernel reads it asynchronously; it must remain valid until completion — S1 guarantees this).


2. Read / write

2.1 submit_read / submit_write

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_read(&mut self, fd: BorrowedFd<'_>, buffer: Vec<u8>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_write(&mut self, fd: BorrowedFd<'_>, buffer: Vec<u8>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
}
}
  • Opcode: IORING_OP_READ (22) / IORING_OP_WRITE (23).
  • Equivalent: pread/pwrite (offset Some) or read/write (offset None). See family-fs.md.
  • Buffer: buf is moved into the slot. For read, its length bounds the number of bytes read (len = buf.len() — the logical length of the Vec is used). For write, buf.len() bytes are written.
  • Completion: completion.into_buffer_result() -> (Vec<u8>, usize) returns the buffer and the number of bytes transferred. res < 0Err(Errno).
  • Preconditions: buf non-empty for a useful read (a zero-byte read is legal and completes at 0). fd opened in the correct mode.
  • Errors: EBUSY (slab full, before syscall), then at completion EBADF, EINVAL, EFAULT, EIO, EAGAIN (non-blocking FD with no data).
  • Performance: zero copy on the facade side (move). On a hot SQPOLL ring, submission ≈ one atomic store.

Example.

#![allow(unused)]
fn main() {
let buf = vec![0u8; 4096];
let tok = ring.submit_read(file.as_fd(), buf, Some(0))?;
ring.submit_and_wait(1)?;
let cmp = ring.wait_completion()?;
let (buf, n) = cmp.into_buffer_result()?;   // buffer recovered, n bytes read
}

2.2 submit_readv / submit_writev

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_readv(&mut self, fd: BorrowedFd<'_>, buffers: Vec<Vec<u8>>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_writev(&mut self, fd: BorrowedFd<'_>, buffers: Vec<Vec<u8>>, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
}
}
  • Opcode: IORING_OP_READV (1) / IORING_OP_WRITEV (2).
  • Equivalent: preadv/pwritev. See family-fs.md.
  • Buffers: we own a vector of owned buffers (Vec<Vec<u8>>) to remain safe (ownership transfer). The iovec array transmitted to the kernel is built internally and stored in the slot; it does not outlive the caller. (The vectorized high-performance path without copying belongs to Stage 4.)
  • Completion: completion.into_vectored_result() -> (Vec<Vec<u8>>, usize).
  • Errors: same as read/write + EINVAL if the number of iovecs > IOV_MAX.

2.3 submit_read_fixed / submit_write_fixed

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_read_fixed(&mut self, fd: BorrowedFd<'_>, buf: RegisteredBufferSlice, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
    pub fn submit_write_fixed(&mut self, fd: BorrowedFd<'_>, buf: RegisteredBufferSlice, offset: Option<u64>)
        -> Result<SubmissionToken, Errno>;
}
}
  • Opcode: IORING_OP_READ_FIXED (4) / IORING_OP_WRITE_FIXED (5).
  • Buffer: RegisteredBufferSlice designates a slice of a pre-registered buffer (IORING_REGISTER_BUFFERS2, Stage 3a). No per-operation ownership transfer: the buffer belongs to the registration.
  • Benefit: the kernel does not need to pin/translate pages on each op (already done at registration time). Hot path (AirCom data plane).
  • Completion: completion.into_result() -> i32 (bytes); the buffer remains owned by the registration.
  • Cross-reference: registration semantics are detailed at Stage 3a.

3. Synchronization, allocation, truncation

3.1 submit_fsync

#![allow(unused)]
fn main() {
pub fn submit_fsync(&mut self, fd: BorrowedFd<'_>, flags: FsyncFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_FSYNC (3). Equivalent: fsync/fdatasync (FsyncFlags::DATASYNCIORING_FSYNC_DATASYNC).
  • Completion: completed() -> Result<(), Errno>.
  • Errors: EBADF, EIO, EINVAL.

3.2 submit_sync_file_range

#![allow(unused)]
fn main() {
pub fn submit_sync_file_range(&mut self, fd: BorrowedFd<'_>, offset: u64, nbytes: u64, flags: SyncFileRangeFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_SYNC_FILE_RANGE (8). Equivalent: sync_file_range. See family-fs.md for flags.
  • Completion: completed().

3.3 submit_fallocate

#![allow(unused)]
fn main() {
pub fn submit_fallocate(&mut self, fd: BorrowedFd<'_>, mode: FallocateMode, offset: i64, length: i64)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_FALLOCATE (17). Equivalent: fallocate.
  • Note: offset/len as i64 in conformance with the ABI; upstream validation (len > 0, no overflow) is performed before submission (Principle 4).
  • Completion: completed(). Errors: EBADF, EFBIG, ENOSPC, EINVAL.

3.4 submit_ftruncate

#![allow(unused)]
fn main() {
pub fn submit_ftruncate(&mut self, fd: BorrowedFd<'_>, length: u64)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_FTRUNCATE (55). Equivalent: ftruncate.
  • Completion: completed(). Errors: EBADF, EINVAL, EFBIG, EPERM.

4. Open / close

4.1 submit_openat2

#![allow(unused)]
fn main() {
pub fn submit_openat2(&mut self, dirfd: DirFd, path: CString, how: OpenHow)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_OPENAT2 (28). Equivalent: openat2 (no. 437 x86_64/ARM64). See family-fs.md for OpenHow (flags, mode, resolve).
  • Note: IORING_OP_OPENAT (18) is excluded (UNSUPPORTED.md) in favour of openat2, which is a superset (see master document §4).
  • Completion: completion.opened_fd() -> Result<OwnedFd, Errno> (CLOEXEC according to how). The direct descriptor variant (result placed in a slot of the registered FD table via file_index) belongs to Stage 3a; submit_openat2_direct will be exposed there.
  • Errors: ENOENT, EACCES, ELOOP, ENFILE, EINVAL

4.2 submit_close

#![allow(unused)]
fn main() {
pub fn submit_close(&mut self, fd: OwnedFd) -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_CLOSE (19). Equivalent: close.
  • Ownership: submit_close consumes the OwnedFd (moved into the slot) — it can no longer be reused, guaranteeing the absence of a double close. The FD is only actually closed upon kernel execution.
  • Completion: completed(). Error: EBADF (unlikely, FD is owned).
  • Fixed variant: closing a slot in the registered FD table → Stage 3a.

5. Metadata

5.1 submit_statx

#![allow(unused)]
fn main() {
pub fn submit_statx(&mut self, dirfd: DirFd, path: CString, flags: StatxFlags, mask: StatxMask, out: Box<Statx>)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_STATX (21). Equivalent: statx (no. 332 x86_64 / 291 ARM64). See family-fs.md for StatxFlags, StatxMask, Statx.
  • Output buffer: the kernel writes the structure into out; the ownership of Box<Statx> is transferred into the slot (it must remain valid until completion — S1).
  • Completion: completion.into_statx() -> Result<Box<Statx>, Errno>.
  • Errors: ENOENT, EACCES, EINVAL.

6. Access advice (advise)

6.1 submit_fadvise

#![allow(unused)]
fn main() {
pub fn submit_fadvise(&mut self, fd: BorrowedFd<'_>, offset: u64, length: u64, advice: Advice)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_FADVISE (24). Equivalent: posix_fadvise.
  • Completion: completed().

6.2 submit_madviseimplemented (coordinated family-mem PR)

#![allow(unused)]
fn main() {
pub fn submit_madvise(&mut self, region: &MmapRegion, range: Range<usize>, advice: MadviseAdvice)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_MADVISE (25). Equivalent: madvise.
  • Safety (settled decision, realized): no raw range (addr/len) or unsafe API. submit_madvise takes a shareable MmapRegion (type from family-mem, see family-mem-mmap-region.md) and a sub-range validated against the region’s bounds (Principle 4; range.end ≤ region.len() and range.start ≤ range.end otherwise Err(EINVAL) before submission). The region must remain mapped until completion: slot S1 retains an MmapRegionLiveness (clone of the inner Arc, region.liveness_handle()), so that munmap cannot happen while a madvise op is in flight — safe by construction (no use-after-unmap, no leak: munmap at last drop), proven with Miri + loom. The same handle is reused by futex (Stage 2c §6.1) and will be by the memory registration in Stage 3a.
  • Completion: completed().

7. Zero-copy between FDs

7.1 submit_splice

#![allow(unused)]
fn main() {
pub fn submit_splice(&mut self, fd_in: BorrowedFd<'_>, offset_in: Option<u64>,
                     fd_out: BorrowedFd<'_>, offset_out: Option<u64>,
                     length: u32, flags: SpliceFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_SPLICE (30). Equivalent: splice. See family-ipc.md (the ipc family already documents splice/tee).
  • Precondition: at least one of the two FDs is a pipe (as with splice(2)). off_in/off_out as Option<u64> (None = current position / non-seekable FD).
  • Completion: completion.into_result() -> i32 (bytes transferred).
  • Note: SPLICE_F_FD_IN_FIXED (bit 31) allows using a registered FD as input → exposed cleanly when fd_in is a fixed FD (Stage 3a).

7.2 submit_tee

#![allow(unused)]
fn main() {
pub fn submit_tee(&mut self, fd_in: BorrowedFd<'_>, fd_out: BorrowedFd<'_>, length: u32, flags: SpliceFlags)
    -> Result<SubmissionToken, Errno>;
}
  • Opcode: IORING_OP_TEE (33). Equivalent: tee. Both FDs are pipes; duplicates data without consuming the source. Completion: into_result.

Uniform operations: paths as CString moved into the slot, dirfd: DirFd, completion completed(). Equivalents and flags: see family-fs.md.

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_mkdirat(&mut self, dirfd: DirFd, path: CString, mode: Mode)
        -> Result<SubmissionToken, Errno>;                              // OP 37
    pub fn submit_unlinkat(&mut self, dirfd: DirFd, path: CString, flags: UnlinkFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 36
    pub fn submit_renameat(&mut self, old_dirfd: DirFd, old_path: CString,
                           new_dirfd: DirFd, new_path: CString, flags: RenameFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 35 (renameat2 semantics)
    pub fn submit_linkat(&mut self, old_dirfd: DirFd, old_path: CString,
                         new_dirfd: DirFd, new_path: CString, flags: LinkFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 39
    pub fn submit_symlinkat(&mut self, target: CString, new_dirfd: DirFd, link_path: CString)
        -> Result<SubmissionToken, Errno>;                              // OP 38
}
}
  • renameat carries RenameFlags (RENAME_NOREPLACE, RENAME_EXCHANGE, RENAME_WHITEOUT) — renameat2 semantics.
  • Typical errors: ENOENT, EEXIST, ENOTEMPTY, EACCES, EXDEV (linkat cross-device), EINVAL (flags).
  • Note: each rename/link op carries two CString values → two buffers moved into the slot; slot S1 stores a composite state, documented accordingly.

9. Extended attributes (xattr)

#![allow(unused)]
fn main() {
impl IoUring {
    pub fn submit_setxattr(&mut self, path: CString, name: CString, value: Vec<u8>, flags: XattrFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 42
    pub fn submit_getxattr(&mut self, path: CString, name: CString, value: Vec<u8>)
        -> Result<SubmissionToken, Errno>;                              // OP 44
    pub fn submit_fsetxattr(&mut self, fd: BorrowedFd<'_>, name: CString, value: Vec<u8>, flags: XattrFlags)
        -> Result<SubmissionToken, Errno>;                              // OP 41
    pub fn submit_fgetxattr(&mut self, fd: BorrowedFd<'_>, name: CString, value: Vec<u8>)
        -> Result<SubmissionToken, Errno>;                              // OP 43
}
}
  • Equivalents: setxattr/getxattr/fsetxattr/fgetxattr. See family-fs.md.
  • Buffers: name (CString) and value (Vec<u8>, raw bytes — Principle 3) are moved into the slot. For get*, value serves as an output buffer; completion completion.into_xattr_result() -> (Vec<u8>, usize) (the returned size allows the buffer to be resized).
  • Errors: ENODATA, ERANGE (buffer too short), ENOTSUP, EACCES.

10. Summary of the 26 operations

# opcodeOpcodeFacadeCompletion
1READVsubmit_readvinto_vectored_result
2WRITEVsubmit_writevinto_vectored_result
3FSYNCsubmit_fsynccompleted
4READ_FIXEDsubmit_read_fixedinto_result
5WRITE_FIXEDsubmit_write_fixedinto_result
8SYNC_FILE_RANGEsubmit_sync_file_rangecompleted
17FALLOCATEsubmit_fallocatecompleted
19CLOSEsubmit_closecompleted
21STATXsubmit_statxinto_statx
22READsubmit_readinto_buffer_result
23WRITEsubmit_writeinto_buffer_result
24FADVISEsubmit_fadvisecompleted
25MADVISEsubmit_madvisecompleted
28OPENAT2submit_openat2opened_fd
30SPLICEsubmit_spliceinto_result
33TEEsubmit_teeinto_result
35RENAMEATsubmit_renameatcompleted
36UNLINKATsubmit_unlinkatcompleted
37MKDIRATsubmit_mkdiratcompleted
38SYMLINKATsubmit_symlinkatcompleted
39LINKATsubmit_linkatcompleted
41FSETXATTRsubmit_fsetxattrcompleted
42SETXATTRsubmit_setxattrcompleted
43FGETXATTRsubmit_fgetxattrinto_xattr_result
44GETXATTRsubmit_getxattrinto_xattr_result
55FTRUNCATEsubmit_ftruncatecompleted

Intentionally absent (pseudo-opcodes from a former overview, non-existent in the io_uring 6.12 ABI): copy_file_range, fchmodat, fchownat. Permission and ownership changes remain synchronous (family-fs).


11. Added / shared types

No new ring types; reuses the core (Stage 1). Business types shared with the families: OpenHow, Statx, StatxFlags, StatxMask, Mode, DirFd, RenameFlags, UnlinkFlags, LinkFlags, FsyncFlags, SyncFileRangeFlags, FallocateMode, Advice, SpliceFlags, XattrFlags. New at Stage 2a: RegisteredBufferSlice (shape; defined at Stage 3a). madvise reuses MmapRegion from family-mem (shared liveness handle, specified jointly). New Completion methods: into_vectored_result, into_statx, into_xattr_result.


12. Test strategy

  • Integration (kernel 6.12, tmpfs/ext4): read/write round-trips with content verification, offset Some/None, multi-buffer readv, openat2 + read + close chained, statx, rename/link/unlink, xattr set/get round-trip, file→pipe splice.
  • Property-based: for read/write, bytes transferred ≤ len and faithful buffer restitution; for paths, robustness to non-UTF-8 bytes.
  • Errors via simulator: ENOENT, EACCES, ERANGE (xattr), ENOSPC (fallocate), EXDEV (linkat).
  • Soundness: Miri on buffer transfer/return paths; verify that an in-flight buffer can be neither freed nor reused (the type makes it impossible).
  • Fuzzing: decoding of statx/getxattr results (data coming from the kernel = external data).
  • Coverage: 100% lines + branches; legitimate exceptions in COVERAGE-EXCEPTIONS.md.

13. Key decisions that emerged at Stage 2a

  1. offset: Option<u64> — direct application of ADR-021 conv. 1 (None = current position, no magic -1).
  2. readv/writev with owned Vec<Vec<u8>> — safety first; the zero-copy vectorized path is at Stage 4, not a shortcut here.
  3. submit_close(OwnedFd) consumes the FD — double close is prevented by the type system.
  4. No copy_file_range/fchmodat/fchownat — these opcodes do not exist in io_uring 6.12; no fake facade is built. The corresponding operations remain synchronous.
  5. “Direct descriptor” and “fixed buffer” variants deferred to Stage 3a — registration is not introduced here in order to keep 2a focused on operations with simple ownership.

14. Next steps

Next spec: io-uring-2b-network-en.md (12 network operations, including the zero-copy variants send_zc/sendmsg_zc and bind/listen). The global English translation is produced once the French documents have been validated.


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