Open Source im professionellen Einsatz

Kernel- und Treiberprogrammierung mit dem Kernel 2.6 - Folge 47

Kern-Technik

,

Sicherheitsbewusste Kernelhacker beäugen skeptisch Netzwerkapplikationen, die in den Kernel wandern sollen. Manchmal gibt\'s dafür aber gute Gründe - Performance etwa.

Wenn Anwender besondere Anforderungen an den Leistungsdurchsatz legen, kann die Verlagerung von Code aus dem Userland in den Kernel sinnvoll sein. Die Maßnahme spart Kontextwechsel-Zeiten ebenso wie aufwändige Kopieraktionen zwischen User- und Kernelspace. Allerdings hebt sie die sinnvolle Trennung von Applikation und Kernel auf. Die Applikation im Kernel hebelt die sonst vorhandenen Schutzmechanismen aus, öffnet im Fall von Programmierfehlern Crackern ein direktes Tor zum Kernel und gerät daher stets zur Speziallösung.

Dennoch lässt sich die Leistungssteigerung für eigene Netzwerkanwendungen im Kernel nutzen. Kleiner Nebeneffekt der Technik: Sämtliche Komponenten einer solchen Anwendung lassen sich in einem Stück Software, nämlich einem Kernelmodul, verpacken. Wo früher also ein oder mehrere Applikationsteile zu installieren waren, lädt der Anwender nur noch ein distributionsunabhängiges Kernelmodul nach.

Doch ist eine Applikation im Kern nicht so einfach zu programmieren wie eine Userspace-Anwendung, weil Linux von dort keinen Zugriff auf normale Bibliotheksfunktionen ermöglicht [1]. Spannenderweise haben die Kernelentwickler gerade für den Netzwerkzugriff aber einen ganzen Reigen von Funktionen geschrieben. Den dürfen sogar Programmierer nutzen, die ihre Software nicht unter die GPL stellen.

Tabelle 1: Prototypen der Netzfunktionen im Kernel

Tabelle 1: Prototypen der Netzfunktionen im Kernel

Abbildung 1: Für bekannte Userspace-Funktionen bietet der Kernel korrespondierende Routinen an.

Abbildung 1: Für bekannte Userspace-Funktionen bietet der Kernel korrespondierende Routinen an.

Die Funktionen kreieren und löschen Sockets, binden sie an Ports, warten auf eingehende Verbindungen oder bauen solche auf (siehe Tabelle 1). Aus der Bandbreite der Userspace-Funktionen steht also auch im Kernel das Wichtigste zur Verfügung (siehe Abbildung 1).

Ähnlich, aber nicht gleich

Entwicklern fallen vier Umstände auf: Erstens haben die Kernel-Pendants andere Namen im Vergleich zu ihren Userspace-Geschwistern, zweitens sind die Aufrufparameter unterschiedlich. Der dritte Unterschied betrifft die Funktionen »read()« und »write()« zum Datenaustausch: Diese Funktionen sucht der Entwickler im Kern vergeblich und muss sie selbst implementieren. Dazu bieten viertens die Funktionen »socket_sendmsg()« und »kernel_sendmsg()« ihre Dienste an - mit auf den ersten Blick ähnlicher Funktionalität.

Einfach anwenden lassen sich die Funktionen »kernel_sendmsg()« und »kernel_recvmsg()«. Beiden übergibt der Kernel-Netzwerker neben dem Socket noch eine Datenstruktur vom Typ »struct msghdr«, die bei verbindungslosen Sockets unter anderem auch die Zieladresse für das Paket enthält (siehe Abbildung 2). Die Daten müssen nicht zwangsläufig an einem zusammenhängenden Speicherort liegen. Vielmehr übergibt der Entwickler ein Feld vom Typ »struct kvec«, das Adressen und Längen der zugehörigen Speicherorte enthält. »kernel_sendmsg()« und »kernel_recvmsg()« kümmern sich darum, dieses Feld in die »struct msghdr« einzuhängen.

Abbildung 2: Die Datenstrukturen für den Datenaustausch erfordern genaues Hinsehen beim Programmieren.

