Grenzen der Garantie

unsafe: fünf Operationen, kein Freibrief

Der Borrow-Checker läuft im unsafe-Block weiter. Was ihr regeln müsst, ist nicht die Sprache, sondern wer solche Stellen schreibt, begründet und gegenliest.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Seit 1997 am Markt Kleine Gruppen Präsenz und Live-Online Zertifizierte Trainer
Worum es geht

Der Irrtum über unsafe kostet mehr als der Code darin

In den meisten Teams gibt es zwei Haltungen, und beide beruhen auf derselben Fehlannahme. Die eine verbietet unsafe pauschal, weil darin angeblich der Borrow-Checker ausgeschaltet ist. Damit fällt jede Anbindung an eine vorhandene C-Bibliothek weg und jeder Zugriff auf ein Hardwareregister. Die andere behandelt den Block als eine Art C-Modus, in dem die üblichen Regeln nicht gelten, und schreibt dort Code, den in Rust niemand mehr gegenliest.

Der größere Teil des unsafe in eurem Produkt stammt ohnehin nicht von euch. Er steckt in den Crates, die ihr direkt eingetragen habt, und in denen, die diese mitbringen. Ein einziges bequemes Paket kann vierzig weitere in den Baum ziehen, von denen niemand die Namen kennt. Geprüft wird bei der Aufnahme fast immer nur die oberste Zeile.

Teuer wird das an zwei Stellen. Erstens bei der Unsoundness: Wenn eine sichere Funktion undefiniertes Verhalten auslösen kann, ohne dass ihre Aufrufer selbst unsafe schreiben, liegt der Fehler nicht im Block, sondern in der Hülle darum, und er trifft Code, der sich auf das Typsystem verlässt. Zweitens beim Nichtstun: Die betroffene Version steht monatelang unverändert in der Cargo.lock, während die zugehörige Meldung längst in der RustSec-Datenbank liegt, weil niemand einen Ablauf dafür hat, der ohne einen Menschen auskommt, der zufällig danach schaut.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt
Wer entscheidet was

Wer entscheidet, wer umsetzt

Regeln zu unsafe scheitern selten am Inhalt und fast immer daran, dass niemand benannt ist. Diese fünf Zeilen decken die Fälle ab, die in einem Rust-Projekt tatsächlich auftreten.

Ein neuer unsafe-Block im eigenen Code

Wer entscheidet
Die technische Leitung des Bereichs zusammen mit der Person, die im Projekt für unsafe benannt ist
Wer setzt um
Wer den Code schreibt, mit SAFETY-Kommentar über dem Block und in einem eigenen Pull Request
Stolperfalle
Der Block reist als Nebensache in einem Feature-Pull-Request mit und wird zusammen mit vierhundert anderen Zeilen durchgewinkt.

Sperre über #![forbid(unsafe_code)]

Wer entscheidet
Die Architektur beim Zuschnitt der Crates, nicht das Team im Nachhinein
Wer setzt um
Wer die Crate anlegt, in der ersten Zeile der lib.rs
Stolperfalle
Die Sperre kommt auf das ganze Projekt und fliegt beim ersten Treiber wieder heraus, statt crate-weise gesetzt zu werden.

Aufnahme einer neuen Abhängigkeit

Wer entscheidet
Die Person, die die Crate gegenlesend freigibt, entlang einer schriftlichen Kriterienliste, damit die Entscheidung nicht von der Tagesform abhängt
Wer setzt um
Wer die Crate braucht, mit einer Begründung im selben Pull Request wie der Eintrag in der Cargo.toml
Stolperfalle
Geprüft wird die eine Crate, nicht die vierzig, die sie in den Abhängigkeitsbaum zieht.

Lizenzen im Abhängigkeitsbaum

Wer entscheidet
Rechtsabteilung oder Einkauf, einmalig als Positivliste erlaubter Lizenzen
Wer setzt um
Das Team über die licenses-Sektion in der deny.toml, sodass ein Verstoß den Build anhält
Stolperfalle
Die Liste liegt als Dokument im Intranet und wird deshalb nie gegen den tatsächlichen Baum gehalten.

Reaktion auf einen neuen RustSec-Eintrag

Wer entscheidet
Wer für das Produkt im Betrieb geradesteht, nicht die Person, die den Fehlschlag zuerst sieht
Wer setzt um
Das Team, entweder mit einer Aktualisierung oder mit einem begründeten Eintrag unter ignore
Stolperfalle
Der Eintrag wird stillgelegt, ohne Datum und ohne jemanden, der ihn wieder aufmacht, und gilt danach als erledigt.

