Skip to content

Catch up to liburing 2.9 / Linux 7.1-rc4 - #16

Merged
tchaloupka merged 15 commits into
tchaloupka:masterfrom
PetarKirov:feat/liburing-2.9
May 19, 2026
Merged

Catch up to liburing 2.9 / Linux 7.1-rc4#16
tchaloupka merged 15 commits into
tchaloupka:masterfrom
PetarKirov:feat/liburing-2.9

Conversation

@PetarKirov

@PetarKirov PetarKirov commented May 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Brings during up to liburing 2.9 / Linux 7.1-rc4 parity. The last
release (0.4.0) was synced to Linux 5.19, so the bulk of liburing 2.7–2.9
plus the post-2.9 kernel additions through 7.1-rc4 was missing. This PR
closes that gap.

Quantified: 18 new opcodes, 14 new register opcodes, 9 new
setup flags
, 5 new feature bits, plus the structural changes to
the SQE / CQE storage paths that the new variable-stride rings
(SQE128, SQE_MIXED, CQE32, CQE_MIXED) require. 38 of the 56
unittest blocks are new and exercise the new prep helpers, register
wrappers, and ring-storage paths; kernel-gated tests bail cleanly on
older hosts via checkKernelVersion().

The branch is shaped as 15 atomic commits, one feature group per
commit, intended to be reviewed and merged in order.

What's in here

Opcodes (18 new):

Kernel Op Prep helper
6.0 SEND_ZC prepSendZc, prepSendZcFixed
6.0 SENDMSG_ZC prepSendmsgZc
6.1 READ_MULTISHOT prepReadMultishot
6.5 WAITID prepWaitid
6.7 FUTEX_WAIT prepFutexWait
6.7 FUTEX_WAKE prepFutexWake
6.7 FUTEX_WAITV prepFutexWaitv
6.7 FIXED_FD_INSTALL prepFixedFdInstall
6.7 FTRUNCATE prepFtruncate
6.11 BIND prepBind
6.11 LISTEN prepListen
6.13 RECV_ZC prepRecvZc
6.13 EPOLL_WAIT prepEpollWait
6.13 READV_FIXED prepReadvFixed
6.13 WRITEV_FIXED prepWritevFixed
6.14 PIPE prepPipe, prepPipeDirect
6.16 NOP128 prepNop128
6.16 URING_CMD128 prepUringCmd128

Plus generic URING_CMD helpers (prepUringCmd, prepCmdSock,
prepCmdGetsockname, prepCmdDiscard) layered over the pre-existing
URING_CMD opcode.

Register opcodes (14 new):

Kernel Register op Wrapper
6.0 REGISTER_SYNC_CANCEL Uring.registerSyncCancel
6.0 REGISTER_FILE_ALLOC_RANGE Uring.registerFileAllocRange
6.8 REGISTER_PBUF_STATUS Uring.bufRingStatus
6.9 REGISTER_NAPI Uring.registerNapi
6.9 UNREGISTER_NAPI Uring.unregisterNapi
6.10 REGISTER_CLOCK Uring.registerClock
6.10 REGISTER_CLONE_BUFFERS Uring.cloneBuffers, cloneBuffersOffset
6.10 REGISTER_SEND_MSG_RING Uring.sendMsgRingSync
6.11 REGISTER_ZCRX_IFQ Uring.registerIfq
6.12 REGISTER_RESIZE_RINGS Uring.resizeRings
6.13 REGISTER_MEM_REGION Uring.registerMemRegion, registerWaitReg
6.15 REGISTER_QUERY — (raw opcode only)
6.16 REGISTER_ZCRX_CTRL — (raw opcode only)
6.16 REGISTER_BPF_FILTER Uring.registerBpfFilter

Setup flags (9 new):

