FlawOpen

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

● CVE-2024-0582 · CVSS 7.8 · Haute
Recherche · FlawOpen

CVE-2024-0582 : use-after-free de l'anneau de tampons mappé d'io_uring sous Linux

CVE-2024-0582 (CVSS 7.8, élevée) : un anneau de tampons enregistré avec IORING_REGISTER_PBUF_RING et IOU_PBUF_RING_MMAP était libéré au démontage alors que l'espace utilisateur l'avait encore mappé via mmap(), laissant une page que l'attaquant pouvait lire et écrire après que le noyau l'avait réutilisée, d'où une élévation locale de privilèges.

💡 Explication en Langage Simple (ELI5)

Une bibliothèque vous laisse réserver une salle de lecture spéciale et vous remet une clé pour entrer et sortir. Quand vous annulez la réservation, le bibliothécaire la remet aussitôt dans le pool, mais ne reprend jamais votre clé. La bibliothèque loue alors cette même salle à quelqu'un d'autre, et vous pouvez toujours y entrer avec votre ancienne clé et réécrire ce qu'il a laissé sur le bureau. Le correctif consiste à garder chaque salle de ce type réservée et à ne la libérer qu'une seule fois, à la fermeture, une fois toutes les clés rendues.

Concepts Clés et Termes

io_uring
Interface d'E/S asynchrone de Linux. Les applications et le noyau partagent des anneaux de soumission et de complétion, évitant un appel système par opération.
Anneau de tampons fourni (IORING_REGISTER_PBUF_RING)
Un anneau de tampons que l'application enregistre pour que le noyau en choisisse un par opération. Avec IOU_PBUF_RING_MMAP, le noyau alloue l'anneau et l'application le mappe avec mmap() au lieu de fournir sa propre mémoire.
Use-after-free (UAF)
Utiliser de la mémoire après sa libération. Une fois que le noyau réalloue cette mémoire à un autre objet, la référence obsolète peut le lire ou le corrompre.
remap_pfn_range / folio_put
L'anneau est mappé dans l'espace utilisateur avec remap_pfn_range(), donc la comptabilité munmap habituelle ne le libère pas. Appeler folio_put() au démontage libérait la page alors que le mappage était encore actif.

Correspondances de référentiels

  • Référence CWE pour la faiblesse décrite dans ce guide : CWE-416

Analyse de Cause Racine

Pour un anneau de tampons créé avec IOU_PBUF_RING_MMAP, io_alloc_pbuf_ring() allouait les pages et l'espace utilisateur les mappait avec mmap() via remap_pfn_range(). Comme ce chemin contourne la comptabilité munmap/release habituelle, __io_remove_buffers() en appelant folio_put() libérait les pages alors qu'un mappage utilisateur pouvait encore exister — un use-after-free (CWE-416) : l'espace utilisateur conservait un mappage valide vers une page que le noyau avait rendue à l'allocateur et pouvait réallouer. Jann Horn l'a signalé. Le commit c392cbecd8ec enregistre chaque anneau alloué par mmap dans une nouvelle io_buf_list et ne le libère que dans io_kbuf_mmap_list_free() au ->release(), une fois le mappage disparu.

Déroulement de l'Attaque Étape par Étape

Étape 1

Enregistrer un anneau adossé à mmap

Un processus non privilégié appelle io_uring_register() avec IORING_REGISTER_PBUF_RING et le drapeau IOU_PBUF_RING_MMAP : le noyau alloue l'anneau de tampons.

Étape 2

Le mapper dans l'espace utilisateur

Le processus mappe l'anneau avec mmap(). Le noyau mappe les pages avec remap_pfn_range() et le processus conserve le mappage.

Étape 3

Libérer l'anneau encore mappé

Désenregistrer les tampons (ou démonter l'anneau) atteint __io_remove_buffers(), dont le folio_put() libère les pages bien que le mappage utilisateur soit toujours actif.

Étape 4

Écrire via le mappage obsolète

Le noyau réalloue la page libérée à un autre objet ; le processus, qui garde le mappage, la lit et la réécrit pour corrompre l'état du noyau et passer root.

Code Source : Vulnérable vs Sécurisé

IMPLÉMENTATION VULNÉRABLE
/* io_uring/kbuf.c avant le correctif (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 : le ring a été transmis à l’espace utilisateur via mmap() et
			 * remap_pfn_range(). Ici, folio_put() libère la page alors qu’un
			 * mappage peut encore exister ; l’espace utilisateur continue donc d’écrire dans une
			 * page du noyau libérée puis réallouée. */
			folio_put(virt_to_folio(bl->buf_ring));
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}
PATCH SÉCURISÉ ET ROBUSTE
/* io_uring/kbuf.c après le 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 : mémoriser chaque ring accessible par mmap afin de ne libérer ses pages qu’à
	 * ->release(), une fois le mappage supprimé, jamais pendant le démontage. */
	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() libère la ou les pages à ->release()
			 * pour qu’aucune page ne soit libérée ici tant qu’elle peut encore être mappée. */
			bl->buf_ring = NULL;
			bl->is_mmap = 0;
		}
	}
	/* ... */
}

/* Appelé lors de ->release() ou après : les rings mappés peuvent alors être libérés sans risque. */
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);
	}
}

Liste de Contrôle de Sécurité pour l'Ingénierie

Sources