Fehler, die Rust nicht abfängt
(corrode.dev)- Speichersicherheit verbessert sich deutlich, aber selbst in produktivem Rust-Code bleiben Probleme bei der Behandlung von Systemgrenzen bestehen und können zu Schwachstellen führen
- Abläufe, bei denen derselbe Pfad über mehrere Syscalls hinweg erneut aufgelöst wird, Berechtigungen erst nach der Erstellung geändert werden oder stringbasierte Pfadvergleiche verwendet werden, führen leicht zu Problemen wie TOCTOU und Preisgabe von Rechten
- Unter Unix werden Pfade, Umgebungsvariablen und Stream-Daten als rohe Bytes verarbeitet; daher können
String-zentrierte Verarbeitung sowiefrom_utf8_lossy,unwrapundexpectzu Datenbeschädigung oder DoS führen - Wenn Fehler verworfen werden, kann ein Fehlschlag wie ein Erfolg aussehen; auch Verhaltensunterschiede zu GNU coreutils können in Shell-Skripten und privilegierten Werkzeugen unmittelbar zu Sicherheitsproblemen führen
- In diesem Audit wurden keine Bugs aus der Kategorie Speichersicherheit wie buffer overflow, use-after-free oder double-free gefunden; das verbleibende Hauptrisiko konzentrierte sich weniger auf Rust selbst als auf die Grenzen zur Außenwelt
Rusts Grenzen, wie sie im Audit sichtbar wurden
- Die von Canonical veröffentlichten 44 CVEs in uutils zeigen, dass auch in produktivem Rust-Code Schwachstellen verbleiben können, die borrow checker, clippy und cargo audit nicht erfassen
- Im Zentrum der Probleme stand weniger die Speichersicherheit als die Behandlung von Systemgrenzen
- Zwischen Pfad und Syscall gab es Zeitlücken
- Unix-Byte-Daten und UTF-8-Strings passten nicht zusammen
- Es gab Verhaltensunterschiede zum Originalwerkzeug
- Fehlerbehandlung fehlte oder endete in
panic!
- Diese CVE-Liste zeigt in komprimierter Form, wo Sicherheit in Rust endet
Wer einen Pfad zweimal auflöst, erzeugt TOCTOU
- Wenn derselbe Pfad in einem Syscall geprüft und in einem folgenden Syscall erneut verwendet wird, führt das leicht zu einer TOCTOU-Schwachstelle
- Zwischen den beiden Aufrufen kann ein Angreifer mit Schreibrechten auf das übergeordnete Verzeichnis Pfadbestandteile durch symbolische Links ersetzen
- Beim zweiten Aufruf löst der Kernel den Pfad von Anfang an neu auf, sodass eine privilegierte Operation auf ein vom Angreifer gewähltes Ziel gelenkt wird
- Die
std::fs-API von Rust setzt standardmäßig auf erneute Auflösung auf Basis von&Path, was solche Fehler leicht machtfs::metadata,File::create,fs::remove_fileundfs::set_permissionslösen den Pfad bei jedem Aufruf erneut auf- Für privilegierte Werkzeuge, die lokale Angreifer abwehren müssen, ist dieser Standardpfad riskant
- Bei
CVE-2026-35355wurde ein Ablauf ausgenutzt, bei dem nach dem Löschen einer Datei unter demselben Pfad eine neue Datei erstellt wurde- In
src/uu/install/src/install.rsfolgte auffs::remove_file(to)?direktFile::create(to)? - Wenn
tozwischen Löschen und Erstellen in einen symbolischen Link auf ein Ziel wie/etc/shadowgeändert wird, kann ein privilegierter Prozess diese Datei überschreiben
- In
- Die Korrektur verwendet nun
OpenOptions::create_new(true), um nur neue Dateien zu erstellen- Laut Dokumentation akzeptiert
create_newam Zielort weder vorhandene Dateien noch dangling symlinks
- Laut Dokumentation akzeptiert
- Wenn auf demselben Pfad zweimal gearbeitet werden muss, ist es sicherer, am Dateideskriptor festzumachen
- Außer beim Erzeugen neuer Dateien sollte man das Elternverzeichnis einmal öffnen und dann relativ zu diesem Handle arbeiten
- Wenn auf demselben Pfad zweimal gearbeitet wird, sollte das als TOCTOU gelten, bis das Gegenteil bewiesen ist
Berechtigungen nicht nachträglich ändern, sondern beim Erstellen festlegen
- Auch ein Ablauf, bei dem ein Verzeichnis oder eine Datei mit Standardrechten erzeugt und später per
chmodgeändert wird, schafft ein kurzes Offenlegungsfenster- Wenn man etwa
fs::create_dir(&path)?und danachfs::set_permissions(&path, Permissions::from_mode(0o700))?schreibt, existiertpathdazwischen mit Standardrechten - Andere Nutzer können in diesem Zeitfenster
open()ausführen, und auch ein spätereschmodzieht bereits erhaltene Dateideskriptoren nicht zurück
- Wenn man etwa
- Berechtigungen sollten im Moment der Erstellung mitgesetzt werden
- Dafür sollten
OpenOptions::mode()undDirBuilderExt::mode()verwendet werden, damit das Objekt mit den gewünschten Rechten entsteht - Der Kernel wendet zusätzlich die
umaskan; wenn deren Einfluss wichtig ist, muss auch sie explizit behandelt werden
- Dafür sollten
Stringvergleich von Pfaden ist keine Dateisystem-Identität
- Die anfängliche
--preserve-root-Prüfung vonchmodmachte nur einen Stringvergleichrecursive && preserve_root && file == Path::new("/")- Eingaben, die tatsächlich auf das Wurzelverzeichnis zeigten, aber als String nicht
/waren, konnten die Prüfung umgehen, etwa/../,/./,/usr/..oder symbolische Links auf/
- Die Korrektur vergleicht nun nach Auflösung des Pfads in seinen tatsächlichen absoluten Pfad per
fs::canonicalize- PR mit der Korrektur
canonicalizeliefert den tatsächlichen Pfad zurück, in dem..,.und symbolische Links aufgelöst sind
- Im Fall von
--preserve-rootfunktioniert das, weil/kein Elternverzeichnis hat - Wenn man allgemein prüfen will, ob zwei beliebige Pfade auf dasselbe Dateisystemobjekt zeigen, sollte man nicht Strings, sondern
(dev, inode)vergleichen- GNU coreutils macht es ebenso
- Bei
CVE-2026-35363wiesrmzwar.und..zurück, akzeptierte aber./und.///, sodass das aktuelle Verzeichnis gelöscht werden konnte- Wenn Unterschiede in der Eingabeform nur auf Stringebene behandelt werden, lassen sich Prüfungen leicht umgehen
An Unix-Grenzen Bytes vor Strings behandeln
- Rusts
Stringund&strsind immer UTF-8, aber Pfade, Umgebungsvariablen, Argumente und Stream-Daten unter Unix liegen in einer Welt aus rohen Bytes - Die falsche Wahl beim Überschreiten dieser Grenze führt zu zwei Arten von Bugs
- Verlustbehaftete Konvertierungen wie
from_utf8_lossyersetzen ungültige Bytes durchU+FFFDund beschädigen Daten stillschweigend - Strikte Konvertierungen wie
unwrapoder?können Eingaben zurückweisen oder den Prozess beenden
- Verlustbehaftete Konvertierungen wie
CVE-2026-35346incommwar ein Fall beschädigter Ausgabe durch verlustbehaftete Konvertierung- In
src/uu/comm/src/comm.rswurden die EingabebytesraundrbmitString::from_utf8_lossyumgewandelt und dann perprint!ausgegeben - GNU
commreicht Bytes auch bei Binärdateien unverändert durch, aber uutils ersetzte ungültiges UTF-8 durchU+FFFDund beschädigte die Ausgabe - Die Korrektur bestand darin, rohe Bytes mit
BufWriterundwrite_allunverändert nachstdoutzu schreiben
- In
print!erzwingt überDisplayeinen UTF-8-Roundtrip,Write::write_alldagegen nicht- In Systemcode für Unix sollte man je nach Kontext den passenden Typ verwenden
- Wenn man der Formatierungsbequemlichkeit wegen über
Stringgeht, schleicht sich Datenbeschädigung leicht ein
Jedes panic kann zu Denial of Service führen
- In CLI-Programmen können
unwrap,expect, Slice-Indexing, ungeprüfte Arithmetik undfrom_utf8zu DoS-Stellen werden, wenn Angreifer die Eingabe steuern könnenpanic!unwound den Stack und beendet den Prozess- Läuft das Programm in einem cron job, einer CI pipeline oder einem shell script, kann dadurch die gesamte Aufgabe stoppen
- In Umgebungen mit wiederholten Neustarts kann eine Crash-Schleife sogar das ganze System lähmen
- Bei
CVE-2026-35348insort --files0-frombrach das Programm ab, sobald es in einer NUL-getrennten Dateinamenliste auf einen nicht UTF-8-kodierten Dateinamen traf- Der Parser rief für die Bytes jedes Namens
std::str::from_utf8(bytes).expect(...)auf - GNU
sortbehandelt Dateinamen wie der Kernel als rohe Bytes, uutils erzwang jedoch UTF-8 und beendete beim ersten nicht-UTF-8-Pfad den gesamten Prozess
- Der Parser rief für die Bytes jedes Namens
- In Code, der nicht vertrauenswürdige Eingaben verarbeitet, sollten
unwrap,expect, Indexing undas-Casts als potenzielle CVEs gelten- Stattdessen sollte man
?,get,checked_*undtry_fromverwenden und echte Fehler an den Aufrufer weiterreichen
- Stattdessen sollte man
- Vorgeschlagene clippy-Regeln für die CI sind:
unwrap_usedexpect_usedpanicindexing_slicingarithmetic_side_effects
- In Testcode können solche Warnungen zu streng sein; sinnvoll ist daher eine Begrenzung im Bereich
cfg(test)
Wenn Fehler verworfen werden, sieht Scheitern wie Erfolg aus
- Einige CVEs entstanden dadurch, dass Fehler ignoriert wurden oder Fehlerinformationen verloren gingen
chmod -Rundchown -Rgaben über den gesamten Lauf hinweg nur den Exit-Code der letzten Datei zurück- Selbst wenn die Verarbeitung vieler früherer Dateien fehlschlug, konnte das Programm mit
0enden, wenn die letzte Datei erfolgreich war - Skripte konnten dann fälschlich annehmen, die gesamte Operation sei erfolgreich abgeschlossen worden
- Selbst wenn die Verarbeitung vieler früherer Dateien fehlschlug, konnte das Programm mit
ddrief zur Nachbildung des GNU-Verhaltens bei/dev/nullauf dem Ergebnis vonset_len()die MethodeResult::ok()auf- Gedacht war das, um in einem begrenzten Spezialfall Fehler zu verwerfen, aber derselbe Code galt auch für normale Dateien
- Selbst bei voller Festplatte konnte so still eine nur halb geschriebene Zieldatei zurückbleiben
- Wenn
Resultper.ok(),.unwrap_or_default()oderlet _ =verworfen wird, geht oft die entscheidende Ursache eines Fehlschlags verloren - Auch wenn man nicht beim ersten Fehler sofort abbricht, sollte man sich zumindest den schwersten Fehlercode merken und damit beenden
- Wenn ein
Resultwirklich verworfen werden muss, sollte im Code vermerkt sein, warum dieser Fehler sicher ignoriert werden kann
Exakte Kompatibilität zum Originalwerkzeug ist ebenfalls eine Sicherheitsfunktion
- Mehrere CVEs entstanden nicht dadurch, dass gefährliche Operationen ausgeführt wurden, sondern weil sich das Programm anders als GNU verhielt
- Reale Shell-Skripte hängen vom Verhalten des ursprünglichen GNU-Tools ab, und semantische Unterschiede können zu Sicherheitsproblemen werden
- Ein typisches Beispiel ist
CVE-2026-35369beikill -1- GNU interpretiert
-1als signal 1 und verlangt zusätzlich eine PID - uutils deutete dies als Senden des Standard-Signals an PID -1
- Unter Linux bedeutet PID -1 alle sichtbaren Prozesse, sodass ein einfacher Tippfehler ein Kill des gesamten Systems auslösen kann
- GNU interpretiert
- Bei neu implementierten Werkzeugen ist bug-for-bug-Kompatibilität eine Schutzmaßnahme, die Exit-Codes, Fehlermeldungen, Edge Cases und die Bedeutung von Optionen einschließt
- An jeder Stelle mit anderem Verhalten gegenüber GNU steigt die Wahrscheinlichkeit, dass Shell-Skripte falsche Entscheidungen treffen
- uutils führt nun in der CI auch die upstream GNU coreutils testsuite aus
- Das wirkt als passende Verteidigung gegen genau solche Abweichungen
Vor dem Überschreiten einer Vertrauensgrenze zuerst auflösen
CVE-2026-35368war eine local root code execution inchroot- Das Muster des Problems war, dass nach
chroot(new_root)?ein Benutzername innerhalb der vom Angreifer kontrollierten neuen Root aufgelöst wurdeget_user_by_name(name)?las zur Namensauflösung Shared Libraries aus dem Dateisystem der neuen Root- Wenn ein Angreifer innerhalb des chroot Dateien platzierte, konnte das zu Codeausführung mit uid 0 führen
- GNU
chrootlöst den Benutzer vor demchrootauf- Die Korrektur änderte die Reihenfolge entsprechend
- Sobald man eine Vertrauensgrenze überschritten hat, kann praktisch jeder Bibliotheksaufruf Angreifercode ausführen
- Auch statisches Linken verhindert dieses Problem nicht
get_user_by_nameläuft über NSS und kann zur Laufzeitlibnss_*-Module perdlopenladen
Welche Bugs Rust tatsächlich verhindert hat
- Es ist ebenso klar, welche Bug-Arten im Audit nicht gefunden wurden
- Kein buffer overflow
- Kein use-after-free
- Kein double-free
- Keine data race in gemeinsam veränderbarem Zustand
- Keine null-pointer dereference
- Kein uninitialized memory read
- Auch wenn die Werkzeuge Bugs hatten, tauchten im Audit keine Fälle auf, die sich zu beliebigem Speicherlesen ausnutzen ließen
- GNU coreutils hat in den letzten Jahren fortlaufend solche CVE-Klassen der Speichersicherheit produziert
pwddeep path buffer overflownumfmtout-of-bounds readunexpand --tabsheap buffer overflowod --strings -NNUL-Schreiben außerhalb des heap buffersort1-Byte-Read vor dem heap buffersplit --line-bytesheap overwrite in CVE-2024-0684b2sum --checkread aus nicht alloziertem Speicher bei malformed inputtail -fstack buffer overrun
- Im Vergleich über denselben Zeitraum blieb die Rust-Neuimplementierung bei 0 Bugs dieser Kategorie
- Allerdings mit dem Vorbehalt, dass das Audit die Abwesenheit von Speichersicherheitsfehlern nicht bewiesen hat, sondern sie nur nicht gefunden wurden
- Die verbleibenden Probleme entstehen überwiegend nicht in Rust selbst, sondern an den Grenzen zur Außenwelt
- Pfade
- Bytes und Strings
- Syscalls
- Zeitlücken und Änderungen des Dateisystemzustands
Korrektes Rust ist auch idiomatisches Rust
- Idiomatisches Rust ist mehr als Code, der den borrow checker passiert und bei dem
clippyschweigt - Korrektheit muss ebenfalls Teil der Idiomatik sein
- Denn die Formen von Code, die in der Realität bestehen, haben sich aus der Erfahrung der Community heraus verfestigt
- Robuste Systeme sollten die Unordnung der Realität nicht verbergen, sondern direkt abbilden
- Dateideskriptoren statt Pfaden
OsStrstattString?stattunwrap- bug-for-bug-Kompatibilität mit dem Original statt semantisch „sauberer“ wirkender Abweichungen
- Das Typsystem kann vieles ausdrücken, aber nicht Bedingungen außerhalb seiner Kontrolle wie den Zeitablauf zwischen zwei Syscalls
- Idiomatisches Rust sollte in Typen, Namen und Kontrollfluss die Wahrheit der Laufzeitumgebung sichtbar machen
- Selbst wenn das weniger elegant aussieht als schöner Whiteboard-Code, braucht es die ehrlichere Form
Referenzen
- An update on rust-coreutils: Veröffentlichung der Audit-Ergebnisse
- Patterns for Defensive Programming in Rust: Lesenswerte Muster für defensives Rust
- Pitfalls of Safe Rust: Häufige Fehler, die auch in safe Rust auftreten können
- Sharp Edges In The Rust Standard Library: Überraschendes Verhalten in
std - uutils/coreutils on GitHub: GNU coreutils in Rust neu implementiert
Noch keine Kommentare.