flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation
CVE-2024-0582: use-after-free do anel de buffers mapeado do io_uring no Linux
CVE-2024-0582 (CVSS 7.8, alta): um anel de buffers registrado com IORING_REGISTER_PBUF_RING e IOU_PBUF_RING_MMAP era liberado durante o desmonte enquanto o espaço de usuário ainda o tinha mapeado com mmap(), deixando uma página que o atacante podia ler e escrever depois de o kernel reutilizá-la, permitindo escalonamento local de privilégios.
Uma biblioteca deixa você registrar uma sala de leitura especial e lhe entrega uma chave para entrar e sair. Ao cancelar o registro da sala, o bibliotecário a devolve ao acervo na hora, mas nunca recolhe sua chave. A biblioteca então aluga exatamente aquela sala para outra pessoa, e você ainda pode entrar com a chave antiga e reescrever o que ela deixou sobre a mesa. A correção é manter cada sala dessas reservada e liberá-la só uma vez, no fechamento, depois que todas as chaves foram devolvidas.
Conceitos Centrais e Termos
io_uring- Interface de E/S assíncrona do Linux. Aplicações e kernel compartilham anéis de submissão e conclusão, evitando uma syscall por operação.
Anel de buffers fornecido (IORING_REGISTER_PBUF_RING)- Um anel de buffers que a aplicação registra para o kernel escolher um por operação. Com
IOU_PBUF_RING_MMAP, o kernel aloca o anel e a aplicação o mapeia commmap()em vez de fornecer sua própria memória. Use-after-free (UAF)- Usar memória depois de liberada. Quando o kernel realoca essa memória para outro objeto, a referência obsoleta pode ler ou corromper esse objeto.
remap_pfn_range / folio_put- O anel é mapeado no espaço de usuário com
remap_pfn_range(), então a contabilidade normal de munmap não o libera. Chamarfolio_put()no desmonte liberava a página enquanto o mapeamento ainda estava ativo.
Mapeamentos de frameworks
- Referência CWE para a fraqueza descrita neste guia: CWE-416
Análise de Causa Raiz
Para um anel de buffers criado com IOU_PBUF_RING_MMAP, io_alloc_pbuf_ring() alocava as páginas e o espaço de usuário as mapeava com mmap() via remap_pfn_range(). Como esse caminho contorna a contabilidade normal de munmap/release, __io_remove_buffers() ao chamar folio_put() liberava as páginas enquanto ainda podia existir um mapeamento de usuário — um use-after-free (CWE-416): o espaço de usuário mantinha um mapeamento válido para uma página que o kernel havia devolvido ao alocador e podia então realocar. Jann Horn relatou a falha. O commit c392cbecd8ec registra cada anel alocado por mmap em uma nova io_buf_list e só o libera em io_kbuf_mmap_list_free() no ->release(), depois que o mapeamento acaba.
Fluxo de Ataque Passo a Paso
Registrar um anel via mmap
Um processo sem privilégios chama io_uring_register() com IORING_REGISTER_PBUF_RING e a flag IOU_PBUF_RING_MMAP, então o kernel aloca o anel de buffers.
Mapeá-lo no espaço de usuário
O processo mapeia o anel com mmap(). O kernel mapeia as páginas com remap_pfn_range(), e o processo mantém o mapeamento.
Liberar o anel ainda mapeado
Cancelar o registro dos buffers (ou desmontar o anel) chega a __io_remove_buffers(), cujo folio_put() libera as páginas mesmo com o mapeamento de usuário ainda ativo.
Escrever pelo mapeamento obsoleto
O kernel realoca a página liberada para outro objeto; o processo, ainda com o mapeamento, o lê e sobrescreve para corromper o estado do kernel e escalar para root.
Código-Fonte: Vulnerável vs. Seguro
/* io_uring/kbuf.c antes da correção (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: o ring foi entregue ao userspace por mmap() e
* remap_pfn_range(). Aqui, folio_put() libera a página enquanto um
* mapeamento ainda pode existir; assim, o userspace continua gravando em uma
* página do kernel liberada e realocada. */
folio_put(virt_to_folio(bl->buf_ring));
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* io_uring/kbuf.c após o 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: registre cada ring que pode usar mmap para liberar suas páginas somente em
* ->release(), depois que o mapeamento desaparecer, nunca durante a desmontagem. */
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 a(s) página(s) em ->release()
* para que nada seja liberado aqui enquanto ainda puder estar mapeado. */
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* Chamado em ->release() ou depois: agora é seguro liberar os 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 Verificação de Segurança para Engenharia
- ✓Use um kernel com o commit c392cbecd8ec (Linux 6.6.5, 6.7 ou posterior, ou um kernel de distribuição cujo changelog cite a
CVE-2024-0582). - ✓Onde as cargas não precisarem de io_uring, restrinja-o: defina
kernel.io_uring_disabled=2ou controle-o por carga com os mecanismos de seccomp/SELinux da sua distribuição. - ✓Quando entregar memória ao espaço de usuário via
mmap()/remap_pfn_range(), ligue o tempo de vida dela ao->release(), não a um caminho de desmonte anterior, para liberá-la uma única vez, depois que o mapeamento acabar. - ✓Compile e faça fuzzing dos kernels com KASAN e
slub_debugpara que um free-enquanto-mapeado seja pego como use-after-free nos testes. - ✓Para cargas locais não confiáveis, prefira kernels compilados sem
CONFIG_IO_URING, ou bloqueie as syscallsio_uring_setup/io_uring_registercom seccomp.