FlawOpen

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

● CVE-2024-0582 · CVSS 7.8 · 高危
安全研究 · FlawOpen

CVE-2024-0582:Linux io_uring 已映射缓冲区环的释放后使用

CVE-2024-0582(CVSS 7.8,高危):用 IORING_REGISTER_PBUF_RING 和 IOU_PBUF_RING_MMAP 注册的缓冲区环,在拆除时被释放,而用户空间仍通过 mmap() 保持着映射,于是留下一页在内核重新使用后仍可被攻击者读写,造成本地权限提升。

💡 通俗易懂的原理解析 (ELI5)

图书馆允许你登记一间专用阅览室,并给你一把可以随意进出的钥匙。你注销这间房时,管理员立刻把它放回房源池,却从不收回你的钥匙。图书馆随后把同一间房租给别人,而你仍能用旧钥匙进去,改写他留在桌上的东西。修复办法是:让每一间这样的房都保持占用,只在闭馆时、所有钥匙都归还之后,才释放一次。

核心概念与专有名词

io_uring
Linux 的异步 I/O 接口。应用与内核共享提交环和完成环,从而省去每次操作一次系统调用。
提供的缓冲区环(IORING_REGISTER_PBUF_RING)
应用注册的一圈缓冲区,供内核每次操作挑选一个。带 IOU_PBUF_RING_MMAP 时,由内核分配这个环,应用用 mmap() 映射它,而不是自己提供内存。
释放后使用(UAF)
在内存被释放后仍去访问它。一旦内核把这块内存重新分配给别的对象,过期引用就能读取或破坏它。
remap_pfn_range / folio_put
该环通过 remap_pfn_range() 映射进用户空间,因此普通的 munmap 记账不会释放它。拆除时调用 folio_put() 会在映射仍然存在时释放该页。

框架映射

  • 本指南所述弱点的 CWE 参考: CWE-416

根本原因剖析 (Root Cause)

对于用 IOU_PBUF_RING_MMAP 创建的缓冲区环,io_alloc_pbuf_ring() 分配页面,用户空间再通过 remap_pfn_range() 用 mmap() 映射它们。由于这条路径绕过了普通的 munmap/release 记账,__io_remove_buffers() 调用 folio_put() 会在用户映射可能仍然存在时释放这些页面——这就是释放后使用(CWE-416):用户空间保留着一个仍然有效的映射,指向一页已被内核归还给分配器、可以再分配的内存。此问题由 Jann Horn 报告。提交 c392cbecd8ec 把每个由 mmap 分配的环记录到新的 io_buf_list 中,只有在 ->release() 的 io_kbuf_mmap_list_free() 里、映射消失之后才释放它。

攻击执行流程分解

步骤 1

注册一个由 mmap 支撑的环

无特权进程用 IORING_REGISTER_PBUF_RING 和 IOU_PBUF_RING_MMAP 标志调用 io_uring_register(),于是内核分配这个缓冲区环。

步骤 2

把它映射进用户空间

进程用 mmap() 映射该环。内核用 remap_pfn_range() 映射这些页面,进程保留着这个映射。

步骤 3

在仍被映射时释放该环

注销缓冲区(或拆除该环)会走到 __io_remove_buffers(),其中的 folio_put() 在用户映射仍然存在的情况下就释放了这些页面。

步骤 4

通过过期映射写入

内核把已释放的页面重新分配给别的对象;仍持有映射的进程读取并覆写它,从而破坏内核状态并提升到 root。

源代码对比:漏洞与安全实现

存在漏洞的实现
/* io_uring/kbuf.c 修复前(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) {
			/* 缺陷:ring 通过 mmap() 和
			 * remap_pfn_range() 提供给用户空间。此处的 folio_put() 会释放页面,但
			 * 映射可能仍然存在,因此用户空间会继续写入已释放并重新分配的
			 * 内核页面。 */
			folio_put(virt_to_folio(bl->buf_ring));
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}
加固后的安全修复
/* io_uring/kbuf.c 在 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;

	/* 修复:记录每个可通过 mmap 访问的 ring,仅在
	 * ->release() 时、映射消失后才释放其页面,不能在 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() 会在 ->release() 时释放页面,
			 * 因此只要页面仍可能被映射,此处就不会释放它。 */
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}

/* 在 ->release() 时或之后调用:此时可以安全释放 mmap 的 ring。 */
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);
	}
}

工程与系统安全加固清单

参考来源