Abbildung 2: Die Datenstrukturen für den Datenaustausch erfordern genaues Hinsehen beim Programmieren.

Variantenreich

Bei den Funktionen »sock_sendmsg()« und »sock_recvmsg()« hängt der Programmierer die Speicherort-Adressen - jetzt vom Typ »struct iovec« - vor dem Aufruf in den Messageheader ein. Das allein ist aber nicht der entscheidende Unterschied. Systemcalls wie die zwei »sock_«-Funktionen sind qua Implementierung darauf geeicht, Daten zwischen Kernel- und Userspace auszutauschen.

Der Transfer von Kernel zu Kernel ist hingegen nur in seltenen Fällen notwendig. Daher muss der Entwickler vor dem Aufruf der zum Systemcall gehörenden Kernelfunktion die Speicherverwaltung mit einem Trick überlisten. Es gilt, sie zu überreden auch aus dem Kernel selbst einen Zugriff zu gestatten.

Das zugrunde liegende Problem und die Lösung stellt der Kasten "Datentransfer innerhalb des Kernels" vor. Falls sich die in »iov« spezifizierten Speicherorte im Kernelspace befinden und der Entwickler den Systemaufruf »sock_sendmsg()« verwendet, bettet er ihn in die in Listing 1 dargestellte Codesequenz ein.

Datentransfer innerhalb des
Kernels

Systemcalls, die Dienste des Betriebssystemkerns, tauschen Daten zwischen Userspace und Kernelspace vorwiegend mit den Funktionen »copy_to_user()« und »copy_from_user()« aus [2]. Sie verarbeiten Adressen im Userspace, die von der Applikation stammen. Sie sind aus Sicht des Betriebssystemkerns alles andere als vertrauenswürdig. Daher überprüft die Speicherverwaltung vor einem Datentransfer, ob die Adressen gültig sind, ob sie nicht in der so genannten Zeropage liegen oder ob die Adressen nicht ungewollt (oder von einem Malevolenten gar manipuliert) auf eine Adresse im Kernelspace zeigen. Das wäre ein Wert oberhalb der Grenze zwischen User- und Kernelspace.

Damit ein Systemcall Daten aus dem Kern akzeptiert, verschiebt der Entwickler diese Grenze kurzfristig. Linux legte sie früher im FS-Register der x86-CPU ab. Wegen dieser Geschichte heißen bis heute die Funktion, die die Grenze auslesen und einstellen, »get_fs()« und »set_fs()«. Vor dem Systemcall aus dem Kernel rettet der Programmierer also den alten Wert und erlaubt temporär mit »KERNELDS« den internen Zugriff. Er darf natürlich nach dieser Operation nicht vergessen, die ursprünglichen und sicheren Werte wieder zu reaktivieren.

Listing 1: Zugriff auf
Kerneldaten

01 msg.msg_iov    = &iov;
02 msg.msg_iovlen = 1;
03 
04 oldfs = get_fs();  // Grenze sichern
05 set_fs(KERNEL_DS); // Aushebeln des Schutzmechanismus
06 len = sock_sendmsg(sock, &msg, len);
07 set_fs(oldfs); // Restauration des Ursprungszustands

Außerdem gilt für Netzwerkcode im Linux-Kern, dass viele Zugriffe auf das Netz-Interface nur im Prozesskontext, nicht aber innerhalb einer Interrup-Service-Routine (ISR), eines Tasklet oder einer Timerfunktion statthaft ist. Das Programm muss sich also entweder den Prozesskontext einer Applikation borgen (zum Beispiel den des Programms »insmod«) oder aber mit Hilfe der Funktion »kernel_thread()« eine unabhängige Task aufziehen.

Diesen Artikel als PDF kaufen

Express-Kauf als PDF

Umfang: 5 Heftseiten

Preis € 0,99
(inkl. 19% MwSt.)

Als digitales Abo

Als PDF im Abo bestellen

comments powered by Disqus

Ausgabe 07/2013

Preis € 6,40

Insecurity Bulletin

Insecurity Bulletin

Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...

Linux-Magazin auf Facebook