flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation
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.
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 avecmmap()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. Appelerfolio_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
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.
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.
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.
É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é
/* 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;
}
}
/* ... */
}
/* 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
- ✓Utilisez un noyau contenant le commit c392cbecd8ec (Linux 6.6.5, 6.7 ou ultérieur, ou un noyau de distribution dont le changelog cite
CVE-2024-0582). - ✓Là où les charges n'ont pas besoin d'io_uring, restreignez-le : positionnez
kernel.io_uring_disabled=2ou contrôlez-le par charge avec les mécanismes seccomp/SELinux de votre distribution. - ✓Quand vous confiez de la mémoire à l'espace utilisateur via
mmap()/remap_pfn_range(), liez sa durée de vie à->release(), pas à un chemin de démontage antérieur, afin de la libérer une seule fois, une fois le mappage disparu. - ✓Compilez et fuzzez les noyaux avec KASAN et
slub_debugpour qu'un free-pendant-mappage soit détecté comme use-after-free en test. - ✓Pour des charges locales non fiables, préférez des noyaux compilés sans
CONFIG_IO_URING, ou bloquez les appels systèmeio_uring_setup/io_uring_registeravec seccomp.