Linux 6.2 soll Performance trotz Retbleed-Patches zurückbringen

Der Linux-Kernel 6.2 setzt eine neue Retbleed-Strategie um, die Performance-Einbrüche bisheriger Ansätze beseitigt. Auch die Rust-Integration schreitet voran.

In Pocket speichern vorlesen Druckansicht 15 Kommentare lesen

(Bild: heise online)

Lesezeit: 12 Min.
Von
  • Oliver Müller
Inhaltsverzeichnis

Linux 6.2 erblickte in der Nacht von Sonntag auf Montag dieser Woche das Licht der Welt. Die Rust-Integration verlässt die "Hello, world!"-Nische. BPF soll sich langfristig für Modul-Programmierung eigenen. Zudem legt der Kernel bei Sicherheitseinrichtungen nach.

Wie erwartet legte der Kernel eine zusätzliche Testwoche in Form eines achten Release-Kandidaten (rc8) ein. Das hatte aber diesmal keine technischen Gründe, wie das Beheben schwerwiegender, spät entdeckter Fehler. Vielmehr war die Extrarunde des Vorgängers indirekt Ursache für die Verzögerung. Der Auftakt für die Arbeiten an Linux 6.2 schob sich durch das späte Release von 6.1 auf die Weihnachtsfesttage und Neujahr. Um trotz der urlaubsbedingt abwesenden Entwickler ausreichend Tests zu ermöglichen, hatte Torvalds bereits früh und wiederholt betont, dass er einen weiteren Release Candidate 8 einschieben wolle. Nach rc7 hielt er an seiner Aussage fest und die Extratestwoche kam. Unter "normalen" Umständen hätte Torvalds nach rc7 den finalen Linux 6.2 freigegeben.

Linux 5.19 führte Kernel-Patches gegen die Spectre-Variante Retbleed ein. Bereits im Release-Candiate rc7 sorgten diese für Performance-Probleme, sodass Nacharbeiten in Form eines zusätzlichen rc8 und damit eine weitere Testwoche nötig wurde. Die Patches stützten sich auf die Intel-eigenen Hardware-basierte "Indirect Branch Restriction Speculation" (IBRS). IBRS funktioniert zwar, beschert aber auf Intel Skylake CPUs teils drastische Leistungseinbrüche. Davon ist auch Linux seit 5.19 betroffen.

Linux 6.2 steuert nun gegen und bringt einen schnelleren Kniff für Skylake-basierte Systeme. Die Kernel-Boot-Option retbleed=stuff aktiviert eine rein softwarebasierte Lösung. Sie benötigt keine IBRS und "umschifft" diese.

Im Prozessor existiert ein spezieller Stapel, der "Return Stack Buffer" (RSB). Dieser nimmt Rücksprungadressen auf, die das System im Zuge seiner spekulativen Befehlsausführung als mögliche Rücksprungziele festhält. Dieser RSB kann nur 16 Adressen aufnehmen. Soll eine weitere auf den Stapel, geht die älteste Adresse verloren. Das ist dann der Fall, wenn die Aufrufketten länger werden – beim Linux-Kernel durchaus keine Seltenheit.

Beim "Rückarbeiten" eines solchen Stapels, dem ältere Adressen auf diese Weise verloren gingen, kommt es zwangsläufig zu einem Unterlauf (Underflow) des Stapels. Es fehlen Rücksprungadressen. Die Spekulationsmaschinerie der CPU stoppt an dieser Stelle jedoch nicht, sondern weicht auf den "Branch History Buffer" (BHB) aus. In diesem sind vorherige Sprünge protokolliert und dienen als Grundlage für neue Vorhersagen der spekulativen Befehlsausführung.

Dieser BHB kann gezielt durch falsche Rücksprünge "trainiert" werden, um so Schadcode ausführen zu lassen. Dieses "Training" findet im Userspace statt und schlägt auf den privilegierten Code im Kernel durch. Retbleed bringt den RBS zum Unterlauf, und schiebt dann eine falsch trainierte BHB unter.