Kernel Flag Notes
6.0 SINGLE_ISSUER promises a single issuer thread; unlocks DEFER_TASKRUN
6.1 DEFER_TASKRUN requires SINGLE_ISSUER
6.5 NO_MMAP rejected at wrapper boundary — needs caller-owned ring memory
6.5 REGISTERED_FD_ONLY rejected — depends on NO_MMAP path
6.6 NO_SQARRAY wrapper skips SQ-array init
6.13 HYBRID_IOPOLL IOPOLL with sleep-then-spin instead of pure busy-poll
6.18 CQE_MIXED see Storage-path changes
6.18 SQE_MIXED see Storage-path changes
6.18 SQ_REWIND see Storage-path changes

Existing SQE128 / CQE32 rings now have the storage-path support
they were missing.

Storage-path changes:

  • SubmissionQueue now selects the SQE stride (64 B or 128 B) at setup
    time based on SetupFlags.SQE128. The previous code hardcoded
    entries * 64, leaving the trailing cmd[] payload of SQE128 rings
    outside the mapped region — NOP128 and URING_CMD128 were unusable.
  • For SQE_MIXED rings, a new next128() accessor reserves two
    contiguous 64-byte slots and inserts a CQE_SKIP_SUCCESS NOP padder
    if a 128-byte SQE would straddle the ring wrap.
  • For SQ_REWIND rings, head() returns 0 unconditionally,
    flushTail() becomes a no-op, and localTail resets after each
    acknowledged batch.
  • CompletionQueue walks variable-stride CQEs: a per-ring shift
    doubles the cqes slice for CQE32 rings, and popFront / peekAt
    advance by 1 or 2 ring slots depending on the per-CQE F_32 flag.

Examples:

  • examples/zerocopy_echo — TCP echo over IORING_OP_SEND_ZC,
    asserts the kernel emits both the data CQE and the trailing
    notification CQE.
  • examples/futex_pingpong — two threads, io_uring FUTEX_WAIT woken
    via legacy futex(2) FUTEX_WAKE_PRIVATE.

Build / dev infra:

  • All dub.sdl package files now set targetPath "build" so example
    binaries land in build/ instead of in the example root. build/
    is git-ignored.
  • New Nix flake + direnv devshell (purely additive; doesn't touch
    dub). Happy to drop this commit if upstream prefers not to carry it.

Test plan

  • dub test -- --threads=1 — all 56 unit tests pass serially.
    The default parallel silly runner has transient -EMFILE flakes
    on io_uring_setup when many tests open rings at once (a known
    silly-runner artefact, not a regression). Serial run is stable.
  • dub build --config=betterC — green; the new APIs remain
    BetterC-compatible.
  • dub build --config=during-test-betterC — green.
  • dub build in each exampleexamples/echo_server,
    examples/zerocopy_echo, examples/futex_pingpong all build and
    land their binaries in their per-example build/ directory.
  • Probe sanitytests.register probe runs and confirms the
    6.18 host enumerates all new opcodes.

Test environment

The branch was developed and tested on:

  • OS: NixOS 25.11 (Xantusia), kernel 6.18.17 x86_64
  • CPU: AMD Ryzen 9 7940HX
  • Compiler: LDC 1.41.0 (DMD v2.111.0 frontend, LLVM 18.1.8)
  • dub: 1.39.0
  • Toolchain pinned via: the flake.nix introduced in this PR
    (nixpkgs rev 549bd84)

Sources used for grounding

Every opcode number, register opcode number, struct field, flag bit,
and feature bit was cross-checked against:

  • A local clone of torvalds/linux at v7.1-rc4 (io_uring/,
    include/uapi/linux/io_uring.h, include/linux/io_uring*.h).
  • A local clone of axboe/liburing at v2.9
    (src/include/liburing/io_uring.h plus the src/queue.c /
    src/register.c reference implementations of the new helpers).

Wrong opcode numbers are the single highest-risk class of bug in this
kind of catch-up work — they silently misroute requests on real
kernels — so each commit was reviewed against the kernel header
before it was finalised.

LLM disclosure

