Eine kürzlich entdeckte Sicherheitslücke im Dateisystem Ext 4 des Linux-Kernels hat zur Folge, dass ein lokaler Angreifer das System zum Absturz bringen kann.
Ext 4 ist seit Kernel 2.6.19 Bestandteil von Linux, wobei es bis 2.6.27 als experimentell eingestuft wurde. Hauptmerkmal von Ext 4 gegenüber Ext 3 ist die maximal zulässige Dateisystemgröße. Diese beläuft sich bei Ext3 auf 16 Tebibyte und erhöht sich in Ext4 auf ein gewaltiges Exbibyte. Die Beschränkung von Ext 3 rührt unter anderem daher, dass dort die einzelnen 4 KiB-Datenblöcke mit nur 32 Bit langen Blocknummern adressiert werden, womit sich automatisch ein Limit von 16 TiB ergibt. Im Gegensatz dazu verwendet Ext 4 48 Bit für Blocknummern und ersetzt das in Ext 3 gebräuchliche indirekte Blockadressierungsschema durch einfacher zu verwaltende Bereiche von Datenblöcken, die durch Anfangsblock und Blockzahl spezifiziert sind. Diese Bereiche werden Extents genannt und sind im Kernel-Quelltext durch eine eigene Struktur repräsentiert:
struct ext4_extent { __le32 ee_block; /* first logical block extent covers */ __le16 ee_len; /* number of blocks covered by extent */ __le16 ee_start_hi; /* high 16 bits of physical block */ __le32 ee_start_lo; /* low 32 bits of physical block */
};
Das Dateisystem Ext 4
Ext 4 verwendet ebenso wie Ext 3 eine Blockgröße von 4 KiB, aber das Abschaffen der indirekten Adressierung und das Einführen von Extents umgehen die 16-TiB-Grenze von Ext 3. Neben der Einfachheit und dem größeren Speichervolumen haben Extents weitere zahlreiche Vorteile gegenüber der von Ext 3 verwendeten indirekten Blockadressierung. Das indirekte Blockadressierung-Verfahren speichert in einem Datei-Inode die Nummern von maximal zwölf Datenblöcken, die die Daten der zu diesem Inode gehörenden Datei halten. Das entspricht zunächst einmal einer maximalen Dateigröße von 48 KiB. Für größere Dateien schreibt Ext 3 die zusätzlich benötigten Blocknummern einfach in die durch den Inode adressierten Blöcke selbst, adressiert diese also indirekt. Dieses indirekte Schema setzt sich rekursiv fort, bis die maximale Dateigröße von Ext 3 erreicht ist.
Die Ext-4-Extents funktionieren völlig anders, da hier nur Anfang und Länge eines Datenbereichs eingetragen werden und damit kein umständliches Adressieren mehr nötig ist. Wie die Struktur “ext4_extent” zeigt, werden 16 Bit für die Größe eines Extents reserviert, wovon jedoch ein Bit verwendet wird, um anzuzeigen, ob der Extent bereits mit Daten gefüllt ist. Es bleiben also noch 128 MiB übrig. Das verbleibende Füll-Bit erlaubt es Userspace-Anwendungen, Speicherplatz im Dateisystem zu reservieren (so genannte Persistent Preallocation), was in vielen Situationen die Fragmentierung von größeren Dateien deutlich reduzieren kann. Ext 4 verwendet 60 Byte im Inode, um dort genau vier Extents und einen 12 Byte langen Header unterzubringen. Damit lassen sich also 512 MiB direkt über den Inode verwalten, im Gegensatz zu den 48 KiB bei Ext3. Für größere Dateien, die mehr als vier Extents benötigen, werden die zusätzlichen Extents von Ext 4 in einer baumartigen Struktur organisiert, wobei sogenannte Extent-Indizes zum effektiven Zugriff auf die Bäume verwendet werden.
Die zusätzliche Funktionalität von Ext4 schlägt sich auch in einigen neuen Glibc-Funktionen wie “fallocate()” nieder. Diese erledigt beispielsweise Persistent Preallocations, reserviert also Speicherplatz für eine größere Datei, die langsam anwächst. Für Allokationen, Mappings und solche Preallokationen ist die Kernelfunktion “ext4_ext_map_blocks()” in “fs/ext4/extents.c” zuständig. Handelt es sich um eine Preallokation, so ruft sie die Funktion Funktion “ext4_ext_convert_to_initialized()” auf, sobald auf den reservierten Speicherbereich geschrieben wird, dieser also initialisiert werden muss. Das oben diskutierte Füll-Bit zeigt dabei immer genau an, ob der Extent initialisiert ist oder nicht. Unter Umständen kann es bei dem Initialisieren via “ext4_ext_convert_to_initialized()” vorkommen, dass sich der bis dahin noch initialisierte Extent in bis zu maximal drei weitere Extents aufspaltet, wobei grundsätzlich nur drei Fälle auftreten können: Falls kein Split benötigt wird, wird der gesamte Extent initialisiert. Erfolgt der Schreibzugriff am Anfang oder Ende des Extent-Bereichs, so wird der gesamte Extent in zwei kleinere Extents geteilt. Werden jedoch Daten in der Mitte des Extents abgelegt, so wird dieser in drei Teile zerlegt. Dabei kommt auch die Funktion “ext4_ext_insert_extent()” zum Einsatz, die die dabei entstehenden neuen Extents entweder in einen schon bestehenden einfügt, oder aber den Extent als neuen Eintrag in den Extent-Baum integriert.
Der Fehler
In die Details dieser Implementierung hat sich ein Fehler eingeschlichen, der dazu führt, dass ein lokaler Angreifer den Kernel zum Absturz bringen kann. Eine Attacke ist durch geschickten Einsatz der “fallocate()-Funktion” möglich, wobei das Problem beim Aufspalten eines Extents in zwei Extents auftritt. Der Fehler wurde von Zheng Liu am 28. September gemeldet und macht sich beim Ausführen des folgenden Skripts bemerkbar:
mkfs.ext4 -b 4096 /dev/sdb1 1000000 mount -t ext4 /dev/sdb1 /mnt/sdb1 fallocate -l 100M /mnt/sdb1/test sync for((i=0;i<170;i++)) do dd if=/dev/zero of=/mnt/sdb1/test conv=notrunc bs=256k count=1 seek=`expr $i \* 2` done umount /mnt/sdb1 mount -t ext4 /dev/sdb1 /mnt/sdb1 dd if=/dev/zero of=/mnt/sdb1/test conv=notrunc bs=256k count=1 seek=341 umount /mnt/sdb1 mount /dev/sdb1 /mnt/sdb1 dd if=/dev/zero of=/mnt/sdb1/test conv=notrunc bs=256k count=1 seek=340 sync
Dieses löst auf einem betroffenen System einen “BUG_ON()”-Aufruf aus, was sich für den Anwender in einer unliebsamen Kernel Panic äußert. Ursache hierfür ist ein Programmierfehler in der Funktion “ext4_ext_convert_to_initialized()”. Diese adressiert verschiedene Extents im Speicher durch struct ext4_extent-Zeiger:
struct ext4_extent *ex, newex, orig_ex;
Beim Spalten des Extents in zwei kleinere im obigen Skript werden die an diesen Adressen befindlichen Zeiger im Speicher zwar korrekt aktualisiert, aber es wird vergessen diese Änderung konsistent auch im eigentlichen Disk Layout durchzuführen. Genauer gesagt haben die Entwickler versäumt, die Funktion “ext4_ext_dirty()” nach dem Split aufzurufen, die dafür zuständig ist, den entsprechenden Inode als “dirty” zu markieren. Dirtieness zeigt dabei an, dass dieser Inode verändert wurde. Durch das Vergessen dieses Aufrufs schleicht sich eine Inkonsistenz in das Dateisystem ein, die dann später von Ext 4 detektiert und mit “BUG_ON()” quittiert wird.
Die Schwachstelle ist in Kernel 3.0 behoben, frühere Versionen, etwa Distributionskernel, können betroffen sein.

