In den 1990ern konnten Linux-Server-Admins mit Uptimes von 200 oder 300 Tagen prahlen. Seit für den Kernel alle naselang Securitypatches erscheinen, ist die Grandezza dahin. Die Kernelhacker liefern mit dem technisch hochkomplexen Live-Patching einen Mechanismus, der alten Glanz zurückbringt.
Wer sich auf einem frisch installierten Ubuntu 18.04 LTS erstmals einloggt, kriegt die Frage vorgelegt, ob er Livepatch einrichten möchte (Abbildung 1). Das Feature – so die Zusatzinfo – hilft dabei, den Computer sicher zu halten, indem das System einige Updates ohne Neustart einspielt. Das Thema ist nicht, irgendwelche Applikationen zu updaten, sondern ein Mechanismus, der im laufenden Betrieb Fehler im Kernelcode repariert. Bildlich gesprochen: Es geht um eine Operation am offenen und – vor allem – am schlagenden Herzen.
Linuxer mit einem guten Erinnerungsvermögen werden nun einwenden: Live-Updates ohne Neustarts, gibt es die nicht schon länger? Ja und nein: Suse und Red Hat hatten vor ein paar Jahren mit Kpatch und Kgraft konkurrierende Projekte am Start [1]. Allerdings erwiesen die sich in der Praxis nicht als universell handhabbar, sodass die Kernelcommunity nochmals nachgelegt und in den Kernel ein gutes Stück hochkomplexe Technik zur Code-Live-Substitution implantiert hat.
Aber wozu das Risiko einer OP am offenen Herzen eingehen? Weil man den Unternehmensserver, auf dem geschäftskritische Applikationen laufen, höchst selten neu starten kann: Er führt vielleicht gerade langwierige Berechnungen durch, deren (Zwischen-)Ergebnisse mit einem Reboot verloren wären. Oder er hält gerade Netzwerkverbindungen offen, deren Abbruch Umsatzeinbußen oder Störungen anderer Dienste zur Folge hätte.
Das Dilemma ist, dass es gleichwohl essenziell ist, ein anstehendes Kernelupdate zu absolvieren. Denn Sicherheitslücken im Betriebssystemkern gefährden das gesamte System, und skrupellose Hacker, die Schwachstellen auszunutzen trachten, finden sich zuhauf.
Livepatch ist Canonicals Marketing-Sprech für das Kernelfeature Live-Patching. Der Mechanismus ermöglicht es zwar nicht, einzelne, losgelöste Codesequenzen auszutauschen, aber sehr wohl ganze Funktionen, deren Aufrufkonvention beziehungsweise Prototyp identisch ist. Die beachtliche Leistung der Linux-Entwickler in der Implementierung steckt darin, eine extrem komplexe Technik leicht handhabbar zu gestalten. Dabei setzen sie auf die bewährten, zur Laufzeit ladbaren Kernelmodule, um den neuen Code in den Kernel zu injizieren.
Handhabung
Die Operation am schlagenden Herzen beginnt also mit dem Programmieren eines Kernelmoduls. Es beinhaltet den Code der fehlerkorrigierten, also der gepatchten Funktionen zusammen mit einigen beschreibenden Datenstrukturen, die das so genannte Patch-Objekt definieren. Dieses Objekt übergibt das Modul dem Live-Patching-Subsystem (KLP-Subsystem) im Zuge der Initialisierung durch Aufruf der Funktion »klp_enable_patch()«.
Der Admin lädt also das vorbereitete Patch-Modul per »insmod« oder per »modprobe«, und schwups tauscht das KLP-Subsystem alle im Patch definierten Funktionen gegen neue Varianten aus. Im besten Fall sind damit die im neuen Code gefixten Sicherheitslücken geschlossen und Hacker erfolgreich ausgesperrt.
Abbildung 2 zeigt den Aufbau des im Modul definierten Patch-Objekts. Auf unterster Ebene steht eine Zuordnung zwischen fehlerhafter und korrigierter Funktion (»struct klp_func«). Eine Reihe solcher Zuordnungen fasst »struct klp_object« quasi tabellarisch zusammen.
Zusätzlich vermerkt ist, in welcher Komponente das KLP-Subsystem die mit C-Code-Namen angegebenen Funktionen findet, nämlich entweder im Kernel selbst (»vmlinux«) oder in einem anderen Kernelmodul. Ein oder mehrere »struct klp_object« ergeben schließlich das Patch-Objekt, das das Live-Patching zusammengehörig patcht, sofern sich die zugehörigen Codesquenzen allesamt im Speicher befinden.
Damit der Programmierer die auszutauschenden Funktionen elegant über deren Namen und nicht umständlich in Form der zugehörige Kerneladressen angeben darf, greift KLP auf den im Kernel verankerten Mechanismus von Kallsyms zurück. Sollte ein Name nicht eindeutig sein, beispielsweise weil ein Funktionsname in zwei Modulen auftaucht, kann der Programmierer über einen optionalen Parameter die Auswahl eindeutig gestalten. Für die neue, fehlerbereinigte Funktion ist das alles nicht notwendig. Hier trägt der Compiler beim Generieren des Codes die Adresse selbst ein.
Wechselwillig
Wer Live-Patching selbst testen will, findet Beispiele in den Kernelquellen unter »lib/livepatch«. Eine leicht modifizierte Variante, »test_klp_livepatch.c«, ist in Listing 1 dargestellt. Das Modul tauscht die Funktion »cmdline_proc_show()« (Listing 2) aus, die über ein so genanntes Sequence-File [2] die Kommandozeile ausgibt, mit der der Kernel gebootet hat.
Den Aufruf dieser Funktion triggert der User durch Eingabe von »cat /proc/cmdline«. Ein beispielhaftes Ergebnis eines solchen Aufrufs der ungepatchten Funktion ist in Abbildung 3 zu sehen. Die neue Funktion hat den Namen »livepatch_cmdline_proc_show()«. Anstelle der Kommandozeile erscheint im Sequence-File der Text »this has been live patched for the Linux-Magazin«.
Listing 1
Kernelmodul, um cmdline_proc_show() zu patchen
01 // SPDX-License-Identifier: GPL-2.0
02 // Copyright (C) 2014 Seth Jennings <sjenning@redhat.com>
03
04 #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
05
06 #include <linux/module.h>
07 #include <linux/kernel.h>
08 #include <linux/livepatch.h>
09
10 #include <linux/seq_file.h>
11 static int livepatch_cmdline_proc_show(struct seq_file *m, void *v)
12 {
13 seq_printf(m, "%s: %s\n", THIS_MODULE->name,
14 "this has been live patched for the Linux-Magazin");
15 return 0;
16 }
17
18 static struct klp_func funcs[] = {
19 {
20 .old_name = "cmdline_proc_show",
21 .new_func = livepatch_cmdline_proc_show,
22 }, { }
23 };
24
25 static struct klp_object objs[] = {
26 {
27 /* name being NULL means vmlinux */
28 .funcs = funcs,
29 }, { }
30 };
31
32 static struct klp_patch patch = {
33 .mod = THIS_MODULE,
34 .objs = objs,
35 };
36
37 static int test_klp_livepatch_init(void)
38 {
39 return klp_enable_patch(&patch);
40 }
41
42 static void test_klp_livepatch_exit(void)
43 {
44 }
45
46 module_init(test_klp_livepatch_init);
47 module_exit(test_klp_livepatch_exit);
48 MODULE_LICENSE("GPL");
49 MODULE_INFO(livepatch, "Y");
50 MODULE_AUTHOR("Seth Jennings <sjenning@redhat.com>");
51 MODULE_DESCRIPTION("Livepatch test: livepatch module");
Listing 2
Original-Funktion cmdline_proc_show()
01 // SPDX-License-Identifier: GPL-2.0
02 #include <linux/fs.h>
03 #include <linux/init.h>
04 #include <linux/proc_fs.h>
05 #include <linux/seq_file.h>
06
07 static int cmdline_proc_show(struct seq_file *m, void *v)
08 {
09 seq_puts(m, saved_command_line);
10 seq_putc(m, '\n');
11 return 0;
12 }
13
14 static int __init proc_cmdline_init(void)
15 {
16 proc_create_single("cmdline", 0, NULL, cmdline_proc_show);
17 return 0;
18 }
19 fs_initcall(proc_cmdline_init);
Make Kernel great again
Da das Modul keine weiteren Abhängigkeiten hat, dürfen Experimentierfreudige den Code in ein Verzeichnis kopieren und ihn mit Hilfe des Makefile aus Listing 3 per »make«-Aufruf generieren. Alternativ lassen sich bei der Kernelkonfiguration die Optionen »Kernel hacking | Runtime Testing | Test livepatching« (Abbildung 4) und »Kernel hacking | Build live patching samples« setzen sowie im Quellcode-Verzeichnis »make -j4 modules« aufrufen. Ein »make modules-install« installiert dann die frisch generierten Module und damit die Test-Patches.
Apropos Konfiguration: Live-Patching funktioniert nur, wenn der Linuxer in der Kernelkonfiguration »Processor type and features | Kernel Live Patching« ausgewählt hat. Um den Kernel live zu patchen, lädt der Admin das eben erzeugte Modul. Allerdings schlug das auf einem getesteten Ubuntu 16.04 mit dem Standardkernel 4.15.51 mit der Fehlermeldung »Invalid parameters« fehl. Auf einem selbst generierten Kernel 5.1 war das Laden des Moduls und damit das Kernel-Patchen kein Problem – ein »cat /proc/cmdline« liefert die gepatchte Ausgabe.