The bulk of this PR was drafted with Claude Opus 4.7 (high reasoning,
1M-token context)
, with a follow-up review pass by GPT-5.5 Codex (high
reasoning)
. Throughout, the assistants were grounded against the local kernel
and liburing git trees noted above rather than relying on training data. New
prep helpers and register wrappers have focused functional or ABI-surface tests
where the operation can be exercised on a stock test host; hardware- or
configuration-dependent paths are covered by compile-time layout checks and
structured-call smoke tests.

PetarKirov added 15 commits May 19, 2026 03:03
Set `targetPath "build"` in each dub manifest so binaries and intermediate
artifacts land in a per-package `build/` directory, and ignore `build/`
globally. Replaces the per-example `.gitignore` pattern, which required
adding a new entry for every new example binary.
- Extend `Operation` with `SEND_ZC=47`, `SENDMSG_ZC=48`,
  `READ_MULTISHOT=49`, `WAITID=50`.
- Add `prepSendZc`, `prepSendZcFixed`, `prepSendmsgZc`, `prepReadMultishot`,
  `prepWaitid` in `during.package`.
- Extend `CQEFlags` with `NOTIF`, `BUF_MORE`, `SKIP` and add the
  `IORING_NOTIF_USAGE_ZC_COPIED` constant for the zero-copy notification
  CQE's `res` field.
- Extend the `SubmissionEntry` rw_flags union with `waitid_flags`.

Tests (kernel-gated via `checkKernelVersion`):
- `tests.zerocopy`: TCP loopback `SEND_ZC` asserting the two-CQE pattern
  (send result + CQE_F_NOTIF notification).
- `tests.waitid`: fork/exit reaping via async `WAITID`.
- `tests.rw`: pipe-based `READ_MULTISHOT` with two-buffer provided ring.
…ge register ops

- Extend `Operation` with `FUTEX_WAIT=51`, `FUTEX_WAKE=52`, `FUTEX_WAITV=53`.
- Extend `RegisterOpCode` with `REGISTER_SYNC_CANCEL=24` and
  `REGISTER_FILE_ALLOC_RANGE=25` plus the backing structs
  `io_uring_sync_cancel_reg` and `io_uring_file_index_range`.
- Extend the `SubmissionEntry` rw_flags union with `futex_flags`.
- Add `futex_waitv` struct and the `FUTEX2_*` flag constants from
  `<linux/futex.h>` (size + PRIVATE/NUMA).
- Add prep helpers `prepFutexWait`, `prepFutexWake`, `prepFutexWaitv` in
  `during.package`.
- Add `Uring.registerSyncCancel` and `Uring.registerFileAllocRange`.

Tests (kernel-gated): `tests.futex` covers each new SQE op via the safe
mismatch/empty-waiter paths (the wait-vs-wake race in a single ring would
deadlock), plus a no-match `registerSyncCancel` and a sparse-files
`registerFileAllocRange` round-trip.
- New `futex_wait woken by helper thread` exercises a genuine wait/wake
  round-trip: main submits an io_uring FUTEX_WAIT, a pthread helper issues
  legacy `futex(SYS_futex, FUTEX_WAKE_PRIVATE, …)` to wake it. The helper
  retries every 5ms (bounded to ~1s) to defeat the inherent race between
  SQE submission and the kernel actually parking the waiter.
- New `registerSyncCancel cancels matching read` submits a blocked pipe
  read with `IOSQE_ASYNC` and a known `user_data`, then issues
  `register_sync_cancel` matching on that data and confirms both the
  cancel-count (>=1) and the cancelled CQE (`-ECANCELED` or `-EINTR`).
…D_INSTALL, FTRUNCATE

- Extend `Operation` with `FIXED_FD_INSTALL=54`, `FTRUNCATE=55` (Linux 6.7)
  and `BIND=56`, `LISTEN=57` (Linux 6.11).
- Add `IORING_FIXED_FD_NO_CLOEXEC` flag and extend the `SubmissionEntry`
  rw_flags union with `install_fd_flags`.
- Add prep helpers `prepFtruncate`, `prepFixedFdInstall`, `prepBind`,
  `prepListen` in `during.package`.

