
Технический анализ и PoC-эксплойт для CVE-2023-2598 — уязвимости повышения привилегий в ядре Linux в регистрации буферов io_uring, с подробным объяснением Compound Page и внутреннего устройства folio.
Понимание механизмов Compound Page и folio в Linux через CVE-2023-2598 с целью последующего использования 1day CVE-2023-6560
Объём памяти постоянно растёт, но базовый размер страницы в Linux по-прежнему составляет 4 КБ, что уже неэффективно. Поэтому были введены составные страницы для решения этой проблемы. Составная страница — это набор из нескольких страниц, объединённых в одну логическую единицу, при этом две или более физически смежных страницы компонуются вместе, и во многих аспектах их можно рассматривать как одну большую страницу. Они чаще всего используются для создания больших страниц в hugetlbfs или в подсистеме прозрачных больших страниц (transparent huge pages), но также встречаются и в других сценариях. Составные страницы могут использоваться как анонимная память или как буферы в ядре; однако они не могут появляться в page cache, так как page cache работает только с отдельными страницами.
Выделение составной страницы осуществляется вызовом alloc_pages() с установленным флагом __GFP_COMP и порядком страниц более 1 (то есть order >= 1). Это обусловлено механизмом реализации составных страниц.
Примечание: составные страницы обязательно физически непрерывны.
В первой странице (head page) флаг устанавливается как PG_head — маркер того, что это заголовочная страница составной страницы.
Все последующие страницы настраиваются с двумя свойствами: mapping и compound_head. С помощью compound_head определяется, является ли страница хвостовой или заголовочной. Подробнее в функции compound_head().
Во второй странице хранится дополнительная информация о составной странице, поэтому минимальный порядок составной страницы равен 1.
static inline unsigned long _compound_head(const struct page *page)
{
unsigned long head = READ_ONCE(page->compound_head);
if (unlikely(head & 1))
return head - 1;
return (unsigned long)page;
}
Видно, что это поле содержит не только флаг, но и указатель на head page.
Поэтому при получении page легко определить, является ли он составной страницей, и если да, то является ли он head page или tail page. Однако не хватает ключевой информации — размера составной страницы. Если размер неизвестен, то при освобождении составной страницы его необходимо знать. Вся эта информация хранится в поле lru первой хвостовой страницы: размер (order) составной страницы сначала приводится к типу указателя, затем сохраняется в lru.prev, а деструктор — в lru.next.
Зная head page и размер составной страницы, можно корректно освободить эту большую страницу, так как составные страницы физически непрерывны.
Структура показана на рисунке:

folio можно рассматривать как лёгкую обёртку над page, без накладных расходов. folio может быть как отдельной страницей, так и составной страницей.

На рисунке выше — схема структуры page: 64 байта управляют flags, lru, mapping, index, private, {ref_, map_}count, memcg_data и т.д. Когда page является составной страницей, указанные выше flags и другая информация находятся в head page, а tail page повторно использует поля для управления compound_{head, mapcount, order, nr, dtor} и т.д.
struct folio {
/* private: don't document the anon union */
union {
struct {
/* public: */
unsigned long flags;
struct list_head lru;
struct address_space *mapping;
pgoff_t index;
void *private;
atomic_t _mapcount;
atomic_t _refcount;
#ifdef CONFIG_MEMCG
unsigned long memcg_data;
#endif
/* private: the union with struct page is transitional */
};
struct page page;
};
};
В определении структуры folio поля flags, lru и т.д. полностью совпадают с полями page, поэтому они могут быть объединены в union. Таким образом, можно напрямую использовать folio->flags, а не folio->page->flags.
#define page_folio(p) (_Generic((p), \
const struct page *: (const struct folio *)_compound_head(p), \
struct page *: (struct folio *)_compound_head(p)))
#define nth_page(page,n) ((page) + (n))
#define folio_page(folio, n) nth_page(&(folio)->page, n)
С первого взгляда page_folio может показаться запутанным, но на самом деле он эквивалентен:
switch (typeof(p)) {
case const struct page *:
return (const struct folio *)_compound_head(p);
case struct page *:
return (struct folio *)_compound_head(p)));
}
Из макроса page_folio видно, что folio — это, по сути, head page составной страницы. При преобразовании folio в page, folio->page используется для получения head page, а folio_page(folio, n) — для получения tail page.
Зачем нужен folio? В первую очередь, для разработки и эффективности. Если бы folio не было, функция не могла бы определить, является ли current page head page, и вызывала бы _compound_head. Если таких вызовов много на пути выполнения, каждый вызов _compound_head снижает производительность. Но если функция принимает только struct folio *, то этот folio уже указывает на head page, и внутри функции не нужно снова вызывать _compound_head.
Таким образом, основные преимущества:
compound_head.В io_uring в функции io_uring_register_buffer есть следующая логика:

Когда количество страниц, переданных из пользовательского пространства, больше 1, io_uring проверяет, является ли переданный buffer объектом folio. Проверка выполняется с помощью page_folio(): получаем head page для page[i], и если head page для page[i] равен head page для page[0], то считается, что они принадлежат одной составной странице.
В обычном случае эта обработка не вызывает проблем, но существует особый случай: если в пользовательском пространстве с помощью mmap отобразить одну и ту же физическую страницу на непрерывное виртуальное адресное пространство, это также удовлетворяет условию проверки, и тогда выполнение попадает в эту ветку:

В этот момент пользовательское пространство запросило только одну физическую страницу, но итоговый размер равен размеру непрерывной виртуальной памяти, что приводит к тому, что размер может превышать фактическую область физической памяти. В результате возникает переполнение при чтении/записи.
Распыление cred, а затем с помощью интерфейса выхода за границы (overread/overwrite) изменение uid.
В отличие от эксплойта, найденного в сети, этот эксплойт не зависит от адреса, так как изменяется uid. Если уязвимость присутствует, эксплойт можно использовать.
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <liburing.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <mqueue.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <assert.h>