Linux 5.10: Kommende Kernel-Version bricht mit fast 30-jährigem Erbe

Das allmähliche Ausrangieren einer unsicheren Funktion soll den Kernelspace ab Linux 5.10 besser schützen. Wir gehen der Änderung auf den Grund.

In Pocket speichern vorlesen Druckansicht 111 Kommentare lesen

(Bild: Gordon Leggett / Wikimedia Commons / CC BY-SA 4.0)

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

Mit dem ersten Release Candidate von Linux 5.10 kündigte Linus Torvalds Ende Oktober eine historische Änderung an: Der bereits seit 1991 vorhandene set_fs()-Mechanismus soll mit der kommenden Kernel-Version wenn schon nicht auf allen, so doch zumindest auf einigen CPU-Architekturen ausgemustert werden.

Der Aufruf von set_fs() ermöglicht nachfolgenden Userspace-Zugriffsfunktionen typischerweise den Zugriff auf den geschützten Speicherbereich des Kernels – oder nimmt ihnen diese Möglichkeit wieder. Das Erbe aus alten 386er-Tagen sorgte in der Vergangenheit allerdings auch für schwerwiegende Sicherheitsprobleme. In diesem Artikel schauen wir uns die Funktionsweise von set_fs(), die der Funktion innewohnenden Gefahren und die sich aus dem Wegfall ergebenden Konsequenzen ein wenig näher an.

Die Kernelfunktion set_fs() war seit dem Release 0.10, also seit Ende 1991, fester Bestandteil des Linux-Kernels. Linux war damals noch auf den 80386-Prozessor zugeschnitten und nutzte zwangsläufig das, was diese CPU bot.

Das Betriebssystem verwendete das FS-Register des 386ers, um für aufzurufende Funktionen jeweils Kernel- oder Userspace zugänglich zu machen. Logische Speicheradressen setzen sich aus einem Segmentselektor und einem Offset zusammen. Der Segmentselektor im FS-Register zeigt unter normalen Umständen auf den Userspace. Durch das Setzen des FS-Registers durch set_fs() auf den Kernelspace können Userspace-Zugriffsfunktionen wie get_user_byte() auf den Kernelspace zugreifen. Das kommt einem Umdeklarieren gleich: Es lassen sich auf diese Weise Zeiger zwischen Userspace und Kernelspace direkt austauschen.

Gedacht war dieser Mechanismus, um effizient aus dem Kernelspace heraus Funktionen anzusprechen, die sonst auf den Userspace beschränkt sind. Ein Beispiel aus jenen Tagen ist das UMSDOS-Dateisystem. Es bildet ein Unix-Dateisystem auf einem darunterliegenden MS-DOS-Dateisystem ab und greift über Systemfunktionen auf den MS-DOS-Dateisystemtreiber zu. Da beide – UMSDOS- und MS-DOS-Dateisystemtreiber – im Kernelspace liegen, bedient(e) man sich des set_fs()-Tricks, um den Funktionen "vorzugaukeln", aus dem Userspace zu kommen.

Die Namen der Funktionen set_fs(), get_fs() und Co. sind gleichgeblieben – das FS-Register verwendet der Kernel bei ihrem Aufruf heute aber nicht mehr. An seine Stelle trat ab Version 2.2 die plattformunabhängige globale Variable addr_limit, deren Wert die Grenze zwischen User- und Kernelspace anzeigt. Alles unterhalb ist Userspace, alles darüber Kernelspace. Im Vergleich zum FS-Register ermöglicht addr_limit eine wesentlich einfachere Prüfung einer Adresse auf Gültigkeit. Es genügt ein arithmetischer Vergleich mit dem Variablenwert, um festzustellen, ob die Adresse im User- oder im Kernelspace beheimatet ist.

Die Kernelfunktion access_ok() prüft beispielsweise eine Adresse gegen addr_limit, um sie allgemein dem User- oder Kernelspace zuordnen und den Zugriff zu bewerten. Den weiteren Zugriffsschutz überlässt der Kernel mittlerweile der Memory Management Unit (MMU) des Prozessors.

Um die Sicherheitsrelevanz von set_fs() besser zu verstehen, lohnt ein Blick auf eines von hunderten im Kernel-Code vorhandenen Anwendungsszenarien.

Der folgende Code stammt aus der Funktion kernel_readv() aus Linux 5.9.3 (fs/splice.c ab Zeile 352). Verwendet wird kernel_readv() im splice()-Systemcall, der zum performanten Kopieren von Daten zwischen einer Datei und einer Pipe dient. Normalerweise wäre ein "Umweg" über den Userspace notwendig, um aus einem File-Handle zu lesen und in ein anderes hineinzuschreiben. splice() delegiert hingegen den gesamten Kopiervorgang an den Kernel.

01 old_fs = get_fs();
02 set_fs(KERNEL_DS);
03 /* The cast to a user pointer is valid due to the set_fs() */
04 res = vfs_readv(file, (const struct iovec __user *)vec, vlen, &pos, 0);
05 set_fs(old_fs);

In der ersten Zeile sichert die Funktion den aktuellen Inhalt von FS. In der zweiten Zeile setzt sie FS auf den Kernelspace (KERNEL_DS = Kernel-Datensegment). Anschließend ruft sie die "normale" Dateisystemroutine vfs_readv() auf, die normalerweise auf Userspace-Puffer beschränkt wäre. Zu guter Letzt schreibt sie den gesicherten Inhalt von FS über set_fs() wieder zurück: FS zeigt wieder auf den Userspace. Der Kommentar in der dritten Zeile fasst den Vorgang recht gut zusammen.