Fünf Operationen gibt unsafe frei, alles andere bleibt geprüft

  1. 01 Rohe Zeiger dereferenzieren, ohne dass jemand ihre Gültigkeit nachweist.
  2. 02 Unsafe-Funktionen aufrufen, deren Bedingungen im Doc-Kommentar stehen.
  3. 03 Veränderliche statische Variablen lesen oder schreiben, ohne Synchronisierung.
  4. 04 Unsafe-Traits wie Send und Sync von Hand implementieren.
  5. 05 Auf ein Feld einer Union zugreifen, das niemand zuletzt beschrieben hat.
  6. 06 Borrow-Checker, Typprüfung und Lebensdauern laufen im Block unverändert weiter.
Was du mitnimmst

Danach ist unsafe im Projekt eine Entscheidung und kein Zufall

Die beiden Themen gehören zusammen, weil beide dieselbe Frage stellen: Wer steht dafür gerade, dass eine Zusicherung eingehalten wird, die der Compiler nicht mehr prüft? Beim eigenen Code beantwortet das eine Regel im Review, bei fremdem Code eine Aufnahmeprüfung und zwei Werkzeuge, die in der Pipeline laufen.

Die fünf Operationen benennen

Du kannst in einer Diskussion sagen, was ein unsafe-Block tatsächlich freigibt und was weiterhin geprüft wird. Damit endet die Debatte darüber, ob Rust mit unsafe wieder C sei.

Sichere Hüllen bauen

Du legst unsafe in das kleinste mögliche Modul, schreibst die Invariante als SAFETY-Kommentar darüber und stellst sicher, dass kein Aufrufer sie mit rein sicherem Code brechen kann.

forbid statt deny einsetzen

Du sperrst unsafe crate-weise über #![forbid(unsafe_code)] und weißt, warum forbid sich anders verhält als deny: Ein inneres allow hebt es nicht auf.

Miri dort einsetzen, wo es trägt

Du lässt die Tests der rechnenden Kerne unter Miri laufen und kennst die Grenze: Code, der echte Hardware oder fremde Bibliotheken anspricht, erreicht der Interpreter nicht.

Abhängigkeiten nach festen Kriterien aufnehmen

Letzte Veröffentlichung, Zahl der Personen mit Veröffentlichungsrecht, Lizenz, Zahl der mitgebrachten Crates und die Frage nach einer kleineren Alternative. Die Entscheidung steht im Pull Request, nicht im Wiki.

Prüfung automatisieren

cargo-audit gegen die RustSec-Datenbank und cargo-deny für Meldungen, Lizenzen, Dubletten und Bezugsquellen laufen bei jedem Pull Request und zusätzlich nachts gegen den Hauptzweig.

KI-generiertDieses Bild wurde mit KI erzeugt · Yves Hoppe / KI / cmt

Fünf Operationen, und der Rest wird weiter geprüft

Das Rust-Buch ist an dieser Stelle ungewöhnlich deutlich: unsafe schaltet weder den Borrow-Checker noch eine andere Sicherheitsprüfung ab. Verwendest du im Block eine Referenz, wird sie weiterhin geprüft. Das Schlüsselwort gibt genau fünf Fähigkeiten frei, die der Compiler danach nicht mehr auf Speichersicherheit prüft: einen rohen Zeiger dereferenzieren, eine unsafe-Funktion oder -Methode aufrufen, eine veränderliche statische Variable lesen oder schreiben, ein unsafe-Trait implementieren und auf ein Feld einer Union zugreifen. Mehr steht nicht auf der Liste.

Was du im Gegenzug übernimmst, ist keine Stilfrage, sondern eine Zusicherung an den Optimierer. Ein roher Zeiger muss zum Zeitpunkt des Zugriffs ausgerichtet, ungleich null und auf initialisierten Speicher des passenden Typs gerichtet sein. Zwei veränderliche Referenzen auf dieselbe Stelle dürfen nie gleichzeitig existieren, auch dann nicht, wenn du sie über Zeiger erzeugt hast und der Compiler die Verwandtschaft nicht sieht. Ein Union-Feld, das nie geschrieben wurde, darfst du nicht lesen. Der erzeugte Maschinencode rechnet mit diesen Regeln, deshalb äußert sich ein Bruch typischerweise nicht als Absturz an Ort und Stelle, sondern als falsches Ergebnis drei Funktionen weiter und nur im Release-Build.

