FlawOpen

flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation

● CVE-2024-0582 · CVSS 7.8 · High
FlawOpen Security Research

CVE-2024-0582: Linux io_uring Mapped Buffer Ring Use-After-Free

CVE-2024-0582 (CVSS 7.8, High): a provided buffer ring registered with IORING_REGISTER_PBUF_RING and IOU_PBUF_RING_MMAP was freed during teardown while user space still had it mmap()ed, leaving a page the attacker could read and write after the kernel reused it, for local privilege escalation.

💡 Plain English Explainer (ELI5)

A library lets you register a special reading room and hands you a key so you can come and go. When you deregister the room, the librarian gives it back to the pool right away, but never takes back your key. The library then rents that exact room to someone else, and you can still walk in with your old key and rewrite whatever they left on the desk. The fix is to keep each such room reserved and only release it once, at closing time, after every key has been handed back.

Core Concepts & Subsystem Terms

io_uring
Linux's asynchronous I/O interface. Applications and the kernel share submission and completion rings, avoiding a syscall per operation.
Provided buffer ring (IORING_REGISTER_PBUF_RING)
A ring of buffers the application registers so the kernel can pick one per operation. With IOU_PBUF_RING_MMAP the kernel allocates the ring and the application mmap()s it instead of supplying its own memory.
Use-after-free (UAF)
Using memory after it has been freed. Once the kernel reallocates that memory for another object, the stale reference can read or corrupt it.
remap_pfn_range / folio_put
The ring is mapped into user space with remap_pfn_range(), so normal munmap accounting does not free it. Calling folio_put() at teardown dropped the page while the mapping was still live.

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-416

Root Cause Analysis

For a provided buffer ring created with IOU_PBUF_RING_MMAP, io_alloc_pbuf_ring() allocated the pages and user space mapped them with mmap() via remap_pfn_range(). Because that path bypasses the normal munmap/release accounting, __io_remove_buffers() calling folio_put() freed the pages while a user mapping could still exist, a use-after-free (CWE-416): user space kept a valid mapping to a page the kernel had returned to the allocator and could then reallocate. Jann Horn reported it. Commit c392cbecd8ec records each mmap-allocated ring on a new io_buf_list and frees it only in io_kbuf_mmap_list_free() at ->release(), after the mapping is gone.

Step-by-Step Attack Flow

Step 1

Register an mmap-backed ring

An unprivileged process calls io_uring_register() with IORING_REGISTER_PBUF_RING and the IOU_PBUF_RING_MMAP flag, so the kernel allocates the buffer ring.

Step 2

Map it into user space

The process mmap()s the ring. The kernel maps the pages with remap_pfn_range(), and the process keeps the mapping.

Step 3

Free the ring while still mapped

Unregistering the buffers (or tearing the ring down) reaches __io_remove_buffers(), whose folio_put() frees the pages even though the user mapping is still live.

Step 4

Write through the stale mapping

The kernel reallocates the freed page for another object; the process, still holding the mapping, reads and overwrites it to corrupt kernel state and escalate to root.

Source Code: Flaw vs. Secure Implementation

UNPATCHED FLAW
/* io_uring/kbuf.c before the fix (Linux 6.6.4) */
static int io_alloc_pbuf_ring(struct io_uring_buf_reg *reg,
			      struct io_buffer_list *bl)
{
	gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_ZERO | __GFP_NOWARN | __GFP_COMP;
	size_t ring_size;
	void *ptr;

	ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
	ptr = (void *) __get_free_pages(gfp, get_order(ring_size));
	if (!ptr)
		return -ENOMEM;

	bl->buf_ring = ptr;
	bl->is_mapped = 1;
	bl->is_mmap = 1;
	return 0;
}

static int __io_remove_buffers(struct io_ring_ctx *ctx,
			       struct io_buffer_list *bl, unsigned nbufs)
{
	if (bl->is_mapped) {
		i = bl->buf_ring->tail - bl->head;
		if (bl->is_mmap) {
			/* BUG: the ring was handed to user space via mmap() and
			 * remap_pfn_range(). folio_put() here drops the page while a
			 * mapping may still exist, so user space keeps writing to a
			 * freed, reallocated kernel page. */
			folio_put(virt_to_folio(bl->buf_ring));
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}
HARDENED SECURE PATCH
/* io_uring/kbuf.c after commit c392cbecd8ec (Linux 6.6.5 / 6.7) */
struct io_buf_free {
	struct hlist_node		list;
	void				*mem;
};

static int io_alloc_pbuf_ring(struct io_ring_ctx *ctx,
			      struct io_uring_buf_reg *reg,
			      struct io_buffer_list *bl)
{
	struct io_buf_free *ibf;
	size_t ring_size;
	void *ptr;

	ring_size = reg->ring_entries * sizeof(struct io_uring_buf_ring);
	ptr = io_mem_alloc(ring_size);
	if (!ptr)
		return -ENOMEM;

	/* FIX: remember every mmap-able ring so its pages are released only at
	 * ->release() time, after the mapping is gone, never during teardown. */
	ibf = kmalloc(sizeof(*ibf), GFP_KERNEL_ACCOUNT);
	if (!ibf) {
		io_mem_free(ptr);
		return -ENOMEM;
	}
	ibf->mem = ptr;
	hlist_add_head(&ibf->list, &ctx->io_buf_list);

	bl->buf_ring = ptr;
	bl->is_mapped = 1;
	bl->is_mmap = 1;
	return 0;
}

static int __io_remove_buffers(struct io_ring_ctx *ctx,
			       struct io_buffer_list *bl, unsigned nbufs)
{
	if (bl->is_mapped) {
		i = bl->buf_ring->tail - bl->head;
		if (bl->is_mmap) {
			/* io_kbuf_mmap_list_free() frees the page(s) at ->release()
			 * time, so nothing is freed here while it may still be mapped. */
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}

/* Called at or after ->release(): safe to free the mmap'ed rings now. */
void io_kbuf_mmap_list_free(struct io_ring_ctx *ctx)
{
	struct io_buf_free *ibf;
	struct hlist_node *tmp;

	hlist_for_each_entry_safe(ibf, tmp, &ctx->io_buf_list, list) {
		hlist_del(&ibf->list);
		io_mem_free(ibf->mem);
		kfree(ibf);
	}
}

Engineering & System Hardening Checklist

Sources