FlawOpen

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

● CVE-2024-0582 · CVSS 7.8 · Alta
Pesquisa · FlawOpen

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.

💡 Explicação em Linguagem Simples (ELI5)

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 com mmap() 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. Chamar folio_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

Passo 1

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.

Passo 2

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.

Passo 3

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.

Passo 4

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

IMPLEMENTAÇÃO VULNERÁVEL
/* 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;
		}
	}
	/* ... */
}
PATCH SEGURO E ROBUSTO
/* 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

Fontes