
The exploitation of CVE-2023-2598 about io_uring
io_uring is a system call interface for Linux. It has supported almost all system call so far, not only read() and write initially. It enables an application to initiate system calls that can be performed asynchronously.
At the core of every io_uring implementation sit two ring buffers - the submission queue(SQ) and the completion queue(CQ). Those ring buffers are shared between application and kernel.
We can get a submission queue entry(SQE) which describing a syscall you want to be performed by io_uring_get_sqe . The application then performs an io_uring_enter syscall to effectively tell the kernel that there is work waiting to be done in the submission queue.
After the kernel performs the operation it puts a Completion Queue Entry (CQE) into the completion queue ring buffer which can then be consumed by the application.
The function io_sqe_buffer_register implements the mapping of virtual pages and physical addresses.
We should clarify some concepts first.
The application initiates a request for a buffer by io_uring_register. The call chain is as follows:
io_uring_register_buffers->io_uring_register->io_sqe_buffers_register
The source code of function io_sqe_buffers_register is as follows:
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;
}
In this function, we will run into io_sqe_buffer_register. And we will find a logical bug. The source code of function io_sqe_buffer_register is as follows:
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;
}
Here I only mention a few important points.
imu means virtual address/page.page means physical address/page.folio means a lot of pages that are continues physically, preventing the situation that when a function is called and its parameter contains a page, but this page belongs to a continuous range of pages, but we are not sure whether to use the whole page or a single page.struct iovec -> just a structure that describes a buffer, with the start address of the buffer and its length. Nothing more.io_mapped_ubuf is a structure that holds the information about a buffer that has been registered to an io_uring instance.struct io_mapped_ubuf {
u64 ubuf; // the address at which the buffer starts
u64 ubuf_end; // the address at which it ends
unsigned int nr_bvecs; // how many bio_vec(s) are needed to address the buffer
unsigned long acct_pages;
struct bio_vec bvec[]; // array of bio_vec(s)
};
The member bio_ver is a struct like iovec but for physical memory.
...
/* If it's a huge page, try to coalesce them into a single bvec entry */
if (nr_pages > 1) { // if more than one page
folio = page_folio(pages[0]); // converts from page to folio
// returns the folio that contains this page
for (i = 1; i < nr_pages; i++) {
if (page_folio(pages[i]) != folio) { // different folios -> not physically contiguous
folio = NULL; // set folio to NULL as we cannot coalesce into a single entry
break;
}
}
if (folio) { // if all the pages are in the same folio
folio_put_refs(folio, nr_pages - 1);
nr_pages = 1; // sets nr_pages to 1 as it can be represented as a single folio page
}
}
...
The code that checks if the pages are from the same folio doesn’t actually check if they are consecutive. It can be the same page mapped multiple times. During the iteration page_folio(page) would return the same folio again and again passing the checks. This is an obvious logic bug. Let’s continue with io_sqe_buffer_register and see what the fallout is.
...
imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL);
// allocates imu with an array for nr_pages bio_vec(s)
// bio_vec - a contiguous range of physical memory addresses
// we need a bio_vec for each (physical) page
// in the case of a folio - the array of bio_vec(s) will be of size 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; // sets the size to that passed by the user!
/* store original address for later verification */
imu->ubuf = (unsigned long) iov->iov_base; // user-controlled
imu->ubuf_end = imu->ubuf + iov->iov_len; // calculates the end based on the length
imu->nr_bvecs = nr_pages; // this would be 1 in the case of folio
*pimu = imu;
ret = 0;
if (folio) { // in case of folio - we need just a single bio_vec (efficiant!)
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;
}
A single bio_vec is allocated as nr_pages = 1. The size of the buffer that is written in pimu->iov_len and pimu->bvec[0].bv_len is the one passed by the user in iov->iov_len.