
Technische Analyse und Proof-of-Concept-Exploit für CVE-2023-2598, eine Linux-Kernel-Privilegienausweitungsschwachstelle in der Pufferregistrierung von io_uring, mit detaillierter Erklärung der Compound-Page- und Folio-Interna.
Durch CVE-2023-2598 die Mechanismen von Compound Page und folio in Linux verstehen, später prüfen, ob eine Ausnutzung des 1day CVE-2023-6560 möglich ist.
Der Speicher wird immer größer, aber die Basisseiten-Zuweisungseinheit von Linux ist immer noch 4K und wird knapp. Daher werden zusammengesetzte Seiten eingeführt, um dieses Problem zu lösen. Zusammengesetzte Seiten sind im Grunde eine Sammlung mehrerer Seiten, bei denen zwei oder mehr physikalisch aufeinanderfolgende Seiten zu einer Einheit zusammengefasst werden, die in vielerlei Hinsicht als eine einzelne, größere Seite betrachtet werden kann. Sie werden am häufigsten verwendet, um große Seiten zu erstellen, die in hugetlbfs oder im Transparent Huge Pages-Subsystem verwendet werden, aber sie treten auch in anderen Szenarien auf. Zusammengesetzte Seiten können als anonymer Speicher oder als Puffer im Kernel verwendet werden; sie können jedoch nicht im Page Cache erscheinen, da der Page Cache nur einzelne Seiten verarbeiten kann.
Das Zuweisen einer zusammengesetzten Seite erfolgt durch Aufruf von alloc_pages() mit gesetztem __GFP_COMP-Allokationsflag und einer Seitenrahmenzahl größer als 1, d.h. Order mindestens 1. Dies liegt am Implementierungsmechanismus zusammengesetzter Seiten.
Beachten Sie: Zusammengesetzte Seiten sind physikalisch aufeinanderfolgend.
Im ersten Page wird das Flag PG_head gesetzt, um dies als Kopfseite der zusammengesetzten Seite zu kennzeichnen;
Alle nachfolgenden Seiten erhalten zwei Attribute: mapping und compound_head, und durch compound_head wird bestätigt, ob es sich um eine Endseite oder eine Kopfseite handelt. Details siehe Funktion compound_head().
In der zweiten Seite werden weitere Informationen zur zusammengesetzten Seite gespeichert. Daher muss die Order der zusammengesetzten Seite mindestens 1 sein.
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;
}
Man sieht, dass dieses Feld sowohl das Flag als auch einen Zeiger auf die Kopfseite enthält.
Wenn man also ein page erhält, kann man leicht feststellen, ob es sich um eine zusammengesetzte Seite handelt, und wenn ja, ob es eine Kopf- oder Endseite ist. Es fehlt jedoch noch eine wichtige Information: die Größe dieser zusammengesetzten Seite. Wenn man die Größe nicht kennt, muss man sie beim Freigeben kennen. Diese Informationen werden alle im lru-Feld der ersten Endseite gespeichert: Die Größe (order) der zusammengesetzten Seite wird zuerst in einen Zeigertyp umgewandelt und dann in lru.prev gespeichert, der Destruktor wird in lru.next gespeichert.
Solange man die Kopfseite und die Größe der zusammengesetzten Seite kennt, kann man die große Seite korrekt freigeben, da zusammengesetzte Seiten physikalisch aufeinanderfolgend sind.
Die Struktur ist im folgenden Diagramm dargestellt.

Ein folio kann als eine Schicht um eine page betrachtet werden, ohne Overhead. Ein folio kann eine einzelne Seite oder eine zusammengesetzte Seite sein.

Das obige Bild ist ein schematisches Diagramm der page-Struktur. 64 Byte verwalten flags, lru, mapping, index, private, {ref_, map_}count, memcg_data usw. Wenn die Seite eine zusammengesetzte Seite ist, werden die oben genannten flags usw. in der Kopfseite gespeichert, die Endseiten verwenden die Felder compound_{head, mapcount, order, nr, dtor} wieder.
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;
};
};
In der Strukturdefinition des folio stimmen flags, lru usw. vollständig mit der page überein, sodass eine Union mit page möglich ist. So kann direkt folio->flags anstelle von folio->page->flags verwendet werden.
#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)
Auf den ersten Blick mag page_folio verwirrend sein, aber es ist äquivalent zu:
switch (typeof(p)) {
case const struct page *:
return (const struct folio *)_compound_head(p);
case struct page *:
return (struct folio *)_compound_head(p)));
}
Durch das Makro page_folio sieht man, dass ein folio im Grunde eine Kopfseite einer zusammengesetzten Seite ist. Bei der Konvertierung von folio zu page wird folio->page verwendet, um die Kopfseite zu erhalten, und folio_page(folio, n) kann verwendet werden, um eine Endseite zu erhalten.
Wozu also das folio? Mehr noch, dies dient der Entwicklung und Effizienz. Ohne folio könnte die Funktion intern nicht feststellen, ob die aktuelle Seite eine Kopfseite ist, also würde sie _compound_head aufrufen. Wenn der Ausführungspfad lang ist und jede Funktion auf dem Pfad _compound_head aufruft, beeinträchtigt dies die Effizienz. Wenn die Funktion jedoch nur einen Parameter vom Typ struct folio * akzeptiert, zeigt dieses folio auf die Kopfseite, sodass die Funktion intern _compound_head nicht mehr aufrufen muss.
Daher hat es hauptsächlich drei Vorteile:
compound_head.In io_uring_register_buffer von io_uring gibt es diese Logik:

Wenn die vom Benutzermodus übergebene Seite größer als 1 ist, prüft io_uring, ob der übergebene buffer ein folio ist. Die Methode verwendet page_folio(), um die Kopfseite von page[i] zu erhalten. Wenn die Kopfseite von page[i] gleich page[0] ist, wird angenommen, dass sie zur selben zusammengesetzten Seitentabelle gehören.
Im Allgemeinen ist diese Behandlung problemlos, aber es gibt einen Sonderfall: Wenn der Benutzermodus mit mmap dieselbe physikalische Seitentabelle auf aufeinanderfolgende virtuelle Adressen abbildet, erfüllt dies ebenfalls die Bedingung, und der Zweig wird betreten:

Zu diesem Zeitpunkt hat der Benutzermodus nur eine physische Seite angefordert, aber die endgültige size ist die Größe des aufeinanderfolgenden virtuellen Adressraums, sodass size größer sein kann als der tatsächlich belegte physische Adressbereich. Dies führt zu einem Überlauf beim Lesen/Schreiben.
Creds sprayen und dann diese Grenzüberschreitungs-Lese/Schreibschnittstelle zum Ändern der uid verwenden.