FlawOpen

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

● CVE-2024-0582 · CVSS 7.8 · Alta
Investigación · FlawOpen

CVE-2024-0582: use-after-free del anillo de búferes mapeado de io_uring en Linux

CVE-2024-0582 (CVSS 7.8, alta): un anillo de búferes registrado con IORING_REGISTER_PBUF_RING y IOU_PBUF_RING_MMAP se liberaba durante el desmontaje mientras el espacio de usuario aún lo tenía mapeado con mmap(), dejando una página que el atacante podía leer y escribir después de que el kernel la reutilizara, lo que permitía escalada local de privilegios.

💡 Explicación en Lenguaje Sencillo (ELI5)

Una biblioteca te deja registrar una sala de lectura especial y te da una llave para entrar y salir. Al cancelar el registro de la sala, el bibliotecario la devuelve al fondo de inmediato, pero nunca te recoge la llave. Luego la biblioteca alquila esa misma sala a otra persona, y tú aún puedes entrar con tu vieja llave y reescribir lo que dejó sobre la mesa. La solución es mantener reservada cada sala así y liberarla una sola vez, al cierre, después de que se hayan devuelto todas las llaves.

Conceptos Clave y Términos

io_uring
Interfaz de E/S asíncrona de Linux. Las aplicaciones y el kernel comparten anillos de envío y finalización, evitando una syscall por operación.
Anillo de búferes provisto (IORING_REGISTER_PBUF_RING)
Un anillo de búferes que la aplicación registra para que el kernel elija uno por operación. Con IOU_PBUF_RING_MMAP, el kernel asigna el anillo y la aplicación lo mapea con mmap() en vez de aportar su propia memoria.
Use-after-free (UAF)
Usar memoria después de liberarla. Cuando el kernel reasigna esa memoria a otro objeto, la referencia obsoleta puede leerlo o corromperlo.
remap_pfn_range / folio_put
El anillo se mapea al espacio de usuario con remap_pfn_range(), así que la contabilidad normal de munmap no lo libera. Llamar a folio_put() en el desmontaje liberaba la página mientras el mapeo seguía activo.

Mapeos de marcos

  • Referencia CWE para la debilidad descrita en esta guía: CWE-416

Análisis de Causa Raíz

Para un anillo de búferes creado con IOU_PBUF_RING_MMAP, io_alloc_pbuf_ring() asignaba las páginas y el espacio de usuario las mapeaba con mmap() mediante remap_pfn_range(). Como esa ruta se salta la contabilidad normal de munmap/release, __io_remove_buffers() al llamar a folio_put() liberaba las páginas mientras aún podía existir un mapeo de usuario: un use-after-free (CWE-416), porque el espacio de usuario conservaba un mapeo válido a una página que el kernel había devuelto al asignador y podía reasignar. Jann Horn lo reportó. El commit c392cbecd8ec registra cada anillo asignado por mmap en una nueva io_buf_list y solo lo libera en io_kbuf_mmap_list_free() en ->release(), después de que el mapeo desaparece.

Flujo de Ataque Paso a Paso

Paso 1

Registrar un anillo respaldado por mmap

Un proceso sin privilegios llama a io_uring_register() con IORING_REGISTER_PBUF_RING y la flag IOU_PBUF_RING_MMAP, así que el kernel asigna el anillo de búferes.

Paso 2

Mapearlo al espacio de usuario

El proceso mapea el anillo con mmap(). El kernel mapea las páginas con remap_pfn_range() y el proceso conserva el mapeo.

Paso 3

Liberar el anillo aún mapeado

Cancelar el registro de los búferes (o desmontar el anillo) llega a __io_remove_buffers(), cuyo folio_put() libera las páginas aunque el mapeo de usuario siga activo.

Paso 4

Escribir por el mapeo obsoleto

El kernel reasigna la página liberada a otro objeto; el proceso, que aún tiene el mapeo, lo lee y lo sobrescribe para corromper el estado del kernel y escalar a root.

Código Fuente: Vulnerable vs. Seguro

IMPLEMENTACIÓN VULNERABLE
/* io_uring/kbuf.c antes de la corrección (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: el ring se entregó al espacio de usuario mediante mmap() y
			 * remap_pfn_range(). Aquí, folio_put() libera la página aunque todavía pueda existir
			 * un mapeo, por lo que el espacio de usuario sigue escribiendo en una
			 * página del kernel liberada y reasignada. */
			folio_put(virt_to_folio(bl->buf_ring));
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}
PARCHE SEGURO Y ROBUSTO
/* io_uring/kbuf.c después del 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: registrar cada ring que admite mmap para liberar sus páginas únicamente en
	 * ->release(), cuando el mapeo ya haya desaparecido, nunca durante el desmontaje. */
	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() libera la(s) página(s) en ->release()
			 * para no liberar nada aquí mientras aún pueda estar mapeado. */
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}

/* Se llama en ->release() o después: ya es seguro liberar los rings mapeados. */
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);
	}
}

Lista de Verificación de Seguridad para Ingeniería

Fuentes