Tests:
- `tests.socket`: full BIND+LISTEN round-trip on TCP/127.0.0.1, verified by
  a regular connect()/accept() against the listening socket.
- `tests.fixed_fd`: ftruncate a regular file and verify via fstat; promote
  an OPENAT_DIRECT slot to a real fd via FIXED_FD_INSTALL and verify it is
  readable through a regular read(2).
…s register ops

- Extend `Operation` with `RECV_ZC=58`, `EPOLL_WAIT=59`.
- Extend `RegisterOpCode` with `REGISTER_PBUF_STATUS=26`, `REGISTER_NAPI=27`,
  `UNREGISTER_NAPI=28` plus backing structs `io_uring_buf_status` and
  `io_uring_napi`.
- Add `SubmissionEntry.zcrx_ifq_idx` union member for addressing the
  zerocopy-RX ifq table on RECV_ZC submissions.
- Add prep helpers `prepEpollWait`, `prepRecvZc` in `during.package`.
- Add `Uring.bufRingStatus`, `Uring.registerNapi`, `Uring.unregisterNapi`.

Tests:
- `tests.epoll_wait`: pipe + epoll fd, write to make the pipe readable,
  submit EPOLL_WAIT and assert it reports the pipe fd as ready.
- `tests.register`: `bufRingStatus` against a missing group reports an
  error (validates the io_uring_register plumbing); a `registerNapi` +
  `unregisterNapi` round-trip; static assertion of `RECV_ZC` opcode value
  (functional test requires a configured NIC, deferred to user code).
…bundles, uring_cmd helpers

- Extend `Operation` with `READV_FIXED=60`, `WRITEV_FIXED=61` (Linux 6.13),
  `PIPE=62` (Linux 6.14), `NOP128=63`, `URING_CMD128=64` (Linux 6.16).
- Extend `SubmissionEntry` unions to cover `uring_cmd_flags`, `nop_flags`,
  `pipe_flags`, `optlen`, `addr_len`, `optval`, and the `level`/`optname`
  overlay needed by socket `uring_cmd` ops.
- Add the `IORING_RECVSEND_BUNDLE` flag (Linux 6.10), the
  `IORING_MSG_RING_FLAGS_PASS` flag (Linux 6.3), the `SOCKET_URING_OP_*`
  subcommand enum, and `BLOCK_URING_CMD_DISCARD`.
- Add prep helpers in `during.package`: `prepReadvFixed`, `prepWritevFixed`,
  `prepPipe`, `prepPipeDirect`, `prepNop128`, `prepUringCmd`,
  `prepUringCmd128`, `prepCmdSock`, `prepCmdGetsockname`, `prepCmdDiscard`,
  `prepMsgRingCqeFlags`, `prepSendBundle`, `prepSendSetAddr`.
- Bump `Probe` capacity from 64 to 128 to accommodate the new opcodes.

SQE128 caveat:
- The existing SQE region mmaps `entries * SubmissionEntry.sizeof` (=64 B),
  which is wrong for rings created with `SetupFlags.SQE128` (kernel expects
  128 B/SQE). Fixing the storage requires a separate change to use a runtime
  stride; until then NOP128/URING_CMD128 work for users who maintain their
  own SQE storage. `tests/sqe128.d` documents this and only asserts opcode
  values.

Tests:
- `tests.pipe`: IORING_OP_PIPE creates a usable read/write pair, validated
  via write+read round-trip.
- `tests.rw`: `readv_fixed` / `writev_fixed` round-trip against a registered
  buffer, with two-iovec vectored payload.
- `tests.sqe128`: documented static-only assertion (see caveat above).
Previously the SQE region was mmaped at `entries * SubmissionEntry.sizeof`
(=64 B) regardless of `SetupFlags.SQE128`. With SQE128 the kernel expects
`entries * 128` and the trailing 64 B per slot is where URING_CMD128 reads
its `cmd[]` payload — so any access into that region from the D side fell
off the mapped pages.

