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
1 Kommentare
Hacker-News-Kommentare
Als Maintainer von GNU Coreutils fand ich den Artikel interessant, aber in dem bisschen Rust, das ich verwendet habe, war es viel zu einfach, mit
std::fseinen TOCTOU-Race zu erzeugenIch hoffe, dass am Ende eine
openat-ähnliche API in die Standardbibliothek aufgenommen wirdAußerdem stimme ich der Regel „vor dem Vergleichen von Pfaden auflösen“ nicht zu
Im Allgemeinen ist es besser,
fstataufzurufen undst_devsowiest_inozu vergleichen; der Artikel hatte das teilweise auch erwähntEin seltener bedachter Nebeneffekt sind die Performance-Kosten
In einem realen Beispiel brauchte
cpbei einem sehr tiefen Verzeichnispfad 0,010 Sekunden,uu_cpdagegen 12,857 SekundenIn der Praxis erzeugt man solche Pfade selten absichtlich, aber GNU-Software bemüht sich sehr stark darum, willkürliche Grenzen zu vermeiden
https://www.gnu.org/prep/standards/standards.html#Semantics
Und im Artikel hieß es, der Rust-Rewrite habe im selben Zeitraum 0 Memory-Safety-Bugs gehabt, aber das stimmt nicht :)
https://github.com/advisories/GHSA-w9vv-q986-vj7x
Stimmt,
std::fshat ein Lowest-Common-Denominator-ProblemIn Rust 1.0 musste irgendetwas hinein, und leider hat sich dieser Zustand lange verfestigt
Ich denke,
uutilsist ein guter Ort, um eine alternative API zu std::fs zu entwerfen, bei der man schwerer Fehler machtDanke, dass du diese Perspektive von der anderen Seite so knapp erklärt hast
Ich würde gern fragen, was man hier eigentlich lernen sollte
Für einen Internetbeitrag frage ich absichtlich ziemlich aggressiv, weil Kontrast hilft, Unterschiede und Fehler klarer zu sehen
Natürlich bist du überhaupt nicht verpflichtet, dafür Zeit oder mentale Energie aufzubringen
Ich frage mich, warum ständig Geschwindigkeit, Performance, Race Conditions und
st_inogemeinsam auftauchenEs wirkt, als würden Latenz, tatsächliche Schreibvorgänge auf Speichermedien, Atomizität, ACID und die endliche Geschwindigkeit der Informationsübertragung am Ende auf ein ähnliches Wesen hinauslaufen
Zuverlässige Systeme wie Buchhaltung scheinen letztlich bei ACID zu landen, und unzuverlässige Systeme werden vielleicht so schnell vergessen, dass sich die Unterschiede zwischen Computern gar nicht so groß anfühlen
Außerdem frage ich mich, ob im Alltag Throughput wirklich wichtiger ist als Latenz
Und ich verstehe, warum wegen der Geschichte von C, Unix-artigen Betriebssystemen und GNU coreutils der Fokus auf Inode-Nummern liegt,
aber ich frage mich, wie das bei einem ganz einfachen Beispiel aussieht, etwa dabei, einen USB-Stick zum Speichern von Dateien einfach zuverlässig funktionieren zu lassen
und zwar ohne Komplexitäten wie
libc-I/O-Buffering,fflush, Kernel-Buffering, Multicore, Zeitscheibenbetrieb oder mehrere gleichzeitig laufende Anwendungen zu vermeidenIch bin kompletter Anfänger, aber ich habe mich gefragt, warum man nicht einfach mit
$(yes a/ | head -n $((32 * 1024)) | tr -d '\n')direktcdmacht und stattdessen einewhile-Schleife brauchtEdit: Verstanden. Wegen
-bash: cd: a/a/a/....../a/a/: File name too longFalls du es noch nicht gesehen hast: Es gibt eine Demo zur automatischen Umwandlung von GNU-Utilities wie
wgetin ein speichersicheres C++-Subsethttps://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget
Dabei werden gefährliche C-Elemente fast 1:1 durch sichere C++-Elemente mit entsprechendem Verhalten ersetzt, daher scheint die Wahrscheinlichkeit geringer, neue Bugs und neue Verhaltensunterschiede einzuführen als bei einem Rewrite
Wenn man den Quellcode nur leicht aufräumt, kann die Umwandlung vollständig automatisiert werden, sodass man im Build-Schritt aus dem ursprünglichen C-Quellcode ein etwas langsameres, aber speichersicheres Binary erzeugen kann
Vielleicht eine dumme Frage, aber ich frage mich, ob GNU Coreutils selbst einen Rust-Rewrite prüft oder plant
Rust konnten sie offenbar benutzen, aber mit Unix-APIs und ihrer Semantik sowie ihren Fallstricken waren sie nicht ausreichend vertraut
Die meisten dieser Fehler wären aus Sicht langjähriger GNU-coreutils-, BSD- oder Solaris-basierter Entwickler ziemlich klar Anfängerfehler
Viele dieser Probleme wurden schon vor Jahrzehnten aufgedeckt und aufgearbeitet, und im bestehenden Code gibt es zwar noch immer einen langen Schwanz an Fixes, aber inzwischen kommt größtenteils nur noch eine kleine Menge laufend hinzu
Ich war wirklich fassungslos, als ich den Canonical-Thread gelesen habe
Der Tenor war ungefähr: „Rust ist sicherer, Security hat höchste Priorität, also ist die Auslieferung eines vollständigen coreutils-Rewrites dringend. Wenn etwas kaputtgeht, ist das okay und man fixt es später.“
Ich möchte keinen Code auf meiner Maschine ausführen, der von Leuten mit so einer Denkweise stammt
Ich bin auch für Rust, aber dass Rust sicherer ist, gilt nur wenn alles andere gleich ist
Hier ist alles andere überhaupt nicht gleich
Ein Rewrite wird zwangsläufig deutlich mehr Bugs und Schwachstellen haben als Code, der über Jahrzehnte gepflegt wurde; daher taugt das Sicherheitsargument vielleicht für eine langfristige Migrationsstrategie, aber nicht als Begründung für einen überhasteten Rollout
Nach der Auslieferung die Auswirkungen auf Nutzer kleinzureden oder zu sagen „so werden Bugs eben sichtbar“ oder „die bisherigen coreutils hatten auch keine ordentlichen Tests“ ist viel zu verantwortungslos
Nutzer sind keine Versuchskaninchen
Meiner Ansicht nach haben Maintainer eine moralische Verantwortung, die Zuverlässigkeit der Systeme ihrer Nutzer nicht zu gefährden
Noch grundlegender scheint es so, als würde die Rust-Standardbibliothek Entwickler mit einer sauberen API auf die falsche Abstraktionsebene lenken
zum Beispiel hin zu pfadbasierten statt handlebasierten Dateioperationen
Ich hoffe, ich liege falsch
Für mich besteht der Sinn von Rust darin, die größten und naheliegendsten Fallstricke so weit zu entschärfen, dass man sich nicht ständig um sie kümmern muss
Die Kernaussage des Artikels scheint letztlich zu sein, dass Dateisystem-APIs genau diese Rolle übernehmen sollten
Jemand hat dafür einmal einen ähnlichen Ausdruck geprägt: disassembler rage
Gemeint ist, dass jeder Fehler amateurhaft aussieht, wenn man nur nah genug hinschaut
Der Ausdruck kommt auch aus der Haltung, nur auf den Disassembler zu starren und dann einen High-Level-Programmierer zu beschimpfen, weil er in einer Funktion 100 Frames tief im Call Stack
ifstattswitchverwendet hatIm Moment sehen wir nur ein paar Dinge, die sie falsch gemacht haben, und fast nichts von den Tausenden Zeilen korrekt geschriebenen Codes darum herum
Dass solche Utilities mit
panicabstürzen, ist selbst nach Rust-Maßstäben ein ziemlich amateurhafter FehlerBei etwas wie einem nicht behandelbaren Alloc-Fehler vielleicht noch verständlich, aber
expectundunwrapsind schwer zu entschuldigen, wenn man nicht extrem streng sicherstellt, dass dieser Codepfad niemals ausgeführt werden kannEine Schwierigkeit beim Umschreiben von Code besteht darin, dass der ursprüngliche Code im Lauf der Zeit schrittweise verformt wurde, indem er auf Probleme reagiert hat, die nur in realen Betriebsumgebungen sichtbar wurden
Die dabei gewonnenen Lehren sickern still in den Code ein, und wenn sie nicht dokumentiert sind, gibt es enorm viel versteckte Arbeit, bevor man ein gleichwertiges Niveau erreicht
Der Originaltext zeigt genau so eine Art Liste sehr gut
Bevor man also sofort „amateurhaft“ sagt, sollte man auch sehen, dass dies eines der softwaretypischsten Phänomene überhaupt ist
Wenn es nicht so war, dass für coreutils bereits hervorragende technische Dokumentation und Tests für diese Fälle existierten und trotzdem ignoriert wurden, dann war so etwas fast unvermeidlich
Ein gutes Beispiel aus dem Artikel ist das chroot + NSS-CVE
Dass NSS dynamisch ist und innerhalb eines
chrootBibliotheken perdlopenlädt, steht nirgends auffällig vermerktDas ist eher so etwas, das Systemadministratoren über mehr als 25 Jahre durch leidvolle Erfahrung gelernt haben, und ein Cleanroom-Rewrite lernt es meist erneut als neues CVE
Selbst wenn man denselben Code mit einem LLM portiert, wäre die Lage ähnlich
Funktionssignaturen kann man lesen, aber was man wirklich braucht, sind die Verletzungen und Narben, die im Code zurückgeblieben sind
Wenn man diese Arbeit macht, ohne aus Angst vor der GPL überhaupt den Originalquelltext zu lesen, wird es noch schwieriger
Meiner Meinung nach wäre
uutilsviel besser dran gewesen, wenn es GPL-lizenziert gewesen wäre und sich direkt vom coreutils-Originalquelltext hätte inspirieren lassen dürfenMan sollte aber auch klar sagen, dass es eine schlechte Praxis ist, solche Lehren oder zumindest die absichtlich vermiedenen Bugs und Schwachstellen nicht zu dokumentieren
Natürlich ist es schwierig, jeden einzelnen Bug zu dokumentieren, den man von vornherein durch guten Code implizit vermeidet,
aber für künftige Leser ist es wichtig, Erklärungen zu hinterlassen wie: „Hier wird
foostattbarverwendet, weilbarunter Bedingung ABC ein gefährlichesbazwegen XYZ erzeugt“Auch wenn das wie ein gewisser Aufwand an Zeit und Dokumentationsraum wirkt, ist es meiner Meinung nach besser so
Vieles von dem, was dieser Artikel aufzeigt, hätte meiner Meinung nach, besonders im Vergleich mit dem GNU-coreutils-Quellcode, in halbwegs ordentlichen Unit-Tests oder einer manuellen Review auffallen müssen
Ein coreutils-Rewrite wirkt wie eine schreckliche Idee
https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
und scheint auf die falsche Weise vorangetrieben worden zu sein, ohne genügend von dem Wissen mitzunehmen, das sich in der bestehenden Software angesammelt hat
Wenn man einen Rewrite macht, muss man den Vorgänger vollständig verstehen und von ihm lernen
Sonst wiederholt man dieselben Fehler, und ehrlich gesagt ist das ziemlich peinlich
Um das klarzustellen: Ich mag Rust, benutze es in mehreren Projekten, und es ist großartig
Rust rettet einen nur eben nicht vor schlechter Ingenieursarbeit
Interessanterweise verwendet
uutilsdie GNU-coreutils-TestsuiteErgänzend dazu ist dort auch ausdrücklich festgehalten, dass keine Beiträge angenommen werden, die beim Schreiben GPL-Quellcode gelesen haben
Von der Seite, die
unity,upstartundsnapgemacht hat, liegt so etwas durchaus im erwartbaren BereichNeue Systemprogrammierer sollte man vielleicht so begrüßen:
Unix ist kaputt, am Ende muss man selbst hässliche und didaktisch wenig hilfreiche Workarounds schreiben, und man muss empirisch testen
So funktionieren vertrauenswürdige Software und gute Softwareentwicklung nun einmal
Ich frage mich, warum differential fuzzing solche Bugs nicht gefunden hat
https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz
Wenn man einen Pfad einmal per Syscall prüft und dann mit einem zweiten Syscall auf denselben Pfad die eigentliche Aktion ausführt, führt dieses Muster immer zum gleichen Problem
Ein Angreifer mit Schreibrechten auf das Elternverzeichnis kann in der Zwischenzeit Pfadkomponenten durch symbolische Links austauschen, und der Kernel löst den Pfad beim zweiten Aufruf wieder von vorne auf, sodass die privilegierte Operation auf ein vom Angreifer gewähltes Ziel geht
Ein Angreifer mit Schreibrechten auf das Elternverzeichnis kann auch mit Hardlinks Unfug treiben
Selbst wenn er nur reguläre Dateien manipulieren kann, gibt es praktisch kaum vernünftige Gegenmaßnahmen
Ein Beispiel findet sich hier: https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml
Die Grundursache einiger Bugs scheint zu sein, dass Unix-APIs zu intransparent sind
Dass etwa
get_user_by_namein einem neuen Root-Dateisystem Shared Libraries lädt, um einen Benutzernamen aufzulösen, und dadurch ein Angreifer, der Dateien in dieseschrooteinbringen kann, Code als uid 0 ausführen kann, wirkt fast wie eine Booby TrapDass eine Funktion zum Abrufen von Nutzerdaten plötzlich auch Shared Libraries lädt, wirkt wie ein Design mit vermischten Zuständigkeiten
Meiner Meinung nach sollte man Benutzerdatenabfrage und Bibliotheksladen auf Funktionsebene trennen oder zumindest schon am Namen erkennen lassen, dass so ein Verhalten stattfindet
Teilweise mag das stimmen, aber wenn man coreutils von Grund auf neu schreibt, gehört das Verständnis der POSIX-API buchstäblich zum Kern der Aufgabe
Und wenn der Code zur Prüfung, ob ein Pfad auf die Root des Dateisystems zeigt,
file == Path::new("/")war, dann ist das kein API-ProblemWer so etwas schreibt, ist für die Mitarbeit an diesem Projekt fast nicht geeignet
Im Gegenteil glaube ich, dass funktionale sichere Sprachen einen glauben lassen können, die Daten, mit denen man arbeitet, seien zustandslos
Im Betriebssystem ändert sich jedoch sehr vieles ständig
Bis Dateisysteme mit Snapshots verfügbar sind, muss man alles immer wieder neu prüfen
Letztlich braucht man eine API, die bei Eingabe entweder ein erfolgreiches Ergebnis oder einen Fehlschlag liefert
nicht eine API, die eines von Erfolg, Fehlschlag oder Fehler zurückgibt
Stimmt,
musl libcentfernt genau so einen AspektDie Grundursache ist meiner Ansicht nach weniger die Intransparenz der Unix-API als vielmehr, dass nicht ordentlich bedacht wurde, was es bedeutet, wenn root in ein Verzeichnis chrootet, das er nicht kontrolliert
Alles, worauf
chrootangewendet wird, steht unter der Kontrolle der Seite, die dieses chroot bereitstellt, und wer das nicht versteht, solltechroot()nicht verwendenget_user_by_namemag sich wie eine Falle anfühlen, aber praktisch gibt es kaum einen Unterschied zwischennewroot/etc/passwdund Dingen wienewroot/usr/lib/x86_64-linux-gnu/libnss_compat.soodernewroot/bin/shDeshalb finde ich, dass
/usr/sbin/chrootgar keinen Grund haben sollte, eine Benutzer-ID nachzuschlagentoybox chroottut das auch nichtDer eigentliche Bug war also nicht die Art, wie etwas getan wurde, sondern dass es überhaupt getan wurde
Unix und POSIX sind fraktal: Wohin man auch schneidet, überall lauern Fallen
Selbst wenn man annimmt, dass Leute aus der Rust-Ecke coreutils ohne Linux-Erfahrung neu geschrieben haben, verstehe ich noch weniger, wie Ubuntu das in die mainline aufgenommen hat
Ubuntu scheint fast bei jeder Release die Richtlinie zu haben, irgendein Basiselement des Systems durch ein schlampiges, unvollständiges Experiment zu ersetzen
Genau das ist hier der Kern, nicht etwa „Oh nein, im Rust-Code gab es Bugs“
Das Original steht unter GPL-Lizenz, der Rewrite unter MIT-Lizenz
Wenn die Aussage stimmt, dass „diese Bugs aus tatsächlich ausgeliefertem Rust-Code stammten und die Autoren wussten, was sie tun“,
dann frage ich mich, ob das bedeutet, dass die ursprünglichen Utilities keine Test-Harnesses hatten und der Rewrite auch nicht damit begonnen hat, solche zuerst zu bauen
Selbst wenn es viele Edge Cases gibt, sollte man OS und FS doch bis zu einem gewissen Grad abstrahieren können, um etwa zu prüfen, dass
rm .//tatsächlich nicht wie erwartet das aktuelle Verzeichnis löschtDas wirkt weniger wie schmutziges Coding oder Sprachkritik und mehr wie schon wieder diese alte Haltung, dass man im System Programming nicht testet
Falls die ursprünglichen Utilities andererseits Tests hatten und trotzdem so viele Lücken blieben, könnte auch die ursprüngliche Testsuite selbst stark unzureichend sein
Davon gehe ich aus
Aber ich bin mir nicht so sicher, dass man OS und FS ausreichend abstrahieren kann, um das wirklich zu verifizieren
Leute versuchen so etwas offenbar schon seit vor meiner Geburt, und erfolgreich war das bisher wohl noch nicht
Schon bei der Frage, wie viele zusätzliche
/man in einen Test packen soll, wird es unklarUnd noch weiter gedacht: Wenn
rmdas Löschen verweigern würde, sobald die ersten 9 Bytes einer Dateiimportantsind,wie sollte man auf einen Test kommen, der so ein Verhalten findet, wenn man diese Zeichenfolge vorher nicht kennt?
Noch schwieriger wird es, wenn dieses magische Wort nicht einmal im Wörterbuch steht
Ich habe kaum je jemanden ernsthaft sagen hören, „im System Programming testet man nicht“
Was ich dagegen oft höre, ist, dass Tests nicht immer die Rolle erfüllen, die Menschen von ihnen erwarten
Soweit ich verstanden habe, gab es im Entwicklungsprozess von
uutilsumfangreiche Vergleichstests des Verhaltens mit den Original-Utilities, und man versuchte sogar, Bugs beizubehaltenAus einem dieser Gründe deaktiviert Windows Symlinks standardmäßig
also nicht durch Abstraktion, sondern faktisch durch weitgehendes Entfernen der Funktion selbst
In Unix-artigen Systemen geht das nicht, weil es seit Jahrzehnten zu viel Software gibt, die auf Symlinks angewiesen ist
MacOS hat eine ähnliche Gegenmaßnahme
Zum Beispiel ist der
chroot()-Bug in der Standardkonfiguration praktisch kaum relevant, weil MacOSchroot()standardmäßig blockiertUm es zu verwenden, muss man System Integrity Protection deaktivieren
Das Grundproblem liegt in den scharfen Kanten der POSIX-API, und die Lösung besteht weniger darin, sie zu abstrahieren, als vielmehr darin, sie abzuschaffen
Ich finde es okay, wenn Leute experimentieren und unbeholfen Dinge ausprobieren
So lernt und wächst man nun einmal
Was mich wirklich interessiert, ist, wie die Entscheidungskette bei Ubuntu so kaputt sein konnte, dass so etwas bis in die Produktion gelangt ist