FlawOpen

flawopen.com/Teardowns/cve-2024-0582-linux-io-uring-privilege-escalation

● CVE-2024-0582 · CVSS 7.8 · Высокий
Исследования · FlawOpen

CVE-2024-0582: use-after-free отображённого кольца буферов io_uring в Linux

CVE-2024-0582 (CVSS 7.8, высокая): кольцо буферов, зарегистрированное с IORING_REGISTER_PBUF_RING и IOU_PBUF_RING_MMAP, освобождалось при разборе, пока пространство пользователя всё ещё держало его отображённым через mmap(), оставляя страницу, которую атакующий мог читать и писать после того, как ядро её переиспользовало, — локальное повышение привилегий.

💡 Простыми словами (ELI5)

Библиотека позволяет забронировать особый читальный зал и выдаёт ключ, чтобы входить и выходить. Когда вы снимаете бронь, библиотекарь сразу возвращает зал в общий фонд, но ключ у вас так и не забирает. Затем библиотека сдаёт этот же зал другому, а вы по старому ключу всё ещё можете войти и переписать то, что он оставил на столе. Исправление — держать каждый такой зал забронированным и освобождать лишь однажды, при закрытии, когда все ключи сданы.

Ключевые понятия и термины

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(), когда отображения уже нет.

Пошаговый сценарий атаки

Шаг 1

Зарегистрировать кольцо под mmap

Непривилегированный процесс вызывает io_uring_register() с IORING_REGISTER_PBUF_RING и флагом IOU_PBUF_RING_MMAP, и ядро выделяет кольцо буферов.

Шаг 2

Отобразить его в пространство пользователя

Процесс отображает кольцо через mmap(). Ядро отображает страницы с remap_pfn_range(), и процесс сохраняет отображение.

Шаг 3

Освободить кольцо, пока оно отображено

Снятие регистрации буферов (или разбор кольца) доходит до __io_remove_buffers(), чей folio_put() освобождает страницы, хотя пользовательское отображение ещё живо.

Шаг 4

Запись через устаревшее отображение

Ядро переотдаёт освобождённую страницу другому объекту; процесс, всё ещё держащий отображение, читает и перезаписывает её, портя состояние ядра и повышая привилегии до 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);
	}
}

Чек-лист по защите системы для инженеров

Источники