Internals:
- `SubmissionQueue.sqes` (typed `SubmissionEntry[]`) becomes
  `sqesPtr` (raw `void*`) + `entries` + `stride`. `stride` is set to
  64 or 128 in `mapRings()` based on `params.flags & SetupFlags.SQE128`.
- New `ref SubmissionEntry slot(uint)` accessor does the stride math.
  The `next` / `put` / `putWith` hot path keeps its call-site shape.
- Destructor unmaps `entries * stride` bytes.

Tests in `tests.sqe128`:
- `nop128 round-trip on SQE128 ring`: instantiates the ring with
  `SetupFlags.SQE128`, submits NOP128, validates the CQE.
- `SQE128 cmd payload is addressable`: writes 64 B into the trailing
  `cmd[]` region of a slot and confirms NOP128 still completes
  cleanly — proves the second half of each slot is mapped and writable.
…appers

- Extend `RegisterOpCode` with the kernel 6.10+ register ops:
  `REGISTER_CLOCK=29`, `REGISTER_CLONE_BUFFERS=30`,
  `REGISTER_SEND_MSG_RING=31`, `REGISTER_ZCRX_IFQ=32`,
  `REGISTER_RESIZE_RINGS=33`, `REGISTER_MEM_REGION=34`,
  `REGISTER_QUERY=35`, `REGISTER_BPF_FILTER=37`.
- Add backing structs `io_uring_clock_register`, `io_uring_clone_buffers`,
  `io_uring_region_desc`, `io_uring_mem_region_reg`,
  `io_uring_zcrx_offsets`, `io_uring_zcrx_ifq_reg`, `io_uring_reg_wait`,
  plus the related flag constants (SRC_REGISTERED, DST_REPLACE,
  MEM_REGION_TYPE_USER, MEM_REGION_REG_WAIT_ARG).
- Add `EnterFlags.EXT_ARG_REG` (Linux 6.13) for the registered-wait-arg path.
- Add `Uring` methods: `registerClock`, `cloneBuffers[_offset]`,
  `resizeRings`, `registerMemRegion`, `registerWaitReg`,
  `submitAndWaitReg`, `submitAndWaitMinTimeout`, `registerIfq`,
  `registerBpfFilter`, and a static `sendMsgRingSync` (no ring required).
- `resizeRings` unmaps and re-maps SQ/CQ + SQE region using the kernel's
  new layout — works on top of the SQE128-stride fix.

Tests:
- `tests.api`: `resizeRings grows and shrinks` — grow from 8 to 32 SQEs,
  then shrink to 4, with NOP round-trips before, between and after to
  prove the cached pointers and the new kernel layout stay in sync.
- `tests.register`: `cloneBuffers` register a buffer in src, clone into
  dst, then read through dst with `prepReadFixed` against the cloned
  slot — proves the buffer table is actually shared.
- `tests.wait_reg`: clock-registration accepts MONOTONIC/BOOTTIME,
  registerWaitReg surface check (kernel may stub-reject, both branches
  asserted), and registerBpfFilter rejects an invalid prog fd.
- Extend `SetupFlags` with `SINGLE_ISSUER` (6.0), `DEFER_TASKRUN` (6.1),
  `NO_MMAP` (6.5), `REGISTERED_FD_ONLY` (6.5), `NO_SQARRAY` (6.6),
  `HYBRID_IOPOLL` (6.13), `CQE_MIXED` / `SQE_MIXED` / `SQ_REWIND` (6.18).
- Extend `SetupFeatures` with `REG_REG_RING` (6.3), `RECVSEND_BUNDLE` (6.10),
  `MIN_TIMEOUT` (6.13), `RW_ATTR` (6.13), `NO_IOWAIT` (6.16).

Tests:
- `tests.api single_issuer + defer_taskrun NOP round-trip`: instantiates a
  ring with `SINGLE_ISSUER | DEFER_TASKRUN` and runs a NOP through it.
  Kernel-gated on 6.1 (the version DEFER_TASKRUN requires SINGLE_ISSUER
  becomes meaningful), accepts `-EINVAL` from older kernels and bails.
…RAY, SQ_REWIND, SQE_MIXED

