io_uring Subsystem Details
Zero-Copy Lifetime Management
Attaching buf_node to req instead of notif in zero-copy operations
causes use-after-free, because req completes before the network/block
layer finishes with the buffers. The notif (struct io_kiocb) is
completed via io_tx_ubuf_complete() only when transmission finishes, so
buffer references (struct io_rsrc_node) must be attached to it.
Zero-copy operations: IORING_OP_SEND_ZC and IORING_OP_SENDMSG_ZC.
Internally these set MSG_ZEROCOPY via io_send_zc_prep().
Buffer import attachment: io_import_reg_buf() and
io_import_reg_vec() call io_find_buf_node(), which attaches buf_node
to the passed io_kiocb — first arg to io_import_reg_buf(), third arg
to io_import_reg_vec().
io_import_reg_buf(sr->notif, ...) // CORRECT
io_import_reg_vec(ITER_SOURCE, &msg_iter, sr->notif, ...) // CORRECT
io_import_reg_buf(req, ...) // WRONG
io_import_reg_vec(ITER_SOURCE, &msg_iter, req, ...) // WRONG
When reviewing vectored zero-copy operations, compare with the non-vectored
equivalent for consistency. IORING_OP_SEND_ZC uses io_send_zc_import()
which correctly passes sr->notif.
Notification flush ordering: io_notif_flush() drops the notif
reference, so calling it twice causes use-after-free. After flushing,
immediately NULL the pointer (zc->notif = NULL). Both the fast path
(inline flush) and cleanup path (io_send_zc_cleanup() via
io_clean_op()) can flush. See io_send_zc() in io_uring/net.c.
REPORT as bugs: Buffer import passing req instead of notif in
zero-copy ops. io_notif_flush() not followed by NULLing the pointer.
REQ_F_NEED_CLEANUP and Cleanup Flag Safety
Missing REQ_F_NEED_CLEANUP before an early-return error in a prep
function causes resource leaks. io_clean_op() checks this flag to call
io_cold_defs[req->opcode].cleanup; without it, resources are never freed.
Invariant: Set REQ_F_NEED_CLEANUP immediately after any allocation
whose release depends on the opcode's cleanup handler. No early return may
exist between allocation and flag-setting.
open->filename = getname(fname);
if (IS_ERR(open->filename))
return PTR_ERR(open->filename);
req->flags |= REQ_F_NEED_CLEANUP; // CORRECT: before any validation
// ... validation that may return -EINVAL is safe here ...
Clearing rules: Only clear REQ_F_NEED_CLEANUP / REQ_F_ASYNC_DATA
after the resource is successfully recycled or freed. io_netmsg_recycle()
clears flags only inside the io_alloc_cache_put() success branch. See
io_uring/net.c.
REPORT as bugs: REQ_F_NEED_CLEANUP set after a code path that can
return early. Unconditional clearing of REQ_F_NEED_CLEANUP or
REQ_F_ASYNC_DATA before confirming the resource was recycled/freed.
Async Data Lifecycle
req->async_data and REQ_F_ASYNC_DATA must always be in sync.
io_clean_op() checks only the flag; mismatches cause use-after-free or
double-free.
Rules:
- Use
io_uring_alloc_async_data(cache, req)to allocate (sets flag on success) - Use
io_req_async_data_free(req)to free (clears pointer + flag) - Use
io_req_async_data_clear(req, extra_flags)for cache-returned data - Never manually assign
req->async_datawithout setting the flag, orkfree()it without using the helpers - Allocate in
prep, notissue— the data must exist before retry or cancellation. Seeio_waitid_prep()inio_uring/waitid.c.
All helpers are in io_uring/io_uring.h.
REPORT as bugs: Manual req->async_data assignment without
REQ_F_ASYNC_DATA. Direct kfree(req->async_data) bypassing helpers.
Async data allocated in issue rather than prep for cancellable ops.
SQE Data Stability for uring_cmd
IORING_OP_URING_CMD delegates SQE interpretation to f_op->uring_cmd(),
which may access SQE fields long after prep. If the SQE slot is reused
before copying, the handler reads stale data.
Rules:
- Standard opcodes read all SQE fields during
prep.uring_cmdis different:ioucmd->sqepoints to the ring slot and is used at issue time and from async completions. - On async punt,
io_uring_cmd_sqe_copy()copies the SQE intoac->sqesand updatesioucmd->sqe.REQ_F_SQE_COPIEDprevents double copies. Both copy and pointer update must happen together. - SQE fields needed at issue must be cached during prep with
READ_ONCE()(e.g.,ioucmd->cmd_op = READ_ONCE(sqe->cmd_op)). Issue code must use cached values, notcmd->sqe->. Seeio_uring_cmd_prep()inio_uring/uring_cmd.candio_uring_cmd_sock()inio_uring/cmd_net.c.
REPORT as bugs: ioucmd->sqe accessed after async punt without copy.
Issue code reading SQE fields through cmd->sqe-> instead of cached values.
CQE Sizing Modes: CQE32 vs CQE_MIXED
Wrong cqe32 boolean to io_get_cqe() / io_get_cqe_overflow() causes
CQ tail mis-advancement and data corruption. The parameter means "mixed-mode
per-CQE 32B entry needing extra advancement," NOT "this CQE is 32 bytes."
The two modes are mutually exclusive (enforced in io_uring_sanitise_params()):
| Ring mode | cqe32 param |
Why |
|---|---|---|
IORING_SETUP_CQE32 |
false |
Ring doubles all slots; handled internally |
IORING_SETUP_CQE_MIXED + IORING_CQE_F_32 |
true |
Per-CQE extra slot needed |
IORING_SETUP_CQE_MIXED w/o IORING_CQE_F_32 |
false |
Standard 16B entry |
| Default 16B ring | false |
Standard 16B entry |
Derive cqe32 from the per-CQE IORING_CQE_F_32 flag, NOT ring-level
IORING_SETUP_CQE32. See io_fill_cqe_req() in io_uring/io_uring.h.
REPORT as bugs: cqe32=true based on IORING_SETUP_CQE32 rather than
IORING_CQE_F_32.
Multishot and CQE Posting
Wrong context for CQE posting or incorrect multishot return values causes warnings, crashes, or hung requests.
CQE posting: io_req_post_cqe() requires task_work context with
uring_lock held, never io-wq. Any request calling it must have
REQ_F_MULTISHOT or REQ_F_APOLL_MULTISHOT set so
io_wq_submit_work() handles it. See io_uring/io_uring.c.
Return values: REQ_F_APOLL_MULTISHOT means the request supports
multishot; issue_flags & IO_URING_F_MULTISHOT means it's executing in
multishot context. Check the latter before returning multishot-specific
status codes (e.g., IOU_STOP_MULTISHOT).
Async punt: Multishot handlers must not return -EAGAIN for io-wq punt.
See __io_read() in io_uring/rw.c.
Buffer recycling: Call io_kbuf_recycle() before returning to poll
waiting. See io_recv() in io_uring/net.c.
REPORT as bugs: io_req_post_cqe() without REQ_F_MULTISHOT /
REQ_F_APOLL_MULTISHOT. Multishot status without IO_URING_F_MULTISHOT
check. Multishot handler returning -EAGAIN for async punt.
Provided Buffer Ring Semantics
Provided buffer rings (struct io_uring_buf in buf_ring) are in
userspace-shared memory. Incorrect access or commit ordering causes data
corruption, infinite loops, or dangling pointers.
Shared memory: All buf->len, buf->addr, buf->bid reads must use
READ_ONCE() into a local; writes must use WRITE_ONCE(). Legacy
struct io_buffer (kernel-only lists) needs no annotations. See
io_ring_buffer_select() and io_kbuf_inc_commit() in io_uring/kbuf.c.
Auto-commit vs explicit commit (io_should_commit() in
io_uring/kbuf.c):
IO_URING_F_UNLOCKED: always auto-commit- Non-pollable, non-uring_cmd: auto-commit
- Pollable or
IORING_OP_URING_CMD: skip (operation commits explicitly)
New opcodes with explicit commit must be exempted in io_should_commit().
Address capture: Capture buf->addr before io_kbuf_commit(), which
may modify buffer metadata.
Retry: On partial completion, commit via io_kbuf_commit() and set
REQ_F_BL_NO_RECYCLE before returning. See io_net_kbuf_recyle() in
io_uring/net.c.
Incremental consumption: io_kbuf_inc_commit() must terminate on
zero-length buffers.
REPORT as bugs: buf_ring field access without READ_ONCE() /
WRITE_ONCE(). Explicit-commit opcode not exempted in io_should_commit().
REQ_F_BL_NO_RECYCLE set without committing.
Registered Buffer Management
Wrong offset calculations for pre-registered buffer bvecs cause silent data corruption or out-of-bounds access.
Rules:
- Never assume
imu->ubufis page/folio-aligned. Useimu->bvec[0].bv_offsetfor sub-folio offset, not address masking. Seeio_vec_fill_bvec()inio_uring/rsrc.c:offset = buf_addr - imu->ubuf; offset += imu->bvec[0].bv_offset; // CORRECT - During registration with folio coalescing,
data.first_folio_page_idx << PAGE_SHIFTaccounts for the first page's position within its folio. Seeio_sqe_buffer_register()inio_uring/rsrc.c. - Use
unpin_user_folio(), neverunpin_user_page()— registered buffers are pinned per-folio after coalescing, so unpin must match. Seeio_release_ubuf().
REPORT as bugs: Offset derived by masking virtual address instead of
imu->bvec[0].bv_offset. unpin_user_page() in io_uring buffer code.
msg_ring Cross-Ring Request Lifetime
msg_ring-allocated io_kiocb freed before the RCU grace period causes
use-after-free. io_msg_data_remote() allocates via kmem_cache_alloc()
and posts to a remote ring; io_msg_tw_complete() frees via kfree_rcu().
See io_uring/msg_ring.c.
Rules:
- Free via
kfree_rcu(req, rcu_head), neverkmem_cache_free()/kfree() - Never place into
io_alloc_cache(bypasses RCU guarantees) - Set
req->tctx = NULLon remote requests — the submitter may exit. Seeio_msg_remote_post().
REPORT as bugs: msg_ring request in io_alloc_cache, freed without
kfree_rcu(), or with non-NULL req->tctx.
SQPOLL Thread Safety
Bare sqd->thread access causes use-after-free — the task_struct is
freed via RCU after thread exit. sqd->thread is __rcu-annotated
(io_uring/sqpoll.h).
Access rules:
- Under
sqd->lock:sqpoll_task_locked(sqd) - Under RCU:
rcu_dereference(sqd->thread) - Assignment:
rcu_assign_pointer(sqd->thread, tsk) - For signaling: use
req->tctx->task, notsqd->thread. Seeio_req_normal_work_add()inio_uring/io_uring.c.
Task ownership: After wake_up_new_task() in
io_sq_offload_create(), the thread owns its reference. The creator must
NOT call put_task_struct() after start.
REPORT as bugs: Bare sqd->thread read. put_task_struct() after
thread start. sqd->thread used for signaling.
DEFER_TASKRUN Task Work Draining
With IORING_SETUP_DEFER_TASKRUN, task work goes to ctx->work_llist. In
non-submitter contexts (e.g., io_ring_exit_work()), only
io_move_task_work_from_local() can drain it. Calling it once before a
cancel loop leaves new work (generated during cancellation) undrained,
causing 100% CPU spin.
Rule: Any cancel loop calling io_uring_try_cancel_requests() outside
the submitter must call io_move_task_work_from_local() on every iteration.
See io_ring_exit_work() in io_uring/io_uring.c.
REPORT as bugs: io_move_task_work_from_local() called only once before
a cancel loop instead of on each iteration.
IOPOLL Completion and Reissue
IOPOLL posts CQEs when iopoll_completed is set. Early return without
setting it makes the request invisible to polling, causing a hang.
Rules:
io_complete_rw_iopoll()must always reachsmp_store_release(&req->iopoll_completed, 1). Never return early. Seeio_uring/rw.c.- Reissue (
-EAGAIN): setREQ_F_REISSUE | REQ_F_BL_NO_RECYCLEand fall through. The flush path inio_uring/io_uring.cchecksREQ_F_REISSUEand callsio_queue_iowq().
REPORT as bugs: Early return in io_complete_rw_iopoll() skipping
iopoll_completed.
Timeout Cancellation and Lock Ordering
Queuing task_work while holding ctx->timeout_lock (raw spinlock) causes
lock ordering violations — completion may call io_eventfd_signal() which
takes a regular spinlock, invalid on PREEMPT_RT.
Two-phase pattern:
- Under
timeout_lock:io_kill_timeout()cancels hrtimers and moves timeouts to a local list (never completes them). - After unlock:
io_flush_killed_timeouts()callsio_req_queue_tw_complete().
See io_uring/timeout.c.
REPORT as bugs: Task_work or completion calls while holding
ctx->timeout_lock.
Quick Checks
- Notif allocation before import:
io_alloc_notif()must precede buffer import in zero-copy paths. - Zero-copy flag detection:
IORING_OP_SEND_ZC,IORING_OP_SENDMSG_ZC, orIORING_RECVSEND_FIXED_BUFwith zc opcode require buffer lifetime validation. - Bundle buffer put:
io_put_kbufs()takes current transfer count (this_ret), not cumulative total. Seeio_recv_finish()inio_uring/net.c. - CQ overflow critical section: Set
ctx->cqe_sentinel = ctx->cqe_cachedbefore dropping CQ lock during overflow flush, forcing concurrent emitters throughio_cqe_cache_refill(). See__io_cqring_overflow_flush(). - zcrx DMA lifecycle: Create DMA mappings in
io_pp_zc_init(), unmap inio_pp_uninstall(). Seeio_uring/zcrx.c. - Registration tags on failure: Clear
node->tagviaio_clear_table_tags()before unregistering a failed all-or-nothing registration. Seeio_uring/rsrc.c. - Inflight tracking for MM-dependent requests: Call
io_req_track_inflight()in prep for requests needing the submitter'smm. Seeio_futex_prep()inio_uring/futex.c. - Task work tokens: Never fabricate
io_tw_token_ton stack. Useio_req_queue_tw_complete()outside task_work context, notio_req_task_complete(). - Buffer list upgrade safety: Destroy old
io_buffer_listand allocate fresh when upgrading to ring-mapped buffers. Seeio_register_pbuf_ring()inio_uring/kbuf.c. - Eventfd RCU freeing: Use
io_eventfd_put()(callscall_rcu()), neverio_eventfd_free()directly. Seeio_eventfd_do_signal()inio_uring/eventfd.c. - Cross-ring cloning accounting: Both rings must share
ctx->userandctx->mm_account. Seeio_clone_buffers()inio_uring/rsrc.c. - io_wq NULL after teardown:
io_queue_iowq()checks!tctx->io_wqandPF_KTHREAD. Seeio_uring/io_uring.c. - SQE flag hierarchy: Gate
READ_ONCE(sqe->field)on the broadest flag covering all variants. Seeio_nop_prep()inio_uring/nop.c. - SQE fields read before use:
READ_ONCE()SQE field into request before using it —req->buf_indexmay hold stale data from reuse. Seeio_uring_cmd_prep()inio_uring/uring_cmd.c. - RESIZE_RINGS and DEFER_TASKRUN:
io_register_resize_rings()requiresIORING_SETUP_DEFER_TASKRUN. New ring-geometry mutations need the same mutual exclusion. Seeio_uring/register.c. - Poll event scope: Generic poll code (
io_uring/poll.c) must not interpret event bits as errors —POLLERRsignals data availability for some sockets (e.g.,MSG_ERRQUEUE). Operation-specific interpretation belongs in issue handlers.