Für den Zeitraum des Aufrufs von vfs_readv() verschiebt sich die in der Variable addr_limit definierte Userspace-Grenze ein Stück in den Kernelspace. Eben hier liegt das potenzielle Sicherheitsrisiko: vfs_readv() kann Daten dank set_fs() temporär in den Kernelspace-Pipe-Buffer schreiben – ohne Zugriffsverletzung und daraus resultierendem Abbruch.

Die beschriebene Vorgehensweise wurde über Jahrzehnte so praktiziert und blieb als Sicherheitsproblem unbemerkt. 2010 jedoch wurde das Gefahrenpotenzial offensichtlich: Basierend auf der von Nelson Elhage entdeckten Sicherheitslücke CVE-2010-4258 zeigte Dan Rosenberg einen Exploit zur Rechteausweitung (Privilege Escalation) zu root im Kontext der bereits erwähnten Kernelfunktion access_ok().

Das Problem ist, dass zwischen den beiden (atomaren) set_fs()-Aufrufen unvorhergesehene Dinge passieren können. Kommt es hier zu einem schwerwiegenden Fehler in Form eines so genannten Oops, unterbleibt das Zurückschreiben des old_fs-Werts. Teile des Kernelspace-Datensegments bleiben somit für den Zugriff aus dem Userspace offen.

Rosenbergs Exploit zeigt, dass sich ein solcher Fehler bewusst provozieren lässt. Aber auch versehentlich herbeigeführte Probleme mit set_fs() sind bekannt: Ende 2016 wurde bei einem Touchscreen-Treiber von LG unter Android in einem möglichen Ablaufpfad das zurücksichernde set_fs() gar nicht aufgerufen. Die Folge: Der Treiber kehrte in gewissen Situationen mit dem offenen Kernelspace in den Userspace zurück.

Der exzessive Einsatz von set_fs() wurde jedoch trotz der offensichtlichen Sicherheitsprobleme nicht angefasst. Stattdessen wurde 2017 ein einfacher Patch von Thomas Garnier aufgenommen. Dieser Patch prüft bei jedem Systemcall, ob addr_limit mit dem originären Userspace-Segment übereinstimmt. Ist das nicht der Fall, löst der Patch eine Kernel Panic aus. Diese recht "brutale" Vorgehensweise ist umstritten, schottet den Kernel andererseits aber wirkungsvoll gegen Eindringlinge ab. Ein wesentlich weitreichenderer Aspekt bei diesem Patch ist der Performance-Impact. Diese Prüfung bei jedem Systemcall auszuführen, verbrennt ungemein CPU-Zeit – in den meisten Fällen für nichts.

Zumindest brachte besagter Patch wieder Leben in die Diskussion. Schließlich nahm sich Entwickler Christoph Hellwig des Themas an: Selbst im relativ neuen Code von BPF (Berkeley Packet Filter) hatte sich die unsichere set_fs()-Praxis durchgesetzt. Um dem entgegen zu wirken, führte Hellwig mit einem Patch einen neuen sockptr_t ein, der angibt, ob er Zeiger im Kernel- oder Userspace nutzt. Auf der Basis dieses Merkmals kann der Kernel Helper-Funktionen zum Einsatz bringen, die das Kopieren von Daten zwischen Kernel- und Userspace übernehmen oder eben doch set_fs() – aber in kontrollierter Form – zum Einsatz bringen

Damit konnten die set_fs()-Aufrufe zunächst aus BPF (Berkeley Packet Filter)-Programmen entfernt werden, in denen sie den Auslöser für Hellwigs Bemühungen darstellten. Anschließend standen die Helper-Funktionen dann Pate, um die set_fs()-Nutzung auch in anderen Teilen des Systems zu reduzieren.

Auf bestehende Anwendungsfälle sollte der Umstieg auf die Helper-Funktionen keine (negativen) Auswirkungen haben. Lediglich die Implementierung von splice() kann Probleme bereiten: Ist die Datenquelle ein Gerät, das splice_read() nicht unterstützt, funktioniert splice() nicht mehr wie gewohnt. Allerdings scheinen alle betroffenen Kernel-Maintainer bereits Lösungen in der Schublade zu haben, um splice_read() "nachzurüsten".

Der Wegfall von set_fs() sollte daher nach aktuellem Stand keine weitreichenden Inkompatiblitäten erzeugen. Lediglich Hersteller von Treibern, die nicht im Kernel-Tree enthalten sind, sollten ihren Code durchkämmen und sich für ein Leben ohne set_fs() rüsten.

Bereits hinter den Kulissen Linux 5.9 war einiges für den Wechsel vorbereitet worden. Linus Torvalds führt die Bereinigung in x86, PowerPC, S390 und RISC-V als "abgeschlossen" an. Andere Architekturen wie beispielsweise Motorola 68k, MIPS, ARM und Sparc setzen den set_fs()-Mechanismus noch ein. Torvalds spricht in der Ankündigung zu Linux 5.10-rc1 von einem getanen Anfang und hofft, dass die anderen Architekturen ebenfalls von dem "historischen Modell" Abschied nehmen können und werden – auch wenn dies noch einige Zeit in Anspruch nehmen dürfte.

(ovw)