The previous commit only added the SetupFlags enum values for these
new 6.5–6.18 setup flags. This commit implements the runtime behaviour
so callers can actually use them.

setup():
- Reject `NO_MMAP` and `REGISTERED_FD_ONLY` with -EINVAL up-front. Both
  expect the caller to supply the ring memory, which this wrapper does
  not expose. Rejecting at the wrapper boundary gives a clear error
  instead of letting the kernel return -EINVAL after partial setup.

UringDesc.mapRings():
- Skip the SQ-array index initialization when `NO_SQARRAY` is set; the
  kernel no longer reads an indirection table in that mode (head and
  tail index the SQE array directly).
- Decode and cache the new `rewind` and `sqeMixed` flags on the
  SubmissionQueue for fast-path use during put / submit.

SQ_REWIND (Linux 6.18):
- `head()` returns 0 unconditionally — the kernel always starts reading
  from slot 0 in rewind mode.
- `flushTail()` becomes a no-op — the kernel doesn't consume a
  user-published tail in rewind mode.
- New `submitted(n)` callback resets `localTail` to 0 after the kernel
  acknowledges the batch. Threaded into all three submission paths
  (`submit`, `submit-and-wait`, the registered-ring variant) so the
  bookkeeping stays in sync regardless of which entry point is used.

SQE_MIXED (Linux 6.18):
- New `Uring.next128()` / `SubmissionQueue.next128()` return a slot
  suitable for a 128-byte SQE. On pure `SQE128` rings the slot stride
  is already 128 bytes, so next128 short-circuits to `next()`; on
  `SQE_MIXED` rings the stride is 64 bytes and a 128-byte SQE has to
  occupy two consecutive slots.
- New private `reserve128Slot()` handles the two-slot reservation. If
  the would-be second slot crosses the ring-wrap boundary, it inserts
  a `CQE_SKIP_SUCCESS` NOP padder so the 128-byte SQE never straddles
  the wrap (the kernel reads SQEs by slot index and would otherwise
  read the extension half from a different ring iteration).
- `put(SubmissionEntry)` auto-detects `NOP128` / `URING_CMD128`
  opcodes and routes them through the two-slot path.

Tests:
- `tests.api no_mmap setup is rejected by wrapper` — covers both
  `NO_MMAP` alone and `NO_MMAP | REGISTERED_FD_ONLY`.
- `tests.api no_sqarray NOP round-trip` — full NOP submit/wait on a
  `NO_SQARRAY` ring.
- `tests.api sq_rewind submits fresh entries from slot zero` — two
  consecutive NOP round-trips through a `NO_SQARRAY | SQ_REWIND`
  ring, verifying the second iteration reuses slot 0.
- `tests.sqe128 sqe_mixed supports 64-byte and 128-byte SQEs` — one
  64-byte NOP plus one 128-byte NOP128 through an `SQE_MIXED` ring,
  asserting submit consumes 3 SQ slots.
- `tests.sqe128 sqe_mixed pads a 128-byte SQE that would wrap the
  ring end` — drains the ring head past the wrap boundary, fills to
  the last slot, then submits a NOP128 to exercise the
  `reserve128Slot` wrap-pad path and assert the padder is suppressed
  from the CQ stream.
- New `examples/zerocopy_echo`: loopback TCP echo over `IORING_OP_SEND_ZC`,
  asserts the two-CQE pattern (send result with `F_MORE`, then the
  `F_NOTIF` zero-copy completion).
- New `examples/futex_pingpong`: io_uring `FUTEX_WAIT` on a 32-bit private
  futex woken by a helper pthread issuing legacy `futex(SYS_futex,
  FUTEX_WAKE_PRIVATE, …)`. Bounded retry on the wake side defeats the
  inherent race between SQE submission and the kernel parking the waiter.
- README: update the "Linux 5.14" line to "Linux 6.x / liburing 2.9", bump
  the version recommendation to `~>0.5.0`, fix the json snippet's missing
  closing quote.