Mit der Edition 2024 sind drei Dinge enger geworden, die vorher stillschweigend durchgingen. extern-Blöcke brauchen jetzt das Schlüsselwort unsafe, die Attribute no_mangle, link_section und export_name werden als unsafe markiert geschrieben, und Referenzen auf static mut sind ein standardmäßig als Fehler gemeldeter Verstoß. Dazu warnt unsafe_op_in_unsafe_fn im Standard: Der Rumpf einer unsafe-Funktion ist nicht länger automatisch ein einziger großer unsafe-Block, du musst die riskante Stelle darin ausdrücklich kennzeichnen. Wer eine ältere Codebasis auf die Edition hebt, sieht genau daran, wie viele Zeilen bisher nur zufällig unbeanstandet blieben.

Jede unsafe-Zeile hat ein Ziel: eine sichere Hülle

Vec, RefCell und Mutex enthalten alle unsafe. Das ist kein Makel, sondern der Zweck der Übung: Die riskante Operation liegt in einem kleinen Modul, und die Invariante, die sie braucht, wird an der Modulgrenze durchgesetzt. Deshalb kannst du Vec benutzen, ohne selbst etwas beweisen zu müssen. Der Fehler, auf den es ankommt, heißt Unsoundness und beschreibt genau den umgekehrten Fall: Ein Aufrufer kann mit ausschließlich sicherem Code undefiniertes Verhalten auslösen. Dann ist nicht der Block kaputt, sondern die Hülle, und der Schaden trifft Leute, die sich auf die Signatur verlassen haben.

Praktisch heißt das: unsafe gehört in das kleinste Modul, das die Invariante noch vollständig kontrolliert, und über jedem Block steht ein SAFETY-Kommentar, der die Bedingung nennt und begründet, warum sie hier gilt. Clippy hilft dabei in zwei Stufen. missing_safety_doc warnt bereits im Standard, wenn eine öffentliche unsafe-Funktion keinen Abschnitt Safety in der Dokumentation hat. undocumented_unsafe_blocks verlangt den Kommentar an jedem einzelnen Block, gehört aber zur restriction-Gruppe und muss ausdrücklich eingeschaltet werden. In einem Projekt mit ernstzunehmendem unsafe-Anteil lohnt sich das, weil der Kommentar im Review die eigentliche Prüfgrundlage ist.

Für den Nachweis gibt es Miri. cargo miri test führt die Tests in einem Interpreter aus, der Zugriffe außerhalb der Grenzen, Zugriffe auf freigegebenen Speicher, falsch ausgerichtete Zeiger, ungültige Werte und Verstöße gegen die Aliasregeln meldet, also genau die Fälle, die im normalen Testlauf durchgehen, weil der Optimierer zufällig gnädig war. Zwei Grenzen musst du kennen: Miri braucht die Nightly-Werkzeugkette und läuft um Größenordnungen langsamer, und er kann Code nicht ausführen, der echte Hardware anspricht oder in fremde Bibliotheken springt. Er deckt den rechnenden Kern ab, nicht den Treiber.

Die Regel im Team, die tatsächlich hält

Der wirksamste Riegel steht in einer einzigen Zeile: #![forbid(unsafe_code)] ganz oben in der lib.rs. Entscheidend ist forbid statt deny, denn ein deny lässt sich mit einem #[allow(unsafe_code)] an der Stelle wieder aufheben, wo es stört, und genau das passiert unter Zeitdruck. Ebenso entscheidend ist der Zuschnitt: Die Sperre gehört crate-weise gesetzt, nicht projektweit. Die Anwendungslogik, die Ableitung von Datenmodellen und die Weboberfläche bekommen sie, die eine Crate, die die C-Bibliothek einbindet, bekommt sie nicht. Wer sie überall setzt, entfernt sie beim ersten echten Bedarf wieder überall.

Für den Rest braucht es zwei Vereinbarungen, die klein genug sind, um eingehalten zu werden. Erstens: Ein Pull Request, der unsafe hinzufügt, enthält nichts anderes. Zweitens: Er bekommt eine zweite gegenlesende Person, deren Aufgabe genau eine ist, nämlich den SAFETY-Kommentar gegen die tatsächlichen Aufrufer zu halten. Nicht die Frage "sieht das richtig aus", sondern die Frage "kann irgendein Aufrufer diese Bedingung verletzen, ohne selbst unsafe zu schreiben". Wenn die Antwort ja lautet, ist die Signatur falsch und die Funktion muss selbst unsafe werden.