Abbildung 4: Der Linux-Quellcode – zu sehen ist die Kernel-Konfiguration – bringt alles Notwendige fürs Live-Patching mit.
Listing 3
Makefile zum Generieren des Patch-Treibers
01 obj-m+=test_klp_livepatch.o 02 all: 03 make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules 04 clean: 05 make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
Patch zurückrollen
Linux wäre nicht Linux, wenn das schon alles bezüglich Handhabung des Live-Patching wäre. Im Verzeichnis »/sys/kernel/livepatch/« findet der Admin seine Patch-Historie (Abbildung 3). Für jedes installierte Patch legt der Kernel ein Unterverzeichnis an.
Durch das Schreiben einer »0« auf die Datei »enabled« kann der Admin ein Patch rückgängig machen, der ursprüngliche Code wird wieder aktiv und das virtuelle Patch-Verzeichnis verschwindet so still und heimlich, wie es aufgetaucht ist. Außerdem entsteht für jede gepatchte Komponente ein Unterverzeichnis, das wiederum Namen und einen Versionszähler der gepatchten Funktion enthält.
Technisch anspruchsvoll
So einfach das Kernel-Patching ohne Reboot zu handhaben ist, so komplex ist der technische Hintergrund. Eine Codesequenz im Hauptspeicher mit einer anderen überschreiben, das klappt nicht einfach so: Die fehlerbereinigte Funktion könnte mehr Platz als die fehlerhafte benötigen. Noch diffiziler: Als Wunderwerk paralleler Verarbeitung könnte der gerade Live-patchende Linux-Kernel im selben Moment eine Instanz abarbeiten, die genau jene Funktion benutzt: Eine Hälfte alter, eine Hälfte neuer Code? Ooops!
Daher setzen die Kernelarchitekten grundsätzlich auf das Umleiten oder Umbiegen der Funktion. Das hat den Nebeneffekt, dass der Systemverantwortliche jederzeit zur ungepatchten Version zurückkehren kann. Funktionen, die nur über Ein- und Ausgabeparameter definiert sind und keine in- oder externen Zustände ändern, lassen sich damit leicht ersetzen. Setzt ein Patch also nur zusätzliche Sicherheits-Checks ein oder sichert einen abgeschlossenen, kritischen Abschnitt, gelingt das sofort.
Unabhängig davon, dass die Technik des Umleitens im Kernel bereits Kernelprobes und Function Tracer (Ftrace, [3]) einsetzt, man sich also mit diesen Subsystemen arrangieren muss, reicht ein einfaches Umbiegen in vielen Fällen nicht aus. Denn wenn ein Patch komplexere Reparaturen durchführt und beispielsweise mehrere Funktionen austauscht, die voneinander abhängig sind, oder über Locking-Mechanismen miteinander verknüpft oder wenn sich Datenstruktur-Elemente ändern, vielleicht sogar eine neue Bedeutung bekommen, dann muss das Live-Patching alle Komponenten, die auf diese Funktionen oder Datenstrukturen zugreifen, gleichzeitig austauschen.
Kein Stopp
Dazu haben die Kernelentwickler ein aus drei Zuständen bestehendes Konsistenz-Modell entwickelt (unpatched, in Transition, patched), das darauf beruht, dass der Kernel über einen gewissen Zeitraum hinweg sowohl die ungepatchte als auch die gepatchte Funktions-Version parallel nutzt (Abbildung 5). Dieses Modell ermöglicht das Live-Patchen, ohne alle Tasks in einen Stopp-Zustand zu überführen, wie es in einer allerersten Live-Patch-Version implementiert war (Ksplice in Oracle Linux, [4]).
Vielmehr hat Linus Torvalds den Task Control Block um das Flag »patch_state« erweitert, das reflektiert, ob die zugehörige Task die ungepatchte oder die gepatchte Version verwenden soll. Anfänglich ist das Flag für alle Tasks auf unpatched gesetzt. Wird das Live-Patching durch Aufruf der Funktion »klp_enable_patch()« angestoßen, geht Linux durch alle Tasks und untersucht anhand des Stack, ob eine davon gerade auf die auszutauschende Funktion zurückgreift. Ist das nicht der Fall, setzt der Kernel das Flag auf patched. Ansonsten muss die Ablaufsteuerung auf einen sicheren Zustand der Task warten. Das ist klassisch dann der Fall, wenn eine Task einen Systemcall abschließt. Widerspenstigen Tasks, die einfach keinen Systemcall abschließen wollen, schickt KLP nach wenigen Sekunden ein Signal – denn ist sie nicht willig, braucht Linux Gewalt.
Ftrace regelt die Umleitung
Die Live-Patch-Technik basiert auf dem Ftrace-Subsystem, das wiederum auf einem Compiler-Feature aufbaut. Bekommt nämlich der C-Compiler (GCC) beim Aufruf die Option »-gp« übergeben, fügt er zu Anfang einer jeden Funktion Extra-Code ein (den Unterprogramm-Aufruf einer »mcount()«-Funktion), um beispielsweise statistische Informationen zur Aufrufhäufigkeit (Profiling) zu erhalten (Abbildung 6). Hier klinkt sich KLP ein.
Solange das Patch nicht abgeschlossen ist (Zustand: in Transition), ruft der Kernel abhängig vom Attribut »patch_state« der aktiven Task entweder die ursprüngliche oder die neue Funktion auf. Erst nach dem Wechsel in den Zustand patched entfällt diese Fallunterscheidung.

