
L'exploitation de CVE-2023-2598 concernant io_uring
io_uring est une interface d'appels système pour Linux. Il prend en charge presque tous les appels système jusqu'à présent, pas seulement read() et write() initialement. Il permet à une application de lancer des appels système qui peuvent être exécutés de manière asynchrone.
Au cœur de chaque implémentation d'io_uring se trouvent deux tampons en anneau : la file d'attente de soumission (SQ) et la file d'attente de complétion (CQ). Ces tampons en anneau sont partagés entre l'application et le noyau.
Nous pouvons obtenir une entrée de file d'attente de soumission (SQE) décrivant un syscall que nous voulons que le noyau exécute via io_uring_get_sqe. L'application effectue ensuite un appel système io_uring_enter pour informer le noyau qu'il y a du travail en attente dans la file d'attente de soumission.
Une fois que le noyau a effectué l'opération, il place une Entrée de file d'attente de complétion (CQE) dans le tampon en anneau de la file d'attente de complétion, qui peut ensuite être consommée par l'application.
La fonction io_sqe_buffer_register implémente le mappage des pages virtuelles et des adresses physiques.
Nous devons d'abord clarifier quelques concepts.
L'application initie une demande de tampon via io_uring_register. La chaîne d'appel est la suivante :
io_uring_register_buffers -> io_uring_register -> io_sqe_buffers_register
Le code source de la fonction io_sqe_buffers_register est le suivant :
int io_sqe_buffers_register(struct io_ring_ctx *ctx, void __user *arg,
unsigned int nr_args, u64 __user *tags)
{
struct page *last_hpage = NULL;
struct io_rsrc_data *data;
int i, ret;
struct iovec iov;
BUILD_BUG_ON(IORING_MAX_REG_BUFFERS >= (1u << 16));
if (ctx->user_bufs)
return -EBUSY;
if (!nr_args || nr_args > IORING_MAX_REG_BUFFERS)
return -EINVAL;
ret = io_rsrc_node_switch_start(ctx);
if (ret)
return ret;
ret = io_rsrc_data_alloc(ctx, io_rsrc_buf_put, tags, nr_args, &data);
if (ret)
return ret;
ret = io_buffers_map_alloc(ctx, nr_args);
if (ret) {
io_rsrc_data_free(data);
return ret;
}
for (i = 0; i < nr_args; i++, ctx->nr_user_bufs++) {
if (arg) {
ret = io_copy_iov(ctx, &iov, arg, i);
if (ret)
break;
ret = io_buffer_validate(&iov);
if (ret)
break;
} else {
memset(&iov, 0, sizeof(iov));
}
if (!iov.iov_base && *io_get_tag_slot(data, i)) {
ret = -EINVAL;
break;
}
ret = io_sqe_buffer_register(ctx, &iov, &ctx->user_bufs[i],
&last_hpage);
if (ret)
break;
}
WARN_ON_ONCE(ctx->buf_data);
ctx->buf_data = data;
if (ret)
__io_sqe_buffers_unregister(ctx);
else
io_rsrc_node_switch(ctx, NULL);
return ret;
}
Dans cette fonction, nous allons entrer dans io_sqe_buffer_register. Et nous allons trouver un bogue logique. Le code source de la fonction io_sqe_buffer_register est le suivant :
static int io_sqe_buffer_register(struct io_ring_ctx *ctx, struct iovec *iov,
struct io_mapped_ubuf **pimu,
struct page **last_hpage)
{
struct io_mapped_ubuf *imu = NULL;
struct page **pages = NULL;
unsigned long off;
size_t size;
int ret, nr_pages, i;
struct folio *folio = NULL;
*pimu = ctx->dummy_ubuf;
if (!iov->iov_base)
return 0;
ret = -ENOMEM;
pages = io_pin_pages((unsigned long) iov->iov_base, iov->iov_len,
&nr_pages);
if (IS_ERR(pages)) {
ret = PTR_ERR(pages);
pages = NULL;
goto done;
}
/* If it's a huge page, try to coalesce them into a single bvec entry */
if (nr_pages > 1) {
folio = page_folio(pages[0]);
for (i = 1; i < nr_pages; i++) {
if (page_folio(pages[i]) != folio) {
folio = NULL;
break;
}
}
if (folio) {
folio_put_refs(folio, nr_pages - 1);
nr_pages = 1;
}
}
imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL);
if (!imu)
goto done;
ret = io_buffer_account_pin(ctx, pages, nr_pages, imu, last_hpage);
if (ret) {
unpin_user_pages(pages, nr_pages);
goto done;
}
off = (unsigned long) iov->iov_base & ~PAGE_MASK;
size = iov->iov_len;
/* store original address for later verification */
imu->ubuf = (unsigned long) iov->iov_base;
imu->ubuf_end = imu->ubuf + iov->iov_len;
imu->nr_bvecs = nr_pages;
*pimu = imu;
ret = 0;
if (folio) {
bvec_set_page(&imu->bvec[0], pages[0], size, off);
goto done;
}
for (i = 0; i < nr_pages; i++) {
size_t vec_len;
vec_len = min_t(size_t, size, PAGE_SIZE - off);
bvec_set_page(&imu->bvec[i], pages[i], vec_len, off);
off = 0;
size -= vec_len;
}
done:
if (ret)
kvfree(imu);
kvfree(pages);
return ret;
}
Ici, je ne mentionne que quelques points importants.
imu désigne une adresse/page virtuelle.page désigne une adresse/page physique.folio désigne un groupe de pages contiguës physiquement, évitant la situation où, lorsqu'une fonction est appelée et que son paramètre contient une page, mais que cette page appartient à une plage continue de pages, on ne sait pas si on doit utiliser la page entière ou une seule page.struct iovec -> juste une structure qui décrit un tampon, avec l'adresse de début du tampon et sa longueur. Rien de plus.io_mapped_ubuf est une structure qui contient les informations relatives à un tampon qui a été enregistré dans une instance io_uring.struct io_mapped_ubuf {
u64 ubuf; // l'adresse à laquelle le tampon commence
u64 ubuf_end; // l'adresse à laquelle il se termine
unsigned int nr_bvecs; // combien de bio_vec(s) sont nécessaires pour adresser le tampon
unsigned long acct_pages;
struct bio_vec bvec[]; // tableau de bio_vec(s)
};
Le membre bio_vec est une struct similaire à iovec mais pour la mémoire physique.
...
/* If it's a huge page, try to coalesce them into a single bvec entry */
if (nr_pages > 1) { // si plus d'une page
folio = page_folio(pages[0]); // convertit une page en folio
// renvoie le folio qui contient cette page
for (i = 1; i < nr_pages; i++) {
if (page_folio(pages[i]) != folio) { // folio différent -> pas contigu physiquement
folio = NULL; // définit folio à NULL car on ne peut pas fusionner en une seule entrée
break;
}
}
if (folio) { // si toutes les pages sont dans le même folio
folio_put_refs(folio, nr_pages - 1);
nr_pages = 1; // définit nr_pages à 1 car on peut le représenter comme une seule page de folio
}
}
...
Le code qui vérifie si les pages proviennent du même folio ne vérifie pas réellement si elles sont consécutives. Il peut s'agir de la même page mappée plusieurs fois. Pendant l'itération, page_folio(page) renverrait le même folio encore et encore, passant ainsi les vérifications. C'est un bogue logique évident. Continuons avec io_sqe_buffer_register et voyons quelles sont les conséquences.
...
imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL);
// alloue imu avec un tableau pour nr_pages bio_vec(s)
// bio_vec - une plage contiguë d'adresses mémoire physiques
// nous avons besoin d'un bio_vec pour chaque page (physique)
// dans le cas d'un folio - le tableau de bio_vec(s) aura une taille de 1
if (!imu)
goto done;
ret = io_buffer_account_pin(ctx, pages, nr_pages, imu, last_hpage);
if (ret) {
unpin_user_pages(pages, nr_pages);
goto done;
}
off = (unsigned long) iov->iov_base & ~PAGE_MASK;
size = iov->iov_len; // définit la taille à celle passée par l'utilisateur !
/* store original address for later verification */
imu->ubuf = (unsigned long) iov->iov_base; // contrôlé par l'utilisateur
imu->ubuf_end = imu->ubuf + iov->iov_len; // calcule la fin basée sur la longueur
imu->nr_bvecs = nr_pages; // ce serait 1 dans le cas d'un folio
*pimu = imu;
ret = 0;