In der Pipeline reicht danach wenig: clippy mit den beiden Lints auf Fehlerniveau, Miri für die Crates, in denen es läuft, und die Editionsprüfung, die euch beim Anheben ohnehin alle Altlasten anzeigt. Eine zusätzliche Textsuche nach dem Wort unsafe braucht ihr nicht, das erledigt der Lint zuverlässiger.

Was eine Abhängigkeit erfüllen muss, bevor sie hereinkommt

Fünf Fragen reichen, wenn sie jedes Mal gestellt werden. Wann erschien die letzte Veröffentlichung? Wie viele Personen haben das Recht, unter diesem Namen zu veröffentlichen? Welche Lizenz steht drauf, und passt sie zu der Art, wie ihr ausliefert? Wie viele weitere Crates zieht sie in den Baum? Und gibt es etwas Kleineres, das denselben Zweck erfüllt, notfalls dreißig selbst geschriebene Zeilen? cargo tree beantwortet die vierte Frage in Sekunden, und der Unterschied zwischen einer Crate ohne weitere Abhängigkeiten und einer, die vierzig mitbringt, ist eine andere Entscheidung, auch wenn beide dasselbe können.

Übersprungen wird meistens die zweite Frage. Ein Paket mit Millionen Abrufen, einer einzigen veröffentlichungsberechtigten Person und ohne Veröffentlichung seit zwei Jahren ist ein Risiko, das ihr bewusst annehmen könnt, aber nicht versehentlich. Die RustSec-Datenbank kennt dafür eine eigene Meldungsart neben den Sicherheitslücken: Einträge zu nicht mehr gepflegten Crates und Einträge zu Unsoundness, also zu Bibliotheken, deren sichere Schnittstelle die eigene Zusicherung nicht hält. Beide führen zu einem Fehlschlag im Werkzeug, obwohl keine ausnutzbare Lücke bekannt ist, und beide sind der frühere und nützlichere Hinweis.

Die Begründung gehört in den Pull Request, in dem der Eintrag in der Cargo.toml auftaucht. Ein halbes Jahr später will jemand wissen, warum diese Crate und nicht die andere, und die einzige Quelle, die dann noch zuverlässig auffindbar ist, ist die Versionsgeschichte.

cargo-audit und cargo-deny, und was sie unterschiedlich machen

cargo audit liest die Cargo.lock und gleicht jede darin festgeschriebene Version gegen die RustSec Advisory Database ab, die von der Secure Code Working Group gepflegt wird. RustSec liefert seine Einträge in die OSV-Datenbank, die GitHub Advisory Database und den Rust-Sicherheitstracker von Debian, sodass dieselbe Meldung euch aus mehreren Richtungen erreicht. Der Grund, den Befehl trotzdem selbst zu fahren, ist die Sperrdatei: Sie enthält die Versionen, die tatsächlich gebaut werden, und die sehen die Scanner der Plattform nicht immer.

cargo deny ist kein Ersatz, sondern vier getrennte Prüfungen mit einer Konfiguration in der deny.toml. advisories nutzt dieselben Daten wie cargo-audit. bans meldet mehrere Versionen derselben Crate im Baum und Crates, die ihr ausdrücklich ausschließt. licenses arbeitet gegen eine Positivliste und schlägt auch dann fehl, wenn das Lizenzfeld unklar ist. sources begrenzt, aus welchen Registries und Git-Quellen überhaupt gezogen werden darf. Der Lizenzteil ist der, der sich im Unternehmen am schnellsten bezahlt macht, weil er aus einem "da müsste mal jemand draufschauen" einen roten Build macht.

Beim Takt machen viele denselben Fehler: Sie prüfen nur bei Änderungen. Eine neue Meldung entsteht aber, ohne dass in eurem Repository eine Zeile anders wird. Deshalb läuft die Prüfung bei jedem Pull Request und zusätzlich einmal täglich gegen den Hauptzweig. Für ausgelieferte Stände gibt es cargo auditable: Der Befehl bettet die Liste der verbauten Crates in die Binärdatei ein, sodass sich ein Artefakt Monate später noch gegen die aktuelle Datenbank prüfen lässt, ohne dass jemand den passenden Quellstand suchen muss.

