Linux io_uring: Asynchronous Kernel Privilege Escalation

Article Hero

Overview & Threat Landscape#

Introduced in Linux kernel 5.1, io_uring was engineered to resolve a fundamental performance bottleneck: the overhead of synchronous system calls. In high-throughput storage and networking workloads running on modern hardware, transitioning between user-space and kernel-space across thousands of individual read()orwrite() calls degrades CPU efficiency, an issue exacerbated by Kernel Page-Table Isolation (KPTI) mitigations against Meltdown.

To bypass this transition cost, io_uring establishes a lockless, shared-memory interface between user-space and the kernel using two ring buffers: a Submission Queue (SQ) and a Completion Queue (CQ). Instead of issuing individual system calls, user processes write Submission Queue Entries (SQEs) directly into mapped memory and notify the kernel in batches using io_uring_enter.

However, this asynchronous architecture introduced a massive and highly complex kernel attack surface. Between 2021 and 2026, io_uring has accounted for dozens of critical local privilege escalation (LPE) vulnerabilities (including CVE-2022-29582, CVE-2023-2598, CVE-2024-0582, and CVE-2026-80713). The subsystem requires the kernel to manage asynchronous request lifecycles, background worker threads (io-wq), fixed buffer tables, and credential caching. A synchronization flaw in any of these mechanisms enables unprivileged local users to trigger Use-After-Free (UAF) conditions and overwrite kernel credentials (struct cred) to achieve host root execution.

[!WARNING] Standard Seccomp-BPF filters cannot inspect operations submitted through io_uring. When an application submits file or network operations embedded inside an SQE, the kernel dispatcher processes them without triggering individual openatorwrite seccomp hooks, turning io_uring into an unmonitored execution side-channel.


Vulnerability & Attack Root-Cause Analysis#

The primary source of exploitable vulnerabilities in io_uring lies in object lifetime mismanagement across asynchronous worker boundaries.

When an unprivileged process requests an asynchronous I/O operation (such as registering fixed memory buffers with IORING_REGISTER_BUFFERS), the kernel allocates internal tracking structures within struct io_ring_ctx. If an operation cannot complete synchronously, the kernel hands off the request to an asynchronous kernel workqueue thread:

  1. Reference Counting Glitches: Requests submitted to io_uring reference memory pages, file descriptors, and task identity credentials. Race conditions between cancelation routines (io_cancel) and active execution workers frequently lead to premature object frees.
  2. Fixed Buffer Confusion: To optimize memory mapping, io_uring allows applications to pre-register user-space buffers (io_uring_register). Flaws in page pinning logic (such as integer overflows in buffer table indexes) allow attackers to manipulate page reference counts, causing the kernel to write raw data into freed page frames.
  3. Identity Impersonation via Personalities: io_uring supports caching process credentials (io_register_personality). If an attacker tricks the kernel into retaining elevated credentials during asynchronous submission, subsequent unprivileged SQEs execute under the retained privilege set.
sequenceDiagram
    autonumber
    participant Attacker as Unprivileged Process (UID 1000)
    participant SharedMem as Mapped SQ/CQ Ring Buffers
    participant KernelSub as io_uring Subsystem (Ring 0)
    participant Worker as Async Worker Thread (io-wq)
    participant KernelCred as Kernel struct cred (Root Target)

    Attacker->>KernelSub: Setup io_uring context (io_uring_setup)
    KernelSub-->>SharedMem: Allocate and mmap SQ and CQ ring buffers
    Attacker->>SharedMem: Write crafted SQEs registering fixed buffers
    Attacker->>KernelSub: Notify batch submission (io_uring_enter)
    KernelSub->>Worker: Dispatch asynchronous request to worker thread
    Attacker->>KernelSub: Trigger asynchronous cancelation race condition
    Note over KernelSub,Worker: Premature free of request object leaves dangling pointer
    Worker->>Worker: Execute pending work on freed slab slot (Use-After-Free)
    Attacker->>SharedMem: Spray synthetic struct cred objects across slab cache
    Worker->>KernelCred: Overwrite task credentials UID 1000 with UID 0
    KernelCred-->>Attacker: Process granted effective host root privileges

Exploit Architecture & Memory Corruption Pipeline#

The diagram below details the operational stages of an io_uring privilege escalation exploit, from initial ring mapping to ring-0 credential corruption:

flowchart TD
    subgraph UserSpace [User-Space Unprivileged Context]
        AttackerProc["Attacker Exploit Binary (UID 1000)"]
        SQRing["Submission Queue: Crafted IORING_OP_PROVIDE_BUFFERS"]
        CQRing["Completion Queue: Ring Event Polling"]
    end

    subgraph KernelMemoryMap [Shared Mapped Memory Boundary]
        MmapTrap["mmap(io_uring_setup file descriptor)"]
        RingContext["Kernel struct io_ring_ctx Context"]
    end

    subgraph KernelExecution [Kernel Ring-0 Subsystems]
        SlabCache["Slab Allocator: kmalloc-512 / cred_jar"]
        AsyncThread["Async Worker Thread: io_sq_thread"]
        UAFSlot["Use-After-Free / Dangling Buffer Descriptor"]
        TargetCred["Target Process struct cred (uid=0)"]
    end

    AttackerProc --> MmapTrap
    MmapTrap --> SQRing
    MmapTrap --> CQRing
    SQRing --> RingContext
    RingContext --> AsyncThread
    AsyncThread --> SlabCache
    AttackerProc -->|Trigger Cancelation Race| UAFSlot
    UAFSlot -->|Heap Spraying & Overwrite| TargetCred
    TargetCred -->|Elevate Privileges| AttackerProc

Attack Path Step-by-Step#

Consider how a security researcher or penetration tester models this exploit primitive inside a Linux environment.

Step 1: Allocating the Ring Context#

The exploit establishes the shared memory boundary using the io_uring_setup system call:

C
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <linux/io_uring.h>
#include <sys/mman.h>

/*
 * Educational Proof-of-Concept:
 * Demonstrates unprivileged io_uring context setup and ring mapping.
 */
int main(void)
{
    struct io_uring_params params = {0};
    
    // Request an io_uring instance with 64 submission entries
    int ring_fd = syscall(__NR_io_uring_setup, 64, &params);
    if (ring_fd < 0) {
        perror("[-] io_uring_setup failed (May be disabled by seccomp)");
        return EXIT_FAILURE;
    }

    printf("[+] io_uring instance created successfully. FD: %d\n", ring_fd);
    printf("[*] Subsystem Features: 0x%x\n", params.features);
    printf("[*] SQ Entries: %u, CQ Entries: %u\n", params.sq_entries, params.cq_entries);

    // Map submission queue and completion queue rings into user space
    // Once mapped, memory corruption in kernel async workers directly mirrors here.
    
    close(ring_fd);
    return EXIT_SUCCESS;
}

Step 2: Triggering Asynchronous Race and Heap Spray#

  1. The exploit submits a chained sequence of IORING_OP_PROVIDE_BUFFERSandIORING_OP_REMOVE_BUFFERS operations designed to trigger an asynchronous reference count decrement.
  2. An asynchronous worker thread receives the operation while the exploit binary concurrently issues a cancelation request (IORING_OP_ASYNC_CANCEL).
  3. Due to missing locking around buffer list unlinking, the kernel frees the underlying buffer descriptor while the worker thread retains a pointer to the memory slot.
  4. The exploit floods the target kmalloc slab cache with controlled memory structures (such as struct file or credential descriptors).
  5. When the worker writes the completion status, it writes into the reallocated memory slot, granting arbitrary write capabilities inside the kernel.
# Compiling and executing the proof-of-concept trigger on vulnerable kernel
gcc -O2 trigger_uaf.c -o trigger_uaf
./trigger_uaf

## Observing kernel ring-0 state transition
id
## uid=0(root) gid=0(root) groups=0(root)

[!CAUTION] Because io_uring is available by default to unprivileged local users on standard Linux distribution kernels, an unprivileged user inside a container can exploit this vulnerability to escape to the underlying host node if io_uring_setup is not explicitly blocked.


Fast Cyber Defense Morning Takeaways#

  1. Asynchronous Syscalls Break Synchronous Security: Security architectures that assume system calls execute synchronously in the caller's context fail against io_uring. The kernel workqueue executes actions asynchronously under differing security contexts.
  2. Seccomp Deep Inspection Fails: Traditional Seccomp filters cannot inspect the contents of submission queues. To secure workloads, administrators must block the root entry point: io_uring_setup.
  3. Container Sandboxes Must Disable io_uring by Default: Unless a container workload specifically requires high-performance asynchronous storage I/O, io_uring should be stripped from container seccomp profiles.

Tonight in EDITION 2 (Night, 8:45 PM BST), we will publish the companion blue team guide:

  • Enforcing Seccomp-BPF profiles to disable io_uring_setup across Docker, Podman, and Kubernetes.
  • Restricting io_uring via Linux kernel sysctls (kernel.io_uring_disabled).
  • Deploying eBPF and auditd telemetry to detect unauthorized io_uring initialization attempts.

Authoritative Technical References#

Comments