Abbildung 6: In den beiden Zuständen in Transition und patched leitet der Kernel per Ftrace-Technik die Funktionsaufrufe um.
Hackers best friend
Mancher hat bereits die Hände über dem Kopf zusammengeschlagen: Das Werkzeug, das Hacker aus dem Kernel fernhalten soll, ist zugleich das Werkzeug, von dem jeder Hacker nur träumt. Daher sind strenge Kontrollmechanismen notwendig, die zu realisieren beispielsweise signierte Kernelmodule [5] helfen.
Canonical hat, ebenso wie Red Hat oder Suse, rund ums Kernel-Live-Patching ein Ökosystem aufgesetzt. Neben dem Anfertigen der Patches enthält es ausgiebige Tests, das gesicherte Verteilen und automatisierte Installation. Der Aufwand kostet Unternehmen natürlich Geld.
Privatpersonen dürfen auf bis zu drei Rechnern Live-Patching kostenlos einsetzen, Unternehmen bezahlen für die Dienstleistung. Jetzt wird auch klar, warum jeder, der auf »Livepatch einrichten …« klickt, sich zuvor bei Canonical registrieren muss.
Wer auf das – wie diese Kern-Technik gezeigt hat – sinnvolle Angebot eingeht, ist gleichwohl nicht auf ewig aus dem Schneider. Das Thema Sicherheit mit seinen vielen Seiten wird Linux wohl immer begleiten.
Infos
-
Martin Loschwitz, “Kpatch und Kgraft”: Linux-Magazin 07/14, S. 64
-
Quade, Kunst, “Linux-Treiber entwickeln”: Dpunkt-Verlag, 2016, S. 280 ff.
-
Ftrace: https://www.kernel.org/doc/Documentation/trace/ftrace.txt
-
Ksplice: https://ksplice.oracle.com
-
Quade, Kunst, “Kern-Technik”, Folge 82 über signierte Module: Linux-Magazin 09/15, S. 86









