Overview
| Attribute | Value |
|---|---|
| CVE ID | CVE-2026-43121 |
| Related | CVE-2026-23263 (page array leak, same subsystem) |
| CVSS Score | ~7.8 (est., NVD enrichment pending) |
| Vendor | Linux Kernel |
| Component | io_uring/ZCRX (zero-copy receive) |
| Vulnerability Type | Missing bounds check → OOB heap write; Race condition → double-free |
| Affected | Linux kernel 6.12–6.19 (CONFIG_IO_URING_ZCRX=y) |
| Patched | ✅ Commit 770594e (bounds check) + 003049b1c4fb (atomic race fix), backported to v6.18.16 |
| Exploit Status | ⚠️ PoC demonstrated (kernel module + userspace harness) |
| KEV Added | Not yet |
What Is ZCRX?
io_uring’s ZCRX (zero-copy receive) is a subsystem new to Linux 6.15 that lets userspace receive network packets directly into a registered memory region — no kernel copy needed. The NIC DMAs into the region, the kernel posts a completion event, and the user returns the slot when done via a refill queue.
The region is divided into 4KB slots. Each slot has a net_iov struct tracking its state. The kernel manages free slots with:
freelist[]— a stack of available slot indices, allocated askvmalloc_array(nr_iovs, sizeof(u32))free_count— the stack depth, also used as the write index for the next push
The bug is simple. There is no upper bound check on free_count.
The Vulnerability
Bug #1: Missing Bounds Check (Freelist OOB)
static void io_zcrx_return_niov_freelist(struct net_iov *niov)
{
struct io_zcrx_area *area = io_zcrx_iov_to_area(niov);
spin_lock_bh(&area->freelist_lock);
area->freelist[area->free_count++] = net_iov_idx(niov); // NO BOUNDS CHECK
spin_unlock_bh(&area->freelist_lock);
}
Two separate kernel teardown paths both return niovs to the same freelist. When they overlap, free_count exceeds num_niovs — and the write at freelist[free_count] walks past the end of the allocated array.
Result: a 4-byte out-of-bounds write into adjacent slab memory.
Bug #2: Race Condition → Double-Free (CVE-2026-43121)
The io_zcrx_put_niov_uref() function uses a non-atomic check-then-decrement on user_refs. Concurrent scrub and refill paths can both claim the final reference:
CPU0 (refill, holds rq_lock) CPU1 (scrub, no rq_lock)
───────────────────── ─────────────────────
atomic_read(uref) → 1
atomic_xchg(uref, 0) → 1
return_niov_freelist(niov) [PUSH #1]
atomic_dec(uref) → wraps to -1
returns true
return_niov_freelist(niov) [PUSH #2: DOUBLE-FREE]
Same niov pushed twice → free_count > nr_iovs → subsequent pushes write past the array end.
The OOB value is a niov index — a small integer between 0 and N-1. “That sounds useless. It is not.”
Exploitation
Write Primitive
The freelist[] array is allocated via kvmalloc_array(nr_iovs, sizeof(u32)). For small N values, this lands in the kmalloc-64 slab cache. The 4-byte OOB write at offset num_niovs * 4 corrupts the header of the next slab object.
Confirmed effects:
- Adjacent slab corruption:
kmalloc-64fields overwritten (e.g.,0xAABBCCDD → 0x00000007) - Credential manipulation: With controlled kernel context, UID transition from non-root to root demonstrated via
prepare_creds()+ credential zeroing +commit_creds()
Requirements
| Requirement | Detail |
|---|---|
| Capability | CAP_NET_ADMIN (enforced at io_register_zcrx_ifq()) |
| Hardware | NIC supporting page pool-backed memory providers (mlx5, nfp) |
| Kernel config | CONFIG_IO_URING_ZCRX=y |
| Container | ⚠️ CAP_NET_ADMIN commonly granted in Kubernetes networking plugins and Docker containers |
This makes the issue particularly relevant in container environments where CAP_NET_ADMIN is routinely granted.
The Fix
Bounds Check (commit 770594e)
static void io_zcrx_return_niov_freelist(struct net_iov *niov)
{
struct io_zcrx_area *area = io_zcrx_iov_to_area(niov);
guard(spinlock_bh)(&area->freelist_lock);
if (WARN_ON_ONCE(area->free_count >= area->nia.num_niovs))
return;
area->freelist[area->free_count++] = net_iov_idx(niov);
}
Atomic Race Fix (commit 003049b1c4fb)
// Before (vulnerable): non-atomic check-then-decrement
if (niov->user_refs) {
niov->user_refs--;
if (!niov->user_refs)
io_zcrx_return_niov(niov);
}
// After (fixed): atomic compare-and-exchange loop
u32 refs = atomic_read(&niov->user_refs);
do {
if (!refs)
return;
} while (!atomic_try_cmpxchg(&niov->user_refs, &refs, refs - 1));
if (refs == 1)
io_zcrx_return_niov(niov); // only winner pushes
io_uring Attack Surface
Google’s security team reported that 60% of kCTF VRP Linux kernel exploits in 2022 targeted io_uring. Multiple container runtimes (gVisor, ChromeOS) disable io_uring entirely due to its attack surface.
ZCRX adds shared-memory buffer management between kernel and userspace — exactly the pattern where atomicity bugs hide. This is the third io_uring CVE in recent months, and it won’t be the last.
Comparison with Related CVEs
| CVE | Bug | Primitive | Race? | Bypasses Copy Fail? |
|---|---|---|---|---|
| CVE-2026-23263 | Page array leak | Memory leak | ❌ | N/A |
| CVE-2026-43121 | Freelist OOB + double-free | 4-byte controlled OOB write | ✅ | ❌ (different path) |
| CVE-2026-31431 | Copy Fail (authencesn) | 4-byte page cache write | ❌ | N/A |
| CVE-2026-43284 | Dirty Frag (ESP) | 4-byte arbitrary STORE | ❌ | ✅ Bypasses algif_aead |
Remediation
Check Vulnerability
# Check if ZCRX is enabled in your kernel config
grep CONFIG_IO_URING_ZCRX /boot/config-$(uname -r)
# Check kernel version (6.18.16+ has the fix)
uname -r
Immediate Mitigations
# Option 1: Restrict io_uring via seccomp (recommended for containers)
# Add to container runtime profile or seccomp policy:
# SCMP_SYS(io_uring_setup) → ENOSYS
# SCMP_SYS(io_uring_enter) → ENOSYS
# SCMP_SYS(io_uring_register) → ENOSYS
# Option 2: Restrict CAP_NET_ADMIN in container runtime profiles
# Remove CAP_NET_ADMIN from default Docker/K8s capabilities
# Option 3: Disable io_uring system-wide (aggressive, may break workloads)
sysctl -w kernel.io_uring_disabled=2
Proper Fix
- Update kernel to 6.18.16+ or backport commits
770594eand003049b1c4fb - All major distros should ship fixes in upcoming kernel updates
Disclosure Timeline
| Date | Event |
|---|---|
| 2026-03-18 | CVE-2026-23263 assigned (page array leak, related) |
| 2026-04-21 | Commit 770594e adds WARN_ON_ONCE bounds check |
| 2026-05-03 | Mohamed salem Eddah requests CVE for freelist OOB on oss-sec |
| 2026-05-03 | Greg KH responds (standard CVE process) |
| 2026-05-04 | Pavel Begunkov (io_uring maintainer) comments on thread |
| 2026-05-07 | Extended oss-sec discussion (Jens Axboe, Solar Designer) |
| 2026-05-08 | ze3tar publishes “Damned OOB” writeup |
| 2026-05-10 | CVE-2026-43121 assigned (user_ref race → double-free → OOB) |
Sources
- ze3tar — “Damned OOB” — Primary writeup
- SnailSploit — io_uring/zcrx Race Condition
- oss-sec — CVE request thread
- NVD — CVE-2026-43121
- Kernel commit 770594e — bounds check
- Kernel commit 003049b1c4fb — atomic race fix
- Linux 6.18.16 ChangeLog
See also: CVE-2026-31431 — Copy Fail · CVE-2026-43284 — Dirty Frag · CVE Database