flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation
CVE-2024-0582: use-after-free отображённого кольца буферов io_uring в Linux
CVE-2024-0582 (CVSS 7.8, высокая): кольцо буферов, зарегистрированное с IORING_REGISTER_PBUF_RING и IOU_PBUF_RING_MMAP, освобождалось при разборе, пока пространство пользователя всё ещё держало его отображённым через mmap(), оставляя страницу, которую атакующий мог читать и писать после того, как ядро её переиспользовало, — локальное повышение привилегий.
Библиотека позволяет забронировать особый читальный зал и выдаёт ключ, чтобы входить и выходить. Когда вы снимаете бронь, библиотекарь сразу возвращает зал в общий фонд, но ключ у вас так и не забирает. Затем библиотека сдаёт этот же зал другому, а вы по старому ключу всё ещё можете войти и переписать то, что он оставил на столе. Исправление — держать каждый такой зал забронированным и освобождать лишь однажды, при закрытии, когда все ключи сданы.
Ключевые понятия и термины
io_uring- Интерфейс асинхронного ввода-вывода Linux. Приложение и ядро делят кольца отправки и завершения, избегая системного вызова на каждую операцию.
Предоставленное кольцо буферов (IORING_REGISTER_PBUF_RING)- Кольцо буферов, которое приложение регистрирует, чтобы ядро выбирало по одному на операцию. С
IOU_PBUF_RING_MMAPядро само выделяет кольцо, а приложение отображает его черезmmap(), не предоставляя своей памяти. Use-after-free (UAF)- Обращение к памяти после её освобождения. Когда ядро переотдаёт эту память другому объекту, устаревшая ссылка может его читать или портить.
remap_pfn_range / folio_put- Кольцо отображается в пространство пользователя через
remap_pfn_range(), поэтому обычный учёт munmap его не освобождает. Вызовfolio_put()при разборе освобождал страницу, пока отображение ещё было живо.
Связи со стандартами
- Ссылка CWE для слабого места, описанного в этом руководстве: CWE-416
Анализ первопричины
Для кольца буферов, созданного с IOU_PBUF_RING_MMAP, io_alloc_pbuf_ring() выделяла страницы, а пространство пользователя отображало их через mmap() посредством remap_pfn_range(). Поскольку этот путь обходит обычный учёт munmap/release, __io_remove_buffers(), вызывая folio_put(), освобождала страницы, когда пользовательское отображение ещё могло существовать, — use-after-free (CWE-416): пространство пользователя сохраняло действительное отображение на страницу, которую ядро вернуло аллокатору и могло переотдать. Об этом сообщил Jann Horn. Коммит c392cbecd8ec заносит каждое выделенное под mmap кольцо в новый io_buf_list и освобождает его лишь в io_kbuf_mmap_list_free() при ->release(), когда отображения уже нет.
Пошаговый сценарий атаки
Зарегистрировать кольцо под mmap
Непривилегированный процесс вызывает io_uring_register() с IORING_REGISTER_PBUF_RING и флагом IOU_PBUF_RING_MMAP, и ядро выделяет кольцо буферов.
Отобразить его в пространство пользователя
Процесс отображает кольцо через mmap(). Ядро отображает страницы с remap_pfn_range(), и процесс сохраняет отображение.
Освободить кольцо, пока оно отображено
Снятие регистрации буферов (или разбор кольца) доходит до __io_remove_buffers(), чей folio_put() освобождает страницы, хотя пользовательское отображение ещё живо.
Запись через устаревшее отображение
Ядро переотдаёт освобождённую страницу другому объекту; процесс, всё ещё держащий отображение, читает и перезаписывает её, портя состояние ядра и повышая привилегии до root.
Исходный код: Уязвимый vs Защищённый вариант
/* io_uring/kbuf.c до исправления (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) {
/* ОШИБКА: ring был передан в userspace через mmap() и
* remap_pfn_range(). Здесь folio_put() освобождает страницу, хотя
* отображение ещё может существовать, поэтому userspace продолжает запись в
* освобождённую и повторно выделенную страницу ядра. */
folio_put(virt_to_folio(bl->buf_ring));
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* io_uring/kbuf.c после 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;
/* ИСПРАВЛЕНИЕ: сохранять каждый ring, доступный через mmap, чтобы освобождать его страницы только при
* ->release(), после удаления отображения, а не во время очистки. */
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() освобождает страницу(ы) при ->release(),
* поэтому здесь ничего не освобождается, пока объект может быть отображён. */
bl->buf_ring = NULL;
bl->is_mmap = 0;
}
}
/* ... */
}
/* Вызывается при ->release() или позже: теперь можно безопасно освободить mmap-rings. */
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);
}
}
Чек-лист по защите системы для инженеров
- ✓Используйте ядро с коммитом c392cbecd8ec (Linux 6.6.5, 6.7 или новее, либо дистрибутивное ядро, в журнале изменений которого указана
CVE-2024-0582). - ✓Там, где нагрузкам io_uring не нужен, ограничьте его: задайте
kernel.io_uring_disabled=2или управляйте им по нагрузке средствами seccomp/SELinux вашего дистрибутива. - ✓Когда память отдаётся в пространство пользователя через
mmap()/remap_pfn_range(), привязывайте её время жизни к->release(), а не к более раннему пути разбора, чтобы освобождать её один раз, когда отображения уже нет. - ✓Собирайте и фаззите ядра с KASAN и
slub_debug, чтобы освобождение-при-отображении ловилось в тестах как use-after-free. - ✓Для недоверенных локальных нагрузок предпочитайте ядра без
CONFIG_IO_URINGили блокируйте системные вызовыio_uring_setup/io_uring_registerчерез seccomp.