- CHANGELOG: cut `[0.5.0]` from the accumulated Unreleased entries.

Bug fix surfaced while writing the futex example:

- `prepFutexWait` / `prepFutexWake` were being called with `mask = ~0UL`.
  The kernel rejects masks whose bits exceed the futex's declared size
  (`FUTEX2_SIZE_U32` → mask must fit in 32 bits) with `-EINVAL`. The Phase
  2 wait/wake test silently bailed on `-EINVAL`, masking the bug. Added
  `FUTEX_BITSET_MATCH_ANY = 0xFFFFFFFF` and `FUTEX2_MPOL` (Linux 6.18),
  updated tests/example to use the correct mask, and tightened the wait
  test so the silent-bail only fires on `-EOPNOTSUPP`.
Picks up the kernel additions that landed between liburing 2.9 and the
current Linux 7.1-rc4 tree. Most are flag-bit / struct-field deltas the
previous commits couldn't surface yet because they're newer than
liburing's last cut.

io_uring.d:
- `SubmissionEntry.write_stream` byte field (6.18 — stream id for
  WRITE / WRITEV / SEND on file-stream-aware backends).
- `CQEFlags.F_32` and `F_TSTAMP_HW` (6.18), plus
  `IORING_TIMESTAMP_HW_SHIFT` / `TYPE_SHIFT`.
- `TimeoutFlags.MULTISHOT` (6.4) and `IMMEDIATE_ARG` (6.18).
- `PollFlags.ADD_LEVEL` (6.0 — level-triggered poll).
- `CancelFlags.CANCEL_FD_FIXED` (6.1), `CANCEL_USERDATA` /
  `CANCEL_OP` (6.6).
- `IORING_ACCEPT_DONTWAIT` and `IORING_ACCEPT_POLL_FIRST` (6.6),
  `IORING_SEND_VECTORIZED` (7.0), `IORING_MSG_RING_CQE_SKIP` (6.5).
- All six `IORING_NOP_*` flags (6.13/6.18), `IORING_URING_CMD_FIXED`
  (6.0) / `_MULTISHOT` (6.18) / `_MASK`, `IORING_RW_ATTR_FLAG_PI` (6.13).
- `EnterFlags.ABS_TIMER` (6.10) and `NO_IOWAIT` (6.16).
- `RegisterOpCode.REGISTER_ZCRX_CTRL` (6.16) and
  `SetupParameters.PROVIDED_BUFFER_RING_OFFSET` / `_SHIFT` / `MMAP_MASK`.
- ZCRX scaffolding: `io_uring_zcrx_rqe`, `io_uring_zcrx_cqe`,
  `io_uring_zcrx_area_reg`, `IORING_ZCRX_AREA_DMABUF`,
  `IORING_ZCRX_AREA_SHIFT` / `_MASK`, plus the `io_timespec` struct used
  by the new timestamp paths.
- `io_uring_napi` extended with `opcode` and `op_param` to match the
  6.18 kernel layout. Backward-compatible — zero-initialised callers
  still get the original register/unregister behaviour. New
  `IO_URING_NAPI_REGISTER_OP` / `STATIC_ADD_ID` / `STATIC_DEL_ID`
  opcode constants and `TRACKING_DYNAMIC` / `STATIC` / `INACTIVE`
  strategy constants.

Tests (each kernel-gated via `checkKernelVersion`):
- `tests.api nop inject_result drives custom cqe.res` — sets
  `IORING_NOP_INJECT_RESULT` plus `sqe.len`, asserts the value surfaces
  in `cqe.res`. Gated on 6.13.
- `tests.timeout timeout multishot fires repeatedly` — counts three
  CQEs from a single MULTISHOT timeout, asserts only the last lacks
  `CQEFlags.MORE`. Gated on 6.4.
- `tests.timeout timeout immediate_arg accepts inline nanoseconds` —
  builds a TIMEOUT SQE by hand with `IMMEDIATE_ARG`, places the
  duration in `sqe.addr`, asserts the expected `-ETIME`. Gated on 6.18.
