Spec layer 0 — Module io_uring, Stage 2b: network operations
Technical specification — Version 1.0 (target kernel: Linux 6.12 LTS)
Position. Stage 2b specifies the 12 network operations of io_uring in 6.12 (master doc
io-uring-0-inventaire-en.md, axis B). They reuse the core from Stage 1 and the conventions of Stage 2a (references to families, buffer model S1, typed offsets/sentinels). This Stage is strategic for AirCom (ADR-001): this is where zero-copy (send_zc,sendmsg_zc) for the data plane and FD passing (capabilities) viasendmsg/recvmsgcontrol messages live.
1. Cross-cutting conventions for Stage 2b
- Types shared with
family-net(ADR-022 D2):SocketAddr,SocketDomain,SocketType,MessageFlags,ShutdownMode,AcceptFlags,SendMessageRequest,ReceiveMessageRequest,ControlMessages. Seefamily-net.mdfor their definitions and equivalent syscall numbers. MSG_NOSIGNALby default on all sends: noSIGPIPEon a closed socket (the error surfaces asEPIPEin the completion). This is an Air invariant, explicitly overridable.SOCK_CLOEXECby default on created FDs (accept,socket): consistent with thenetfamily and the other layer 0 families.- Owned addresses: a
SocketAddrto be transmitted is serialized into an ownedsockaddrstorage, moved into slot S1 (the kernel reads it asynchronously — it must survive until completion). - Direct descriptor variants (result stored in a registered FD table
rather than an ordinary FD) and multishot: deferred to Stage 3a
and Stage 3d respectively.
bind/listenexist precisely to operate on these direct FDs without an ordinary FD.
2. Connection establishment
2.1 submit_accept
#![allow(unused)]
fn main() {
impl IoUring {
pub fn submit_accept(&mut self, listener: BorrowedFd<'_>, flags: AcceptFlags)
-> Result<SubmissionToken, Errno>;
pub fn submit_accept_with_peer(&mut self, listener: BorrowedFd<'_>, flags: AcceptFlags)
-> Result<SubmissionToken, Errno>;
}
}
- Opcode:
IORING_OP_ACCEPT(13). Equivalent:accept4. - Completion:
completion.accepted_fd() -> Result<OwnedFd, Errno>(CLOEXEC by default). The_with_peervariant captures the peer address in an owned storage in the slot ⇒completion.into_accept_result() -> (OwnedFd, SocketAddr). - Multishot:
submit_accept_multishotat Stage 3d (a single SQE accepts continuously). Direct fd:_directvariant at Stage 3a. - Errors:
EAGAIN(nothing to accept, non-blocking FD),EMFILE/ENFILE,ECONNABORTED,EINVAL.
2.2 submit_connect
#![allow(unused)]
fn main() {
pub fn submit_connect(&mut self, sock: BorrowedFd<'_>, address: SocketAddr)
-> Result<SubmissionToken, Errno>;
}
- Opcode:
IORING_OP_CONNECT(16). Equivalent:connect. - Completion:
completed(). Errors:ECONNREFUSED,ETIMEDOUT,EINPROGRESS(should not surface in io_uring: the op completes at the end),EALREADY,EISCONN.
2.3 submit_socket
#![allow(unused)]
fn main() {
pub fn submit_socket(&mut self, domain: SocketDomain, ty: SocketType, protocol: i32)
-> Result<SubmissionToken, Errno>;
}
- Opcode:
IORING_OP_SOCKET(45). Equivalent:socket. - Completion:
completion.into_socket_fd() -> Result<OwnedFd, Errno>(CLOEXEC by default). Direct fd:_directvariant at Stage 3a. - Motivation: create a socket inside the ring, chainable (Stage 3c)
with
connect/bindwithout a synchronous round-trip.
2.4 submit_bind / submit_listen
#![allow(unused)]
fn main() {
pub fn submit_bind(&mut self, sock: BorrowedFd<'_>, address: SocketAddr)
-> Result<SubmissionToken, Errno>; // OP 56
pub fn submit_listen(&mut self, sock: BorrowedFd<'_>, backlog: u32)
-> Result<SubmissionToken, Errno>; // OP 57
}
- Opcodes:
IORING_OP_BIND(56) /IORING_OP_LISTEN(57) — added in 6.11. Equivalents:bind/listen. - Motivation: allow
bind/listenon a socket created as a direct descriptor (submit_socket_direct), which has no ordinary FD usable by the classicbind(2)/listen(2)calls. - Completion:
completed(). Errors:EADDRINUSE,EACCES,EINVAL.
3. Data transfer (one-shot)
3.1 submit_send / submit_receive
#![allow(unused)]
fn main() {
pub fn submit_send(&mut self, sock: BorrowedFd<'_>, buffer: Vec<u8>, flags: MessageFlags)
-> Result<SubmissionToken, Errno>; // OP 26
pub fn submit_receive(&mut self, sock: BorrowedFd<'_>, buffer: Vec<u8>, flags: MessageFlags)
-> Result<SubmissionToken, Errno>; // OP 27
}
- Equivalents:
send/recv.MSG_NOSIGNALadded by default tosend. - Buffer: ownership transfer (S1). Completion:
into_buffer_result() -> (Vec<u8>, usize). recv:completion.socket_has_pending_data()(CQE_F_SOCK_NONEMPTY) indicates there is more data to read — useful for deciding whether to issue anotherrecv.- Multishot recv and provided buffers (
BUFFER_SELECT): Stage 3d/3b. - Errors:
ECONNRESET,EPIPE(send on closed socket, thanks to NOSIGNAL),EAGAIN,EMSGSIZE.
3.2 submit_send_message / submit_receive_message
#![allow(unused)]
fn main() {
pub fn submit_send_message(&mut self, sock: BorrowedFd<'_>, request: SendMessageRequest)
-> Result<SubmissionToken, Errno>; // OP 9
pub fn submit_receive_message(&mut self, sock: BorrowedFd<'_>, request: ReceiveMessageRequest)
-> Result<SubmissionToken, Errno>; // OP 10
}
- Equivalents:
sendmsg/recvmsg. SendMessageRequest(owned, moved into the slot) aggregates: data buffers (Vec<Vec<u8>>), an optional destination address, and control messages (ControlMessages) — includingSCM_RIGHTSfor FD passing. This is the mechanism by which AirCom transports a capability (an unforgeable FD) between two peers (ADR-001). The façade builds thestruct msghdr+cmsginternally, stored in the slot.ReceiveMessageRequest(owned): receive buffers + a control buffer pre-sized for incoming cmsgs (received FDs). Completion:completion.into_receive_message_result() -> Result<(ReceiveMessageRequest, ReceiveMessageMeta), Errno>whereReceiveMessageMeta(backed byio_uring_recvmsg_out) providesnamelen,controllen,payloadlen,flags(includingMSG_TRUNC/MSG_CTRUNCto check). Received FDs are extracted from the cmsgs asOwnedFd(CLOEXEC).- FD passing safety: received FDs are materialized as
OwnedFd; a truncated cmsg (MSG_CTRUNC) is reported and partial FDs are closed cleanly (no FD leak). - Errors: same as send/recv +
EINVAL(malformed cmsg on the send side).
4. Zero-copy (AirCom data plane)
4.1 Two-completion lifecycle — the key point
send_zc/sendmsg_zc avoid the kernel copy of the payload: the kernel
references the userspace pages directly. This has a major impact on
ownership:
A zero-copy submission produces two completions:
- Result completion — carries the number of bytes sent (
res ≥ 0) and the flagCQE_F_MORE(a notification will follow).- Notification completion — carries
CQE_F_NOTIF; it signals that the kernel no longer references the buffer, which may now be reused or freed.
The buffer must remain alive until the NOTIF completion, not just until the result. The façade manages this via slot S1:
- the slot retains the buffer until
CQE_F_NOTIFis received; - the result completion (
F_MORE) exposes the byte count viainto_result()without returning the buffer; - the NOTIF completion (
is_notif()) returns the buffer viainto_zero_copy_buffer()and releases the slot.
This is the only case in the module where a slot outlives its first completion; it is specified explicitly here and tested (result→NOTIF sequencing, NOTIF without a preceding result on early failure).
4.2 submit_send_zero_copy / submit_send_message_zero_copy
#![allow(unused)]
fn main() {
pub fn submit_send_zero_copy(&mut self, sock: BorrowedFd<'_>, buffer: Vec<u8>, flags: MessageFlags, zero_copy: ZeroCopyFlags)
-> Result<SubmissionToken, Errno>; // OP 47
pub fn submit_send_message_zero_copy(&mut self, sock: BorrowedFd<'_>, request: SendMessageRequest, zero_copy: ZeroCopyFlags)
-> Result<SubmissionToken, Errno>; // OP 48
}
- Equivalents: zero-copy variants of
send/sendmsg. ZeroCopyFlags: exposesREPORT_USAGE(IORING_SEND_ZC_REPORT_USAGE→ the NOTIF completion indicates whether zero-copy actually took place or whether the kernel had to copy; readable viaCompletion::zero_copy_copied()). Bundle (RECVSEND_BUNDLE,FEAT_RECVSEND_BUNDLE) with provided buffers: Stage 3b.- When to use it: large payloads (≳ 10 KB, measure first); for small
messages, ordinary
sendis simpler and often faster (zero-copy has a fixed notification-management overhead). Recommendation aligned with Principle 5 (measure). - Completion: see §4.1. Early failure ⇒ NOTIF without a preceding
F_MORE: the façade then returns the buffer and propagates the error.
5. Teardown
5.1 submit_shutdown
#![allow(unused)]
fn main() {
pub fn submit_shutdown(&mut self, sock: BorrowedFd<'_>, how: ShutdownMode)
-> Result<SubmissionToken, Errno>; // OP 34
}
- Equivalent:
shutdown.ShutdownMode:Read/Write/Both. - Completion:
completed(). Errors:ENOTCONN,EINVAL. - Note: closing the FD itself is done via
submit_close(Stage 2a) or the RAII drop of theOwnedFd.
6. Summary of the 12 operations
| # opcode | Opcode | Façade | Completion |
|---|---|---|---|
| 9 | SENDMSG | submit_send_message | into_result |
| 10 | RECVMSG | submit_receive_message | into_receive_message_result |
| 13 | ACCEPT | submit_accept / _with_peer | accepted_fd / into_accept_result |
| 16 | CONNECT | submit_connect | completed |
| 26 | SEND | submit_send | into_buffer_result |
| 27 | RECV | submit_receive | into_buffer_result |
| 34 | SHUTDOWN | submit_shutdown | completed |
| 45 | SOCKET | submit_socket | into_socket_fd |
| 47 | SEND_ZC | submit_send_zero_copy | into_result + NOTIF into_zero_copy_buffer |
| 48 | SENDMSG_ZC | submit_send_message_zero_copy | into_result + NOTIF into_zero_copy_buffer |
| 56 | BIND | submit_bind | completed |
| 57 | LISTEN | submit_listen | completed |
7. Added / shared types
Shared with family-net: SocketAddr, SocketDomain, SocketType,
MessageFlags, ShutdownMode, AcceptFlags, SendMessageRequest, ReceiveMessageRequest,
ControlMessages. New at Stage 2b: ZeroCopyFlags, ReceiveMessageMeta (backed by
io_uring_recvmsg_out). New Completion methods:
into_socket_fd, into_accept_result, into_receive_message_result, into_zero_copy_buffer,
zero_copy_copied.
8. Test strategy
- Integration (local sockets AF_UNIX + AF_INET loopback): accept/connect, send/recv round-trip, sendmsg/recvmsg with FD passing (send an FD, receive it, verify it points to the same object — core AirCom test), shutdown, socket+bind+listen chained (Stage 3c), accept of a direct fd.
- Zero-copy: verify the sequence result (
F_MORE) → NOTIF (F_NOTIF), that the buffer is not returned before NOTIF, that it is returned at NOTIF, thezero_copy_copiedreport, and the early-failure case (NOTIF only). - FD passing safety:
MSG_CTRUNC(control buffer too small) ⇒ no FD leak; received FDs correctly wrapped as CLOEXECOwnedFd. - Property-based: bytes sent/received ≤ buffer size; robustness of
recvmsg_outparsing (kernel data = external input, Principle 3). - Errors via simulator:
ECONNRESET,EPIPE,EAGAIN,EADDRINUSE. - Soundness: Miri on the zero-copy two-completion lifecycle (the buffer must neither be freed nor read between submission and NOTIF).
- Coverage 100% lines + branches.
9. Key decisions that emerged at Stage 2b
MSG_NOSIGNALandSOCK_CLOEXECby default — Air invariants, consistent across layer 0; explicit override possible.- Explicit zero-copy two-completion lifecycle (extended S1) — a slot
can outlive its first completion until
CQE_F_NOTIF. The only such case in the module; documented and tested. - First-class FD passing via
sendmsg/recvmsg— this is the kernel materialization of AirCom capabilities (ADR-001). Received FDs areOwnedFd, never raw integers. bind/listenexposed even though “obvious” — indispensable for sockets as direct descriptors (without an ordinary FD).- Direct/multishot/bundle variants deferred to Stages 3a/3d/3b — 2b remains focused on one-shot operations with clear ownership.
10. Next steps
Next spec: io-uring-2c-async-en.md (15 operations with no direct
filesystem/network equivalent: nop, timeout, cancel, poll, futex, waitid,
msg_ring, epoll_ctl, files_update, fixed_fd_install). Global English
translation after validation of the French documents.
Document license: MPL 2.0
Status: Technical specification for Stage 2b (network) of the air-sys-syscall::io_uring module, target kernel 6.12 LTS.