flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation
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.
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 conmmap()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 afolio_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
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.
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.
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.
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
/* 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;
}
}
/* ... */
}
/* 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
- ✓Use un kernel con el commit c392cbecd8ec (Linux 6.6.5, 6.7 o posterior, o un kernel de distribución cuyo changelog mencione
CVE-2024-0582). - ✓Donde las cargas no necesiten io_uring, restrínjalo: ponga
kernel.io_uring_disabled=2o contrólelo por carga con los mecanismos de seccomp/SELinux de su distribución. - ✓Cuando entregue memoria al espacio de usuario con
mmap()/remap_pfn_range(), ligue su tiempo de vida a->release(), no a una ruta de desmontaje anterior, para liberarla una sola vez, después de que el mapeo desaparezca. - ✓Compile y haga fuzzing de los kernels con KASAN y
slub_debugpara que un free-mientras-mapeado se detecte como use-after-free en las pruebas. - ✓Para cargas locales no confiables, prefiera kernels compilados sin
CONFIG_IO_URING, o bloquee las syscallsio_uring_setup/io_uring_registercon seccomp.