Pure-Rust io_uring emulator. API-compatible with io-uring 0.7, runs entirely in userspace, and works under Miri for undefined-behavior detection.
Code that targets io_uring on Linux is notoriously difficult to test:
- The real kernel interface is only available on Linux 5.1+
- Miri cannot execute syscalls, so any
io_uringcode is invisible to it - Pointer-heavy SQE/CQE buffers are a common source of UB that goes undetected
miring replaces the kernel with a cooperative userspace emulator. The SQ/CQ ring protocol, buffer registration, and completion semantics are all faithfully reproduced using heap allocations and atomics — giving Miri full visibility into every pointer dereference your io_uring code makes.
[target.'cfg(not(miri))'.dependencies]
io-uring = "0.7"
[target.'cfg(miri)'.dependencies]
miring = "0.1"#[cfg(not(miri))]
use io_uring;
#[cfg(miri)]
use miring as io_uring;Your code uses io_uring::opcode, io_uring::types, etc. as normal. Under Miri, it transparently switches to the emulator.
| Feature | Status |
|---|---|
| SQ/CQ ring protocol (push, submit, complete, overflow) | Emulated |
| 75 opcode builders (full io-uring 0.7.11 parity) | Emulated |
| 63 dispatch handlers that touch user memory | Emulated |
Linked operations (IO_LINK, IO_HARDLINK) |
Emulated |
SKIP_SUCCESS (CQE suppression) |
Emulated |
Buffer selection (IOSQE_BUFFER_SELECT + CQE flags) |
Emulated |
RecvMsgOut::parse (multishot recvmsg buffer parsing) |
Emulated |
Probe (reports actually-handled ops) |
Emulated |
Entry128 / 32-byte CQEs |
Emulated |
Builder setup flags (coop_taskrun, single_issuer, ...) |
Accepted (no-op) |
| Actual I/O (disk, network, etc.) | Not emulated |
The emulator does not perform real I/O. Instead, each dispatch handler walks the same user-memory pointers the kernel would — reads from write buffers, writes sentinel bytes into read buffers — so that Miri can detect any UB in your buffer management, lifetime handling, or pointer arithmetic.
All 75 builder structs from io-uring 0.7.11 are implemented:
Nop, Read, Write, Readv, Writev, ReadFixed, WriteFixed, Fsync, PollAdd, PollRemove, SyncFileRange, SendMsg, RecvMsg, Timeout, TimeoutRemove, TimeoutUpdate, Accept, AcceptMulti, AsyncCancel, AsyncCancel2, LinkTimeout, Connect, Fallocate, OpenAt, OpenAt2, Close, Statx, FilesUpdate, ProvideBuffers, RemoveBuffers, Send, Recv, RecvMulti, RecvBundle, RecvMultiBundle, RecvMsgMulti, SendZc, SendMsgZc, SendBundle, RecvZc, Splice, Tee, Shutdown, RenameAt, UnlinkAt, MkDirAt, SymlinkAt, LinkAt, MsgRing, MsgRingSendFd, Socket, Bind, Listen, Ftruncate, Fadvise, Madvise, EpollCtl, EpollWait, Read(Multi), GetXattr, SetXattr, FGetXattr, FSetXattr, UringCmd16, UringCmd80, SetSockOpt, FutexWait, FutexWake, FutexWaitV, WaitId, FixedFdInstall, ReadvFixed, WritevFixed, Pipe
miring's atomic operations go through a shim that swaps in Loom primitives under cfg(loom), enabling model-checked verification of the ring protocol's concurrency:
RUSTFLAGS='--cfg loom' cargo test --release --lib loom_cargo test # 94 tests, all pass
cargo +nightly miri test # same 94 tests under Miri- No real I/O. Dispatch handlers validate buffer pointers and simulate results (e.g.
Readfills the buffer with0xAA,OpenAtreturns fd 42). They do not interact with the filesystem, network, or any kernel subsystem. - Single-threaded dispatch. Completions are produced by a background thread that joins on the next
submit()call — this is sufficient for Miri and Loom but does not model kernel-level concurrency. - No
io_uring_registerfile/buffer tables.register_buffers,register_files, etc. are accepted but don't change dispatch behavior. - Setup flags are no-ops.
setup_sqpoll,setup_iopoll,setup_coop_taskrun, etc. are accepted by the builder for API compatibility but have no effect on emulation.
MIT OR Apache-2.0