Linux-Systeme gelten als stabil. Um eins abstürzen zu lassen, muss sich ein Angreifer schon mit handoptimiertem Maschinencode [1] auskennen und dazu möglichst Rootrechte besitzen. Das zumindest denken viele Admins. In Wirklichkeit bringt ein Bash-Skript von 6 Byte so maches System in die Knie, wenn sein Verwalter nicht präventive Maßnahmen trifft (siehe Listing 1). Daher sollte niemand, der sich nicht den berechtigten Zorn der Kollegen zuziehen will, der wichtige ungespeicherte Dokumente geöffnet hat oder der mit dem Rechner die Brennstäbe eines Kernkraftwerks steuert, die Kommandosequenz abtippen.
01 $> PATH=.
02 $> echo 'a & a' > a
03 $> chmod 755 a; /bin/bash a
|
Das Programm kommt einem Virus nahe, trägt es doch manch ähnlichen Wesenszug: Es repliziert sich rasend schnell und saugt damit Schritt für Schritt die Lebensenergie aus einem vormals gesunden Rechner. Der Echo-Befehl schreibt ein kurzes Programm in die Datei »a«: Als erstes ruft es sich selbst auf, allerdings im Hintergrund, gekennzeichnet durch den Ampersand »&«. Der ist in der Bash gleichzeitig auch ein Trennzeichen, ganz ähnlich wie das Semikolon - eben nur mit dem Unterschied, dass der vor dem Separator stehende Prozess nicht erst terminieren muss, bevor die Shell den nächsten beginnt. Im Anschluss startet die Bash »a« noch ein zweites Mal. Da dieser Prozess nicht im Hintergrund läuft, beendet sich der erste Aufruf erst, wenn die zweite Neuinkarnation terminiert. Die ihrerseits beendet sich erst, wenn ihr zweiter Kindprozess terminiert und so weiter - also effektiv nie.
Pyramidenbau
Damit laufen nach dem ersten Durchlauf des Skriptes drei Prozesse, im zweiten sieben, 15 in der dritten Iteration und so weiter. Ein Blick auf Abbildung 1 zeigt die Ausrüstung eines typischen Desktop-PC: Core-2-CPU mit 3,0 GHz, 2 GByte Hauptspeicher, von denen nach dem Start von Kubuntu noch rund 1600 MByte frei sind, wenn man den entbehrlichen Plattencache einrechnet. Dank dynamischer Bibliotheken und moderner Speicherverwaltung teilen sich zwar die meisten »a«-Prozesse die Libc und den größten Teil des Bash-Binarys, aber jeder einzelne Prozess belegt auch rund 2 MByte an individuellen Tabellen im Kernel und prozessspezifischen Variablen im Userspace. Hier wirkt sich vor allem die das exponentielle Wachstum der Zahl an Tasks aus, wie Abbildung 2 anschaulich illustriert.
Abbildung 1: Das Testsystem für die Fork-Bombe ist eigentlich gut ausgestattet: Die Core-2-CPU mit 3,0 GHz verfügt über 2 GByte Hauptspeicher. Trotzdem zwingt ein Shell-Einzeiler das System binnen Sekunden in die Knie.
Abbildung 2: Kommt das rekursive Skript erst einmal ins Rollen, ist es nur schwer zu stoppen. Selbst ein »killall«-Befehl verpufft mitunter, da neue Prozesse schneller sprießen als das Tool beenden kann.
Wie kann es aber sein, dass so ein einfaches Programm ein Linux-System so profan zum Absturz zwingt? Die Antwort lautet, dass das System gar nicht abstürzt, sondern sich einfach unerträglich abbremst. Ist irgendwann der virtuelle Hauptspeicher voll, fängt Linux an zu optimieren, sucht nach kleinen Stückchen freien Speichers. Oft wird es dabei sogar fündig, allein der Aufwand gerät immer unverhältnismäßiger - zumal das virale Skript jeden Speicher sofort verschlingt und doppelt so viel nachfordert.
Emsiges Umsortieren
Irgendwann klappt nicht einmal mehr das und der Systemaufruf »fork()« erhält »EAGAIN« oder »ENOMEM« als Antwort, je nachdem ob gerade dem Userspace oder dem Kernel der Speicher ausgegangen ist: Dann terminiert tatsächlich der betroffene Prozess und gibt seinen Speicher wieder frei. Einer Hydra gleich sprießen jedoch einem seiner vielen Brüder zwei neue Köpfe. Der Anwender stellt das dadurch fest, dass die Load rapide ansteigt, da viel mehr Prozesse zur Ausführung bereit sind, als die CPU-Kerne abzuarbeiten in der Lage sind. Genau dieses Verhältnis drückt die Load aus [2].
Noch unmittelbarer ist der Effekt, dass sich Fenster nur noch langsam aufbauen und schließlich sogar die Maus ruckweise bewegt. Das ist insofern bemerkenswert, als dass kurzlaufende, höher priorisierte Interrupt-Routinen die Maus bewegen, zumindest im Vergleich zu erheblich komplexeren Fenster-Aufbauten moderner grafischer Oberflächen.