How an asynchronous ring buffer registration race in fs/io_uring.c freed active kernel pages, allowing unprivileged local attackers to overwrite cred structures and achieve root.
Imagine a shared bank safety deposit box where two clerks share a ledger. Clerk A decides to close the box and shred the paperwork, but Clerk B continues writing checks against the box's memory address. The bank immediately rents that exact box to the town mayor. Clerk B then writes their name onto the mayor's documents, walking out with the keys to the entire city.
An unprivileged attacker thread registers a fixed buffer ring via io_uring_register().
The attacker issues IORING_REGISTER_BUFFERS_UPDATE with an out-of-bounds index, triggering a reference count underflow that frees the underlying page table.
The attacker sprays pipe_buffer objects across kernel memory, landing directly in the newly freed slab cache.
The stale io_uring reference writes to the reallocated memory, rewriting the process credentials to uid=0, gid=0.
Fourni en code source de haut niveau lisible (sans assembleur brut ni diff binaire).
// VULNERABLE: Linux kernel fs/io_uring.c before patch
int io_sqe_buffers_update(struct io_ring_ctx *ctx, void __user *arg, unsigned int nr_args) {
struct io_rsrc_data *data = ctx->buf_data;
// ROOT CAUSE: Incomplete reference accounting when updating buffer rings
for (i = 0; i < nr_args; i++) {
// Releases resource node prematurely while still mapped in current task!
io_rsrc_node_switch(ctx, data);
// Destroys node before awaiting concurrent ring completions!
io_queue_rsrc_removal(data, i, ctx->rsrc_node, (void *)tag);
}
return 0;
}
// SECURE: Hardened reference counting in fs/io_uring.c
int io_sqe_buffers_update(struct io_ring_ctx *ctx, void __user *arg, unsigned int nr_args) {
struct io_rsrc_data *data = ctx->buf_data;
// 1. Lock context to guarantee atomic resource state transition
mutex_lock(&ctx->uring_lock);
for (i = 0; i < nr_args; i++) {
// 2. Validate explicit boundary bounds on buffer array
if (unlikely(i >= data->nr)) {
mutex_unlock(&ctx->uring_lock);
return -EINVAL;
}
// 3. Atomically switch node only after verifying zero in-flight references
io_rsrc_node_switch(ctx, data);
io_queue_rsrc_removal(data, i, ctx->rsrc_node, (void *)tag);
}
mutex_unlock(&ctx->uring_lock);
return 0;
}