Dazu passende Kurse

Wer rohe Zeiger und Fremdschnittstellen regelmäßig anfasst, sollte die Regeln nicht aus Fehlermeldungen ableiten müssen, dafür gibt es Rust-Kurse für sicherheitskritischen Code .

Die Aufnahmeprüfung für fremde Abhängigkeiten ist kein Rust-Thema, sie gehört in denselben Ablauf wie jede andere Freigabe, und dabei helfen Kurse zu sicherer Softwareentwicklung .

Wissen prüfen

Wie sicher bist du beim Thema wirklich?

Lesen fühlt sich schnell nach Können an. Ein kurzer Test zeigt dir, was davon schon sitzt und wo sich ein Kurs lohnt. Kostenlos, ohne Anmeldung, mit einer Erklärung zu jeder Antwort.

Sehr angenehmes, gut vorbereitetes Seminar, kann ich nur empfehlen.
Rust Fortgeschrittenenkurs

Häufige Fragen

Schaltet unsafe den Borrow-Checker ab?
Nein. Referenzen, Lebensdauern und Typprüfung gelten im Block unverändert. unsafe gibt fünf Operationen frei, die der Compiler danach nicht mehr auf Speichersicherheit prüft, und verschiebt für genau diese fünf die Beweislast zu dir. Das Rust-Buch schreibt das ausdrücklich so, und die Verwechslung ist der Grund für die meisten Fehlentscheidungen zu diesem Thema, in beide Richtungen.
Sollten wir unsafe im Projekt komplett verbieten?
In den Crates, die keine Hardware und keine fremde Bibliothek anfassen, ja, und zwar über #![forbid(unsafe_code)] statt über eine Absprache. Ein Verbot über das ganze Repository dagegen kostet euch jede FFI-Anbindung und jeden Registerzugriff, und es ändert nichts an dem unsafe, das über die Abhängigkeiten hereinkommt. Genau dort sitzt der größere Teil.
Wie finde ich heraus, wie viel unsafe in unseren Abhängigkeiten steckt?
Die Zahl der Blöcke ist die falsche Kennzahl, denn sie sagt nichts darüber, ob die Hülle darum hält. Nützlicher ist der Zuschnitt: cargo tree zeigt, welche Crates überhaupt im Baum liegen, und darunter markierst du die Klassen, in denen unsafe unvermeidlich ist, also Parser für fremde Formate, FFI-Hüllen und sperrfreie Datenstrukturen. Für genau diese schaust du auf Wartungsstand und Zahl der Betreuer, den Rest deckt cargo-audit ab.
Brauchen wir cargo-audit und cargo-deny nebeneinander?
In der Pipeline reicht cargo-deny, weil es dieselben Meldungen mitprüft und zusätzlich Lizenzen, Dubletten und Bezugsquellen abdeckt. cargo-audit behältst du aus zwei Gründen: Es ist der kürzere Befehl für die schnelle Prüfung auf dem eigenen Rechner, und zusammen mit cargo auditable prüfst du damit ein fertiges Artefakt, statt den passenden Quellstand suchen zu müssen.

Passt thematisch dazu

Auf der JavaScript-Seite stellt sich dieselbe Frage nach Prüfung und Zuständigkeit noch einmal, und dort hilft ein fester Rhythmus für npm-Aktualisierungen gegen Sprünge, die sich über Monate zu einem eigenen Vorhaben auftürmen.

Persönlich für dich da

Deine Ansprechpartner

Du bist dir nicht sicher, welcher Kurs oder welches Level zu dir passt? Wir beraten dich persönlich und kostenlos.

Yves Hoppe

Yves Hoppe

Weiterbildung & Beratung

Hilft dir, aus dem Rust-Programm den passenden Kurs für deinen Stand zu finden.

Norbert Jansen

Norbert Jansen

Beratung & Inhouse

Plant mit dir Inhouse-Trainings, die auf eure Abläufe und euren Datenbestand zugeschnitten sind.

unsafe gehört an die wenigen Stellen, an denen ihr es begründen könnt

Wo diese Stellen in einem echten Projekt liegen und wie eine Hülle aussieht, die auch der nächste Aufrufer nicht brechen kann, arbeitest du bei cmt an eigenem Code durch statt an Buchbeispielen.