IBRS entgegnet dem, indem im Userspace erlernte Sprungziele im Kernelspace verworfen werden. Im Kernelspace sorgt das für massiven Aufwand. Der neue Ansatz sorgt dafür, dass das Kernproblem – der Unterlauf der RBS – nicht stattfindet und damit die BHB gar nicht erst herangezogen wird. Eine "Schattenbuchhaltung" führt mittels eines Zählers Protokoll über die Tiefe des Stapels. Zudem wird der Aufrufstapel und auch die RSB mit Rücksprungzielen vorbelegt. Diese führen lediglich zu einem CPU-Befehl (INT3), der die Spekulation beendet, bevor die BHB zum Tragen kommt.

Auch das ist tendenziell im Kernel nicht billig. Diese Buchhaltungsfunktionen müssen entsprechenden Funktionsaufrufen voran- und nachgestellt werden. Auch sie kosten Taktzyklen, lassen aber hoffen, weniger Leistung zu schlucken als IBRS.

Linux 6.1 ebnete als erster Mainline-Kernel den Weg, Module auch in Rust zu programmieren. Mehr als ein "Hello, world!" war damit aber nicht möglich. Der neue Kernel legt hier wie vorgesehen nach und liefert notwendige Basiskonstrukte nach. Als Verbinder der C- und Rust-Welt ist das komplexe Makro #[vtable] wie erwartet eingeflossen. Das Rust-Makro hilft beim Erzeugen von C-structs aus Rust-traits. In einem trait kann Rust definierte, aber nicht implementierte Funktionen problemlos darstellen.

Der C-Code im Kernel nutzt in den betreffenden structs Nullzeiger, um eine Funktion als "nicht implementiert" zu markieren. Um nun aus einer Rust-trait eine C-struct zu erzeugen, muss der betreffende Code erkennen, wo Nullzeiger einzufügen sind. #[vtable] generiert für jede definierte Funktion aus einer trait ein spezielles Konstantenelement (constant member) in einer als trait dargestellten Tabelle. Diejenigen, die implementiert sind, erhalten den Wert true, die anderen false. Anhand dessen kann der Code, der die C-struct generiert, zur Compile-Zeit erkennen, was nicht implementiert ist und die Nullzeiger einfügen. Wichtig ist das beispielsweise für die C-struct file_operations, die eine Liste der von einem Treiber unterstützten Funktionen enthält.

Rust kennt zwei String-Typen str und String. Der erste Typ ist eine Referenz (borrow) auf einen String, wohingegen der zweite den String selbst enthält. Diese beiden bildet der Kernel-Rust-Support auf zwei Rust-Varianten CString und CStr ab, die die gleiche Funktion wie Strings in C übernehmen. Zudem gibt es einen speziellen Rust-Typ BStr, der Byte-Strings darstellt. Damit können mittels des Makros b_str!() Strings zu Byte-Zeichenketten konvertiert werden, die auch Nicht-ASCII-Zeichen enthalten.

Für Vektoren kommen neue Konstruktoren hinzu. Zudem kann Rust alle acht Log-Level des Kernel nutzen, da nun die notwendigen pr_*-Funktionen nachgeliefert wurden. Bislang waren nur zwei Log-Level mit pr_info!() und pr_emerg!() nutzbar. Auch liefert der Kernel alle Error-Codes aus errno-base.h und Rust-trait-Implementierungen für den Error-Typ nach. Neue Makros für Asserts und Debugging runden den neuen Rust-Satz ab.

Eine Übersicht über die Änderungen an der Rust-Integration findet sich im entsprechenden Commit des Entwicklers Miguel Ojeda. Die Rust-Einbindung legt damit deutlich zu und verlässt die Demo-Ecke von "Hello, world!". Bis die ersten praxistauglichen Module in Rust im Kernel erscheinen, wird es jedoch noch etwas dauern. Für den nächsten Kernel 6.3 stehen bereits die nächsten Erweiterungen als Pull-Request bereit, die neben neuen Typen auch Nacharbeiten etwa an alloc und somit an Elementarem umfassen.