- `tests.poll poll add_level on pre-readable eventfd` — arms a
  level-triggered poll against an already-readable eventfd and asserts
  the SQE is accepted and completes. Gated on 6.0.

Note: The `CQE32` / `CQE_MIXED` storage-path support that the new
`CQEFlags.F_32` marker drives is implemented in the follow-up commit
("wire up runtime support for CQE32 / CQE_MIXED completion rings").
…mpletion rings

The previous commit added the `SetupFlags.CQE32` / `CQE_MIXED` enum
values and the `CQEFlags.F_32` per-CQE marker but didn't extend the
CompletionQueue to walk variable-stride completion entries. This
commit makes those flags actually usable.

UringDesc.mapRings():
- Cache a `shift` of 1 for `CQE32` rings, 0 otherwise. The `cqes`
  slice is sized as `entries << shift` so the second 16 bytes of each
  32-byte CQE remain addressable.

CompletionQueue.front() / popFront() / peekAt():
- `front()` indexes the cqes array as `(localHead & ringMask) << shift`,
  which collapses to the original behaviour on 16-byte rings and skips
  the trailing half on pure `CQE32` rings.
- `popFront()` advances by `cqeSlots(front)` rather than a fixed 1.
  `cqeSlots` returns 2 when the CQE carries `CQEFlags.F_32`, the
  kernel marker for "this CQE is 32 bytes". This is what makes
  `CQE_MIXED` rings work: entries can be 16 or 32 bytes within the
  same ring, and only the per-CQE flag tells the consumer which is
  in front.
- `peekAt(i)` walks i CQEs from the head, decoding each entry's
  stride on the way rather than assuming a fixed step.

Tests:
- `tests.api cqe_mixed advances over 32-byte CQEs` — submits a NOP
  with `IORING_NOP_CQE32` to a `CQE_MIXED` ring, verifies the CQE
  comes back with `F_32` set, then confirms a subsequent regular NOP
  completes cleanly (i.e. popFront correctly advanced over both ring
  slots of the 32-byte CQE rather than landing on its extension half).
Purely additive: a flake-parts-based `flake.nix` pins `ldc`, `dub`, and
`dtools` from a known-good nixpkgs revision, and an `.envrc` lets
`nix-direnv` users get the toolchain by stepping into the repo. Does
not change how dub builds, what dub uses, or any existing build
instructions — contributors who don't use Nix are unaffected.

Drop this commit if upstream prefers not to carry Nix infrastructure
in-tree.
@tchaloupka
tchaloupka merged commit 499af21 into tchaloupka:master May 19, 2026
0 of 5 checks passed
@tchaloupka

Copy link
Copy Markdown
Owner

thanks!

tchaloupka added a commit that referenced this pull request May 19, 2026
Follow-up to PR #16: complete the io_uring.h uapi mirror to v7.1-rc4
(5200f5f4) incl. io_uring_buf_reg layout and msg/zcrx-ctrl constants,
add liburing 2.9 helper parity (14 prep/register helpers) with smoke
tests, and fix the two CI failures from PR #16 — the O_CLOEXEC import
on pre-2.098 frontends and a betterC link error (_memset128) from a
default-initialised slice array in tests/rw.d.
tchaloupka added a commit that referenced this pull request May 19, 2026
Bugs found reviewing PR #16, each with a regression test:

- CQE32: cqes mmap was sized for 16-byte CQEs, so CQE32|NO_SQARRAY rings
  read past the mapping
- io_uring_getevents_arg waits never set IORING_ENTER_EXT_ARG
- submitAndWaitReg used the wrong EXT_ARG_REG ABI; registerWaitReg passed
  a non-page-aligned region size
- CQE_MIXED: length() counted 16-byte slots instead of logical CQEs
- prepSendBundle crashed on an empty slice and diverged from liburing's
  send_bundle — realigned (now takes a len argument)
- put/putWith built 128-byte ops in a single slot on SQE_MIXED rings
- SQ_REWIND: a partial submit dropped the unsubmitted SQEs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants