Kernel Privilege Escalation via Io_uring
Three design choices make io_uring a reliable source of kernel exploits.

Linux's io_uring interface was built to make I/O fast by cutting syscalls out of the hot path. That same design choice, shared memory between user space and kernel, async workers that run with kernel privilege, and user-managed object lifetimes, is why io_uring has become one of the most reliable sources of kernel privilege escalation bugs in the last five years. Understanding the mechanism explains the pattern in the CVE catalogue far better than treating each bug as an isolated coding mistake.
io_uring landed in Linux 5.1 in May 2019. Its core structure is two ring buffers, a Submission Queue and a Completion Queue, mapped into both user space and kernel space via mmap. A process fills out Submission Queue Entries describing an operation, read, write, poll, fsync, openat, and more than 60 other operation types, and the kernel drains them, doing the work asynchronously and posting results back as Completion Queue Entries. In SQPOLL mode, a dedicated kernel thread polls the submission queue continuously, so the application can issue zero syscalls after the ring is set up. That's the entire point: eliminate the per-I/O syscall tax that made high-throughput servers spend so much of their time in context switches.
Three design properties that jointly create the conditions for privilege escalation
The first property is that the ring buffers are shared memory the user controls directly. Nothing gates each write with a syscall the kernel can inspect at the moment it happens. A kernel worker picks up a submission entry and acts on it later. The entry is trusted at the point of submission but executed at some later point in time, and the object it refers to may have changed state in between. That gap, between what the user wrote and what the kernel eventually reads, is where a meaningful share of the CVEs in this piece live.
The second property is that io_uring's async workers, the io-wq kernel threads, run with kernel privilege rather than the privilege of the process that submitted the request. An io-wq thread performing an operation runs with kernel privilege regardless of the calling process's seccomp policy, because the seccomp check is wired to the syscall boundary. An io-wq thread performing IORING_OP_OPENAT does not check the calling process's seccomp policy, because the seccomp check is wired to the syscall boundary, and an SQE submitted through the ring never crosses that boundary the normal way. A sandbox that blocks the openat syscall outright will do nothing to stop the identical operation submitted as an io_uring entry. Under SQPOLL, the process doesn't even call io_uring_enter after setup, so the one syscall gate that normally exists in the picture is gone too. The practical upshot: seccomp, on its own, is not a sufficient sandbox for any process holding an io_uring file descriptor.
The third property is object registration. Fixed files and fixed buffers get registered to the ring using counts and addresses the user supplies, and the kernel has to reconcile that user-controlled lifetime with its own refcounting whenever an async teardown path runs. That handoff, multiple io-wq threads, a user who controls how long an object lives, and kernel objects shared across both, is hard to reason about correctly, and it recurs throughout the CVE record. No one of these three properties causes privilege escalation by itself. It's the combination, async execution under kernel privilege, acting on state the user controls, over lifetimes the user manages, that produces the exact conditions these bugs exploit.
Google's bug bounty data on io_uring's outsized share of kernel exploits
Google's kCTF and kernelCTF bug bounty programs paid out $1.8 million total since they started. In the year leading up to mid-2023, 60% of all submissions exploited io_uring, accounting for roughly $1 million of that total paid for io_uring bugs alone. That's a subsystem where researchers found they could reliably turn a bug into a working privilege escalation chain. That's a subsystem where researchers found they could reliably turn a bug into a working privilege escalation chain, submission after submission, at a rate no other part of the kernel came close to matching.
In that same window, io_uring vulnerabilities appeared in every single submission that managed to bypass Google's own internal kernel mitigations. Google's response was to disable io_uring by default in some of its own production environments, a decision that amounts to more than routine hardening. A company running some of the largest Linux fleets in the world looked at its own bounty data and decided the subsystem's risk profile didn't justify leaving it on by default. That's a structural judgment.
The recurring bug shapes across the CVE catalogue: refcount errors, race conditions, and integer overflows in registration paths
Refcount and use-after-free bugs are the most common shape in the catalogue, and they follow the same script almost every time: a fixed file or buffer gets freed or its reference count decremented on a path the kernel didn't expect, and something else in the kernel goes on using memory that's no longer valid.
CVE-2022-3910 is a clean example. When io_msg_ring was invoked with a fixed file, io_fput_file() decremented the refcount as though the file were an ordinary, non-fixed file, which it isn't supposed to do for a permanently registered file. The result was a use-after-free that a local, unprivileged process could turn into privilege escalation.
CVE-2022-29582 sits in the file table handling code, in io_uring/fdinfo.c and io_uring/rsrc.c, and comes from a race between file table updates and the teardown of an async operation. It carries a CVSS score of 7.8, was fixed in Linux 5.17.3 and 5.15.34, and has a public exploit posted at github.com/Ruia-ruia/CVE-2022-29582-Exploit. It needs no special privileges and affects default configurations, which is about as bad as an exposure profile gets.
CVE-2023-3389 comes from racing a cancel poll request against a linked timeout, producing a use-after-free in an hrtimer and, again, local privilege escalation. CVE-2024-0582 works through buffer ring mmap: register a buffer ring with IORING_REGISTER_PBUF_RING, mmap it, then free it, and the freed memory leaks in a way that leads to a crash or worse. It's rated CVSS 7.8, affects kernel versions from 6.4.0 onward, sits on a known-exploited-vulnerabilities tracking list, and has five separate public proof-of-concept repositories. The published exploit chain registers and unregisters the buffer ring, reclaims the freed memory using pipe_buffer objects, leaks kernel addresses from that reclaimed memory, overwrites modprobe_path, and then triggers modprobe to spawn a root shell. Every step in that chain is a known, well-worn Linux exploitation technique; io_uring is just the door that got it started.
Integer overflows occur in the registration paths specifically. CVE-2023-2598 is an overflow in the page-count accumulator inside IORING_REGISTER_BUFFERS, in io_sqe_buffers_register() in io_uring/rsrc.c, that allows out-of-bounds access to physical memory past the end of the registered buffer. It was fixed in Linux 6.3, and the public proof-of-concept leaks physical memory, derives the KASLR base from that leak, overwrites a socket's proto->ioctl function pointer with call_usermodehelper_exec, triggers it via an ioctl call, and gets a root shell through /bin/sh. That's a full chain, from a registration-path integer overflow straight through to arbitrary code execution as root.
CVE-2025-40364 is a slightly different shape: a missing ring-state commit before a buffer gets reimported inside io_req_prep_async(). An attacker crafts submissions that use provided buffers and forces asynchronous preparation, and the resulting inconsistency corrupts the kernel's own memory tracking. It needs no elevated privileges and no user interaction, and it went onto the NVD on April 18, 2025 with no public proof-of-concept as of the source date. The fix is to relinquish the buffer cleanly before async preparation and reimport it later if needed, rather than holding an inconsistent reference across the boundary.
And the pattern is already repeating in newer code. zcrx, io_uring's zero-copy network receive feature, has its own emerging set of bugs. CVE-2026-43121 is a race in io_zcrx_put_niov_uref(), which does a non-atomic check-then-decrement on user_refs that's only safe if serialized by rq_lock, while io_zcrx_scrub() modifies the same counter through atomic_xchg() without taking that lock. The same niov object ends up pushed onto the freelist twice, producing a double-free and an out-of-bounds write into adjacent slab memory. It's rated CVSS 4.7, medium severity, with no known public exploit as of early May 2026, and a fix addresses the broken synchronization pattern. CVE-2026-43174 is a use-after-free in zcrx's error handling: closing a queue didn't immediately terminate the page pools tied to it, because the original code released the zcrx context directly instead of going through refcounting, so a page pool could outlive its own parent context. Both bugs are the same structural problem, user-controlled registration paired with async teardown, appearing again in a feature that didn't exist when the earlier CVEs were filed.
Exploit chains that weaponize io_uring's role as a bridge to other kernel subsystems
PinTheft, disclosed in May 2026 by Aaron Esau of V12 Security, is maybe the clearest demonstration of io_uring acting as an amplifier for bugs that live somewhere else in the kernel. The underlying flaw sits in RDS, Reliable Datagram Sockets, in its zerocopy send path: a failed zerocopy send releases the same page twice, and each failed send steals one reference from the first user page. On its own, that's a narrow, awkward primitive, hard to turn into anything reliable.
io_uring's fixed buffer registration is what makes it reliable. By pairing the RDS refcount bug with fixed buffer registration, the exploit chain can overwrite the page cache of a SUID-root binary while it sits in memory, and turn an unprivileged local account into a root shell. The RDS bug has apparently been sitting in the kernel, latent, since version 4.17, years before io_uring's fixed buffer feature existed. io_uring didn't create the bug. It created the exploitation path for a bug that was already there.
A candidate fix went to the netdev mailing list on May 5, 2026, and the issue carries the identifier CVE-2026-43494, though no CVSS score had been assigned as of the source material's publication. Actually pulling the exploit off requires several things to line up: local code execution, the RDS and RDS_TCP kernel modules available and loadable, io_uring enabled, a readable SUID-root binary, and an x86_64 target. V12's research found RDS loadable by default only on Arch Linux among the common distributions it tested; most others either block the module from autoloading or don't ship it. A single-file proof-of-concept is public at github.com/tanzz1337/CVE-2026-43494-PinTheft-PoC, and multi-tenant systems, shared hosting, CI runners, container hosts, running an affected distribution are the ones with the most to worry about here.
CVE-2023-2598 makes the same argument from the other direction. There, the overflow isn't in some unrelated subsystem that io_uring merely helps exploit, it's in io_uring's own buffer registration code, and it's sufficient by itself to reach arbitrary code execution as root. Between the two, the lesson is the same: io_uring's privileged execution context isn't just a bridge to other bugs. Sometimes it's the payload delivery mechanism in its own right.
The Curing rootkit shows how io_uring's syscall bypass creates a detection blind spot
In April 2025, the security firm ARMO published a proof-of-concept rootkit, called Curing, that runs entirely through io_uring operations. It uses the same field of roughly 60 operation types that make the interface fast, and because those operations don't pass through the traditional syscall path, they don't produce the syscall-level events that most runtime security tools are built to watch. In ARMO's testing, Falco, Tetragon, Microsoft Defender, and most of the other Linux runtime security tools evaluated failed to catch it.
This is the same property as the seccomp bypass described earlier. It's the same property, appearing in a different tool. The kernel executes the operation on the process's behalf without that operation ever appearing as a syscall to any hook-based monitor, and that is what breaks seccomp filtering and syscall-based detection at the same time, for the same reason.
An attacker who escalates privilege through an io_uring bug can then use io_uring again, for the post-compromise stage of the intrusion, and do it in a way that evades the very detection tools a defender would normally rely on to notice something went wrong. The escalation and the evasion run through the same door.
Why these vulnerabilities keep recurring, and what the pattern implies for patching and exposure management
None of this reads as a patching failure. It's a design consequence. Async execution, shared mutable state, and user-controlled object lifetimes combine to produce a class of bug that appears in every new feature added to io_uring, and the zcrx CVEs from 2026 are the most recent proof that the pattern hasn't run its course. Whatever feature comes next will likely inherit the same three properties, because those properties are what make io_uring fast.
These bugs tend to surface through edge cases, races that only appear during teardown, integer boundaries hit only during registration, state changes that only matter after the ring topology shifts, rather than through the kind of normal-path fuzzing that catches most kernel bugs quickly. That means the rate of discovery is bounded by how creative the researchers looking for them happen to be, not by how much automated test coverage exists. Fuzzers are good at finding bugs on the paths they exercise constantly. They're much weaker at finding a race that only exists for a few instructions during a specific teardown sequence.
For anyone responsible for exposure management, that points toward a fairly clear hierarchy of options. Where it's operationally acceptable, disabling io_uring system-wide with sysctl kernel.io_uring_disabled=2 (available on kernel 6.6 and newer) removes the entire bug class in one move. Where the interface has to stay available, seccomp can still block the io_uring_setup, io_uring_enter, and io_uring_register syscalls for untrusted workloads, but that block has to happen at setup: once a ring exists, seccomp has no further leverage over what gets submitted to it. And container and sandbox permissions around which workloads get access to io_uring at all deserve the same scrutiny as any other privileged kernel interface, because that's functionally what io_uring is. The subsystem was built to remove overhead from the I/O path. What it also removed, in the process, was a set of checkpoints the rest of the kernel's security model quietly depended on.
