3 Punkte von GN⁺ 2026-04-30 | 1 Kommentare | Auf WhatsApp teilen
  • 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 sowie from_utf8_lossy, unwrap und expect zu 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 macht
  • Bei CVE-2026-35355 wurde 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.rs folgte auf fs::remove_file(to)? direkt File::create(to)?
    • Wenn to zwischen Löschen und Erstellen in einen symbolischen Link auf ein Ziel wie /etc/shadow geändert wird, kann ein privilegierter Prozess diese Datei überschreiben
  • Die Korrektur verwendet nun OpenOptions::create_new(true), um nur neue Dateien zu erstellen
    • Laut Dokumentation akzeptiert create_new am Zielort weder vorhandene Dateien noch dangling symlinks
  • 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
    Anzeige

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 chmod geändert wird, schafft ein kurzes Offenlegungsfenster
    • Wenn man etwa fs::create_dir(&path)? und danach fs::set_permissions(&path, Permissions::from_mode(0o700))? schreibt, existiert path dazwischen mit Standardrechten
    • Andere Nutzer können in diesem Zeitfenster open() ausführen, und auch ein späteres chmod zieht bereits erhaltene Dateideskriptoren nicht zurück
  • Berechtigungen sollten im Moment der Erstellung mitgesetzt werden
    • Dafür sollten OpenOptions::mode() und DirBuilderExt::mode() verwendet werden, damit das Objekt mit den gewünschten Rechten entsteht
    • Der Kernel wendet zusätzlich die umask an; wenn deren Einfluss wichtig ist, muss auch sie explizit behandelt werden

Stringvergleich von Pfaden ist keine Dateisystem-Identität

  • Die anfängliche --preserve-root-Prüfung von chmod machte nur einen Stringvergleich
    • recursive && 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
    • canonicalize liefert den tatsächlichen Pfad zurück, in dem .., . und symbolische Links aufgelöst sind
  • Im Fall von --preserve-root funktioniert 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-35363 wies rm zwar . 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
    Anzeige

An Unix-Grenzen Bytes vor Strings behandeln

  • Rusts String und &str sind 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_lossy ersetzen ungültige Bytes durch U+FFFD und beschädigen Daten stillschweigend
    • Strikte Konvertierungen wie unwrap oder ? können Eingaben zurückweisen oder den Prozess beenden
  • CVE-2026-35346 in comm war ein Fall beschädigter Ausgabe durch verlustbehaftete Konvertierung
    • In src/uu/comm/src/comm.rs wurden die Eingabebytes ra und rb mit String::from_utf8_lossy umgewandelt und dann per print! ausgegeben
    • GNU comm reicht Bytes auch bei Binärdateien unverändert durch, aber uutils ersetzte ungültiges UTF-8 durch U+FFFD und beschädigte die Ausgabe
    • Die Korrektur bestand darin, rohe Bytes mit BufWriter und write_all unverändert nach stdout zu schreiben
  • print! erzwingt über Display einen UTF-8-Roundtrip, Write::write_all dagegen nicht
  • In Systemcode für Unix sollte man je nach Kontext den passenden Typ verwenden
    • Für Dateipfade Path und PathBuf
    • Für Umgebungsvariablen OsString
    • Für Stream-Inhalte Vec<u8> oder &[u8]
  • Wenn man der Formatierungsbequemlichkeit wegen über String geht, 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 und from_utf8 zu DoS-Stellen werden, wenn Angreifer die Eingabe steuern können
    • panic! 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-35348 in sort --files0-from brach 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 sort behandelt Dateinamen wie der Kernel als rohe Bytes, uutils erzwang jedoch UTF-8 und beendete beim ersten nicht-UTF-8-Pfad den gesamten Prozess
  • In Code, der nicht vertrauenswürdige Eingaben verarbeitet, sollten unwrap, expect, Indexing und as-Casts als potenzielle CVEs gelten
    • Stattdessen sollte man ?, get, checked_* und try_from verwenden und echte Fehler an den Aufrufer weiterreichen
    Anzeige
  • Vorgeschlagene clippy-Regeln für die CI sind:
    • unwrap_used
    • expect_used
    • panic
    • indexing_slicing
    • arithmetic_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 -R und chown -R gaben ü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 0 enden, wenn die letzte Datei erfolgreich war
    • Skripte konnten dann fälschlich annehmen, die gesamte Operation sei erfolgreich abgeschlossen worden
  • dd rief zur Nachbildung des GNU-Verhaltens bei /dev/null auf dem Ergebnis von set_len() die Methode Result::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 Result per .ok(), .unwrap_or_default() oder let _ = 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 Result wirklich 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-35369 bei kill -1
    • GNU interpretiert -1 als 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
    Anzeige
  • 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-35368 war eine local root code execution in chroot
  • Das Muster des Problems war, dass nach chroot(new_root)? ein Benutzername innerhalb der vom Angreifer kontrollierten neuen Root aufgelöst wurde
    • get_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 chroot löst den Benutzer vor dem chroot auf
    • 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_name läuft über NSS und kann zur Laufzeit libnss_*-Module per dlopen laden

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
    • pwd deep path buffer overflow
    • numfmt out-of-bounds read
    • unexpand --tabs heap buffer overflow
    • od --strings -N NUL-Schreiben außerhalb des heap buffer
    • sort 1-Byte-Read vor dem heap buffer
    • split --line-bytes heap overwrite in CVE-2024-0684
    • b2sum --check read aus nicht alloziertem Speicher bei malformed input
    • tail -f stack buffer overrun
    Anzeige
  • 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 clippy schweigt
  • 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
    • OsStr statt String
    • ? statt unwrap
    • 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

