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:
- 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. - Registered buffer (
READ_FIXED/WRITE_FIXED): references a pre-registered buffer by index → avoids address translation. TypeRegisteredBufferSlicedefined at Stage 3a; only thesubmit_read_fixedform is exposed here. - 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 offsetn;None: “current position of the file” (transmitted as-1to the kernel, requiresIORING_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(offsetSome) orread/write(offsetNone). Seefamily-fs.md. - Buffer:
bufis moved into the slot. Forread, its length bounds the number of bytes read (len = buf.len()— the logical length of theVecis used). Forwrite,buf.len()bytes are written. - Completion:
completion.into_buffer_result() -> (Vec<u8>, usize)returns the buffer and the number of bytes transferred.res < 0⇒Err(Errno). - Preconditions:
bufnon-empty for a useful read (a zero-byte read is legal and completes at 0).fdopened in the correct mode. - Errors:
EBUSY(slab full, before syscall), then at completionEBADF,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. Seefamily-fs.md. - Buffers: we own a vector of owned buffers (
Vec<Vec<u8>>) to remain safe (ownership transfer). Theiovecarray 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 +
EINVALif 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:
RegisteredBufferSlicedesignates 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::DATASYNC⇒IORING_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. Seefamily-fs.mdfor 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/lenasi64in 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). Seefamily-fs.mdforOpenHow(flags, mode, resolve). - Note:
IORING_OP_OPENAT(18) is excluded (UNSUPPORTED.md) in favour ofopenat2, which is a superset (see master document §4). - Completion:
completion.opened_fd() -> Result<OwnedFd, Errno>(CLOEXEC according tohow). The direct descriptor variant (result placed in a slot of the registered FD table viafile_index) belongs to Stage 3a;submit_openat2_directwill 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_closeconsumes theOwnedFd(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). Seefamily-fs.mdforStatxFlags,StatxMask,Statx. - Output buffer: the kernel writes the structure into
out; the ownership ofBox<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_madvise — implemented (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) orunsafeAPI.submit_madvisetakes a shareableMmapRegion(type fromfamily-mem, seefamily-mem-mmap-region.md) and a sub-rangevalidated against the region’s bounds (Principle 4;range.end ≤ region.len()andrange.start ≤ range.endotherwiseErr(EINVAL)before submission). The region must remain mapped until completion: slot S1 retains anMmapRegionLiveness(clone of the innerArc,region.liveness_handle()), so thatmunmapcannot happen while amadviseop is in flight — safe by construction (no use-after-unmap, no leak:munmapat last drop), proven with Miri + loom. The same handle is reused byfutex(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. Seefamily-ipc.md(theipcfamily already documentssplice/tee). - Precondition: at least one of the two FDs is a pipe (as with
splice(2)).off_in/off_outasOption<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 whenfd_inis 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.
8. Directories and links
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
}
}
renameatcarriesRenameFlags(RENAME_NOREPLACE,RENAME_EXCHANGE,RENAME_WHITEOUT) —renameat2semantics.- Typical errors:
ENOENT,EEXIST,ENOTEMPTY,EACCES,EXDEV(linkat cross-device),EINVAL(flags). - Note: each rename/link op carries two
CStringvalues → 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. Seefamily-fs.md. - Buffers:
name(CString) andvalue(Vec<u8>, raw bytes — Principle 3) are moved into the slot. Forget*,valueserves as an output buffer; completioncompletion.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
| # opcode | Opcode | Facade | Completion |
|---|---|---|---|
| 1 | READV | submit_readv | into_vectored_result |
| 2 | WRITEV | submit_writev | into_vectored_result |
| 3 | FSYNC | submit_fsync | completed |
| 4 | READ_FIXED | submit_read_fixed | into_result |
| 5 | WRITE_FIXED | submit_write_fixed | into_result |
| 8 | SYNC_FILE_RANGE | submit_sync_file_range | completed |
| 17 | FALLOCATE | submit_fallocate | completed |
| 19 | CLOSE | submit_close | completed |
| 21 | STATX | submit_statx | into_statx |
| 22 | READ | submit_read | into_buffer_result |
| 23 | WRITE | submit_write | into_buffer_result |
| 24 | FADVISE | submit_fadvise | completed |
| 25 | MADVISE | submit_madvise | completed |
| 28 | OPENAT2 | submit_openat2 | opened_fd |
| 30 | SPLICE | submit_splice | into_result |
| 33 | TEE | submit_tee | into_result |
| 35 | RENAMEAT | submit_renameat | completed |
| 36 | UNLINKAT | submit_unlinkat | completed |
| 37 | MKDIRAT | submit_mkdirat | completed |
| 38 | SYMLINKAT | submit_symlinkat | completed |
| 39 | LINKAT | submit_linkat | completed |
| 41 | FSETXATTR | submit_fsetxattr | completed |
| 42 | SETXATTR | submit_setxattr | completed |
| 43 | FGETXATTR | submit_fgetxattr | into_xattr_result |
| 44 | GETXATTR | submit_getxattr | into_xattr_result |
| 55 | FTRUNCATE | submit_ftruncate | completed |
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 ≤ lenand 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/getxattrresults (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
offset: Option<u64>— direct application of ADR-021 conv. 1 (None = current position, no magic-1).readv/writevwith ownedVec<Vec<u8>>— safety first; the zero-copy vectorized path is at Stage 4, not a shortcut here.submit_close(OwnedFd)consumes the FD — double close is prevented by the type system.- 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. - “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.