
Эксплуатация CVE-2023-2598 о io_uring
io_uring — это интерфейс системных вызовов для Linux. На данный момент он поддерживает почти все системные вызовы, не только изначальные read() и write(). Он позволяет приложению инициировать системные вызовы, которые могут выполняться асинхронно.
В основе каждой реализации io_uring лежат два кольцевых буфера — очередь отправки (SQ) и очередь завершения (CQ). Эти кольцевые буферы являются общими для приложения и ядра.
Мы можем получить элемент очереди отправки (SQE), который описывает syscall, который должен быть выполнен с помощью io_uring_get_sqe. Затем приложение выполняет системный вызов io_uring_enter, чтобы сообщить ядру, что в очереди отправки есть работа, ожидающая выполнения.
После того как ядро выполнит операцию, оно помещает элемент очереди завершения (CQE) в кольцевой буфер очереди завершения, который затем может быть использован приложением.
Функция io_sqe_buffer_register реализует отображение виртуальных страниц и физических адресов.
Сначала следует уточнить некоторые понятия.
Приложение инициирует запрос на буфер с помощью io_uring_register. Цепочка вызовов следующая:
io_uring_register_buffers->io_uring_register->io_sqe_buffers_register
Исходный код функции io_sqe_buffers_register:
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;
}
В этой функции мы перейдём к io_sqe_buffer_register. И там мы обнаружим логическую ошибку. Исходный код функции io_sqe_buffer_register:
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;
}
/* Если это огромная страница, попробуйте объединить их в одну запись bvec */
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;
/* сохраняем исходный адрес для последующей проверки */
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;
}
Здесь я упомяну лишь несколько важных моментов.
imu означает виртуальный адрес/страницу.page означает физический адрес/страницу.folio — это много страниц, которые физически непрерывны; предотвращает ситуацию, когда вызывается функция и её параметр содержит страницу, но эта страница принадлежит непрерывному диапазону страниц, и мы не уверены, использовать ли всю страницу или одну страницу.struct iovec — просто структура, описывающая буфер с начальным адресом буфера и его длиной. Ничего больше.io_mapped_ubuf — это структура, содержащая информацию о буфере, зарегистрированном в экземпляре io_uring.struct io_mapped_ubuf {
u64 ubuf; // адрес, с которого начинается буфер
u64 ubuf_end; // адрес, на котором он заканчивается
unsigned int nr_bvecs; // сколько bio_vec требуется для адресации буфера
unsigned long acct_pages;
struct bio_vec bvec[]; // массив bio_vec
};
Член bio_vec — это структура, похожая на iovec, но для физической памяти.
...
/* Если это огромная страница, попробуйте объединить их в одну запись bvec */
if (nr_pages > 1) { // если больше одной страницы
folio = page_folio(pages[0]); // преобразует страницу в folio
// возвращает folio, содержащий эту страницу
for (i = 1; i < nr_pages; i++) {
if (page_folio(pages[i]) != folio) { // разные folio -> не физически непрерывны
folio = NULL; // устанавливаем folio в NULL, так как не можем объединить в одну запись
break;
}
}
if (folio) { // если все страницы находятся в одном folio
folio_put_refs(folio, nr_pages - 1);
nr_pages = 1; // устанавливает nr_pages = 1, так как это можно представить как одну страницу folio
}
}
...
Код, проверяющий, все ли страницы из одного folio, на самом деле не проверяет, являются ли они последовательными. Это может быть одна и та же страница, отображённая несколько раз. Во время итерации page_folio(page) будет снова и снова возвращать один и тот же folio, проходя проверки. Это очевидная логическая ошибка. Продолжим с io_sqe_buffer_register и посмотрим, каковы последствия.
...
imu = kvmalloc(struct_size(imu, bvec, nr_pages), GFP_KERNEL);
// выделяет imu с массивом для nr_pages bio_vec
// bio_vec — непрерывный диапазон физических адресов памяти
// нам нужен bio_vec для каждой (физической) страницы
// в случае folio массив bio_vec будет размером 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; // устанавливает размер равным переданному пользователем!
/* сохраняем исходный адрес для последующей проверки */
imu->ubuf = (unsigned long) iov->iov_base; // контролируется пользователем
imu->ubuf_end = imu->ubuf + iov->iov_len; // вычисляет конец на основе длины
imu->nr_bvecs = nr_pages; // будет 1 в случае folio
*pimu = imu;
ret = 0;
if (folio) { // в случае folio — нужен только один bio_vec (эффективно!)
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;
}
Выделяется один bio_vec, так как nr_pages = 1. Размер буфера, записываемый в pimu->iov_len и pimu->bvec[0].bv_len, равен тому, который передал пользователь в iov->iov_len.