1 Kommentare

 
GN⁺ 2026-04-30
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::fs einen TOCTOU-Race zu erzeugen
    Ich hoffe, dass am Ende eine openat-ähnliche API in die Standardbibliothek aufgenommen wird

    Außerdem stimme ich der Regel „vor dem Vergleichen von Pfaden auflösen“ nicht zu
    Im Allgemeinen ist es besser, fstat aufzurufen und st_dev sowie st_ino zu vergleichen; der Artikel hatte das teilweise auch erwähnt

    Ein seltener bedachter Nebeneffekt sind die Performance-Kosten
    In einem realen Beispiel brauchte cp bei einem sehr tiefen Verzeichnispfad 0,010 Sekunden, uu_cp dagegen 12,857 Sekunden

    In 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::fs hat ein Lowest-Common-Denominator-Problem
      In Rust 1.0 musste irgendetwas hinein, und leider hat sich dieser Zustand lange verfestigt

      Ich denke, uutils ist ein guter Ort, um eine alternative API zu std::fs zu entwerfen, bei der man schwerer Fehler macht

    • Danke, 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_ino gemeinsam auftauchen
      Es 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 vermeiden

    • Ich bin kompletter Anfänger, aber ich habe mich gefragt, warum man nicht einfach mit $(yes a/ | head -n $((32 * 1024)) | tr -d '\n') direkt cd macht und stattdessen eine while-Schleife braucht

      Edit: Verstanden. Wegen -bash: cd: a/a/a/....../a/a/: File name too long

    • Falls du es noch nicht gesehen hast: Es gibt eine Demo zur automatischen Umwandlung von GNU-Utilities wie wget in ein speichersicheres C++-Subset
      https://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 if statt switch verwendet hat

      Im 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 panic abstürzen, ist selbst nach Rust-Maßstäben ein ziemlich amateurhafter Fehler
      Bei etwas wie einem nicht behandelbaren Alloc-Fehler vielleicht noch verständlich, aber expect und unwrap sind schwer zu entschuldigen, wenn man nicht extrem streng sicherstellt, dass dieser Codepfad niemals ausgeführt werden kann

  • Eine 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 chroot Bibliotheken per dlopen lädt, steht nirgends auffällig vermerkt

      Das 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 uutils viel besser dran gewesen, wenn es GPL-lizenziert gewesen wäre und sich direkt vom coreutils-Originalquelltext hätte inspirieren lassen dürfen

    • Man 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 foo statt bar verwendet, weil bar unter Bedingung ABC ein gefährliches baz wegen 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 uutils die GNU-coreutils-Testsuite

      Ergä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, upstart und snap gemacht hat, liegt so etwas durchaus im erwartbaren Bereich

    • Neue 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

    • Tatsächlich ist es sogar noch schlimmer
      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
    • Hm … vielleicht gäbe es eine Möglichkeit, ein write lock auf das Verzeichnis zu setzen, aber sobald noch Dinge wie Timeouts dazukommen, wird es vermutlich schnell noch komplizierter
  • Die Grundursache einiger Bugs scheint zu sein, dass Unix-APIs zu intransparent sind

    Dass etwa get_user_by_name in einem neuen Root-Dateisystem Shared Libraries lädt, um einen Benutzernamen aufzulösen, und dadurch ein Angreifer, der Dateien in dieses chroot einbringen kann, Code als uid 0 ausführen kann, wirkt fast wie eine Booby Trap

    Dass 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-Problem
      Wer 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 libc entfernt genau so einen Aspekt

    • Die 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 chroot angewendet wird, steht unter der Kontrolle der Seite, die dieses chroot bereitstellt, und wer das nicht versteht, sollte chroot() nicht verwenden

      get_user_by_name mag sich wie eine Falle anfühlen, aber praktisch gibt es kaum einen Unterschied zwischen newroot/etc/passwd und Dingen wie newroot/usr/lib/x86_64-linux-gnu/libnss_compat.so oder newroot/bin/sh

      Deshalb finde ich, dass /usr/sbin/chroot gar keinen Grund haben sollte, eine Benutzer-ID nachzuschlagen
      toybox chroot tut das auch nicht
      Der 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öscht

    Das 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 unklar

      Und noch weiter gedacht: Wenn rm das Löschen verweigern würde, sobald die ersten 9 Bytes einer Datei important sind,
      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 uutils umfangreiche Vergleichstests des Verhaltens mit den Original-Utilities, und man versuchte sogar, Bugs beizubehalten

    • Aus 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 MacOS chroot() standardmäßig blockiert
      Um 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

    • Manchmal bedeutet Wachstum auch einfach nur, größer zu werden