- Ich denke, vielen ist nicht ausreichend bewusst, wie unvollständig die Dokumentation der Linux-Kernel-API ist und wie Rust einen Teil dieses Problems löst
- Ich habe Rust-Abstraktionen für mehrere Subsysteme geschrieben und musste in fast allen Fällen den C-Quellcode lesen, um vollständig zu verstehen, wie die API sicher zu verwenden ist
- Anhand von Funktionssignaturen und zugehörigen Dokumentationskommentaren oder expliziter Dokumentation allein lässt sich die sichere Verwendung der API nur schwer vollständig erfassen
- ob ein Lock gehalten werden muss
- ob ein Reference-Count-Argument eine Referenz übergibt oder selbst eine Referenz übernimmt
- ob beim Aufruf eines Callback ein Lock gehalten wird oder ob man es selbst erwerben muss
- ob es beim
free-Callback etwas Besonderes gibt - welche Lock-Reihenfolge beabsichtigt ist
- ob es Sonderfälle gibt, in denen bestimmte Operationen je nach Situation mit Locks arbeiten können
- ob
NULL-Argumente erlaubt sind und wie sie gültig verwendet werden dürfen - was im Fehlerfall mit dem Reference Count geschieht
- ob ein zurückgegebener referenzgezählter Pointer bereits inkrementiert ist oder nur ein implizites Borrow aus der Referenz des übergebenen Arguments darstellt
- ob ein Rückgabewert immer ein gültiger Pointer ist,
NULLsein kann oder auchERR_PTRsein kann - ob ein über indirekte Argumente zurückgegebener Pointer im Fehlerfall auf
NULLbereinigt wird oder unverändert bleibt - ob es gültig ist,
NULL **zu übergeben, wenn der zurückgegebene Pointer nicht benötigt wird
- Manchmal waren die Anforderungen vernünftig, aber nicht dokumentiert
- manchmal waren die Anforderungen zu flexibel oder komplex, sodass ich beim Schreiben von Rust-Abstraktionen subjektive Entscheidungen treffen musste, um sie auf sichere Verwendung einzugrenzen
- manchmal musste ich innerhalb der Abstraktion zusätzliche Locks einführen, um Sicherheit zu gewährleisten
- manchmal musste ich den C-Code leicht ändern, um ihn orthogonaler, logischer und einfacher nutzbar zu machen (z. B. eine Unlock-Funktion freilegen, die bei gehaltenem Lock verwendet werden kann)
- manchmal war das Locking so subtil, dass man zwar eine sichere Rust-Abstraktion schreiben konnte, aber große Dokumentationskommentare nötig waren, die auf die korrekte Nutzung und Freigabereihenfolge zur Vermeidung von Deadlocks hinweisen mussten (Rust verhindert Deadlocks nicht grundsätzlich)
- manchmal war das Problem unlösbar, ohne den C-Code selbst sinnvoller zu machen (im Fall von
drm_sched)
- In den meisten Fällen deuten die Kompromisse beim Schreiben von Rust-Abstraktionen jedoch auf Probleme im Design des C-Codes und mögliche Verbesserungsrichtungen hin
- der allgemeine Ansatz ist: „Zuerst Rust-Code schreiben, während der C-Code so wenig wie möglich verändert wird, um Konflikte zu vermeiden, und anschließend auf Basis der gewonnenen Erkenntnisse Änderungen am C-Code vorschlagen“ (zum zweiten Teil bin ich bisher noch nicht gekommen)
- Das Ergebnis ist, dass man in den meisten Fällen bereits durch die Rust-API erkennen kann, wie sie korrekt zu verwenden ist
- man muss sich keine Sorgen mehr um Reference Counts,
NULL-Pointer, vergessene Ergebnisprüfungen oder das Freigeben von Referenzen im Fehlerfall machen - man muss sich keine Sorgen mehr um korrektes Locking, vergessene Referenzübernahmen oder doppelte Freigaben machen
- man muss sich nicht fragen, wie Fehler-Rückgabewerte codiert sind
- denn wenn man diese Dinge falsch macht, kompiliert der Code nicht
- natürlich kann man APIs immer noch falsch verwenden, aber im schlimmsten Fall führt das nur zu Fehler-Rückgaben oder Deadlocks (Deadlocks lassen sich mit
lockdepleicht debuggen, und dieArc<>-Integration kann Locking-Fehler rund um Freigabe und Dereferenzierung erkennen)
- man muss sich keine Sorgen mehr um Reference Counts,
- Selbst die relativ strikt dokumentierte OpenFirmware-/DeviceTree-API ist in C mühsam und fehleranfällig, wenn man alle Regeln einhalten will
- im OF-Code von Treibern sind Referenzlecks wahrscheinlich
- die meisten Systeme kompilieren den Kernel nicht mit
OF_DYNAMIC, daher werden Reference Counts ignoriert und solche Probleme weder entdeckt noch behoben - die von mir geschriebene OF-Rust-Abstraktion behandelt das Reference Counting jedoch automatisch, sodass man sich darum nicht kümmern muss
- Vorteile der Kernel-Entwicklung in Rust gegenüber C
- beim Kernel-Coding in C gibt es praktisch nur zwei Optionen
- man probiert es einfach aus und hofft, dass Reviewer die Probleme finden, oder quält sich durch das Debugging
- oder man investiert vorher stundenlang, um alles zu verstehen, und hofft, wirklich alle Probleme zu entdecken, bevor man den Code überhaupt zu verwenden wagt
- das erhöht auch die Arbeitslast für Reviewer und Maintainer
- sie müssen Einreichungen darauf prüfen, ob alle undokumentierten versteckten Regeln eingehalten wurden
- manchmal übersehen sie Probleme, und manchmal sind die Probleme so groß, dass der Code umfangreich refaktoriert werden muss
- Mit Rust fällt all das weg. Wenn der Code kompiliert, ist er sicher, funktioniert korrekt und hat keine Referenzlecks (Ausnahmen sind
unsafe-Code, aber nur dieser muss gesondert geprüft werden, und dafür gilt die Regel, dass er gut dokumentiert sein soll)- natürlich braucht man weiterhin Code-Reviews und die Hilfe von Experten für das jeweilige Subsystem. Rust macht Code nicht auf magische Weise perfekt
- aber es beseitigt all die dummen Low-Level-Probleme und Fehler, sodass man sich auf die High-Level-Probleme konzentrieren kann
- Haltung gegenüber Linux-Entwicklern
- ich mache Linux-Entwicklern wegen der unvollständigen Dokumentation keinen Vorwurf
- der Linux-Kernel ist extrem komplex und muss viele subtile Probleme behandeln
- die meisten User-Space-APIs haben deutlich einfachere Regeln für sichere Nutzung
- Kernel-Entwicklung ist schwierig
- selbst erfahrene Kernel-Entwickler machen solche Fehler immer wieder
- es ist kein rein technisches Problem, sondern eines menschlicher Grenzen: All diese komplexen Regeln im Kopf zu behalten und jedes Mal korrekt anzuwenden, ist unmöglich
- Die Lösung
- wir brauchen Werkzeuge
- die Lösung ist Rust. Wenn alle Regeln einmal in Code und Typsystem kodiert sind, muss man sich nie wieder darum kümmern
- so wie die Lösung für Debatten über Coding-Stil darin besteht, alle Regeln in einem automatischen Formatter zu kodieren
- dann können wir aufhören, uns um Low-Level-Sicherheit, Ownership und Synchronisationsprobleme zu sorgen, und uns wichtigeren Themen wie dem Design von Treibern und Subsystemen widmen
- Code-Formatierung im Rust-for-Linux-Projekt
- das Rust-for-Linux-Projekt wendet für Einreichungen tatsächlich
rustfmtan - beim Schreiben von Kernel-Rust muss man sich weder um Code-Formatierung noch um Beschwerden im Code-Review dazu sorgen
- einfach
make rustfmtausführen
- das Rust-for-Linux-Projekt wendet für Einreichungen tatsächlich
Meinung von GN⁺
- Dieser Beitrag benennt die Probleme bei API-Dokumentation und Sicherheit in der Linux-Kernel-Entwicklung treffend. Er zeigt die Grenzen von C und die Vorteile von Rust gut auf
- Die Formulierung, Rust sei die „einzige Lösung“, wirkt jedoch etwas überzogen. Auch mit anderen Ansätzen wie statischen Analysewerkzeugen ließen sich einige Verbesserungen erzielen
- Rust löst viele Probleme wie Speichersicherheit, dennoch bleiben sorgfältige Code-Reviews und Tests notwendig. Es ist keine Wunderwaffe
- Der Umstieg auf Rust kann verschiedene Schwierigkeiten mit sich bringen, etwa die Kompatibilität mit bestehendem C-Code oder die Lernkurve für Entwickler. Eine schrittweise Einführung erscheint sinnvoll
- Um alte Praktiken und die Kultur des Linux-Kernels zu verbessern, braucht es neben Rust auch Anstrengungen in Bereichen wie Dokumentation, Mentoring und Kommunikation
- Insgesamt zeigt der Beitrag gut das Potenzial und die Vorteile von Rust in der Linux-Kernel-Entwicklung, warnt aber zugleich vor überzogenen Erwartungen oder blindem Vertrauen und vermittelt damit eine ausgewogene Perspektive. Die Einführung von Rust dürfte technisch wie kulturell schwierig sein, könnte langfristig aber zur Verbesserung von Sicherheit und Wartbarkeit des Kernel-Codes beitragen.
2 Kommentare
Rust ... ich habe es zwar persönlich einmal versucht zu lernen, aber in unserem Unternehmen haben wir es bisher noch nicht eingeführt. Wir haben bereits einen riesigen Berg an Code in C++ geschrieben, und dazu kommt das Problem, dass die bestehenden Mitarbeiter Rust noch einmal neu lernen müssten ... Ich habe gehört, dass es auch in Korea Unternehmen gibt, die Rust bereits in der Produktion einsetzen; es wäre schön, wenn entsprechende Erfahrungen oder Ähnliches geteilt würden.
Hacker-News-Kommentar
Sprachen wie Rust und Swift sind ausdrucksstark genug, dass der Compiler die Thread-Sicherheit von Datentypen oder Methoden erkennen kann
Bei Rust-Bibliotheken ist die Dokumentation oft unzureichend
Beim Versuch, Rust wie C zu verwenden, gerät man wegen des Borrow Checkers schnell in Schwierigkeiten
&selfoder&mut selfzu achten&mut selfvorhanden ist, muss man einen Mutex verwenden, um eine Instanz zwischen Threads zu teilenBei Rust-APIs kann man in den meisten Fällen erkennen, wie sie korrekt zu verwenden sind
Ein konkretes Beispiel in Rust ist die Verwendung von Sperren zum Schutz von Daten
Auch in anderen Sprachen kann eine redundante Implementierung von APIs die Klarheit von Code und Dokumentation erhöhen
Bei der Verwendung von C in Python-Erweiterungen muss man die Calling Conventions kennen
Diese Leute sind Helden und leisten großartige Arbeit
Wir sind dem Ziel von vollständig selbstdokumentierendem Code einen Schritt näher gekommen