2 Punkte von GN⁺ 2024-12-22 | 1 Kommentare | Auf WhatsApp teilen
  • Scylla schlägt einen Weg vor, ursprüngliches C schrittweise umzustrukturieren, damit es direkt in sicheres Rust kompiliert werden kann, statt bestehendes C erst nach unsafe Rust zu übertragen und anschließend zu reparieren
  • Ziel ist eine applicative C-Teilmenge mit Datenverarbeitung, Pointer-Arithmetik, strukturiertem Kontrollfluss und portablem Code; ausgeschlossen sind Code mit Abhängigkeit von goto, Integer-Pointer-Casts, Pointer-Tricks, Bitfeldern und untagged unions
  • Die Transformation besteht aus zwei Schritten: Zuerst wird der Clang-AST zu Mini-C abgesenkt, anschließend nach sicherem Rust übertragen; dabei werden subtile C-Verhaltensweisen wie Integer-Promotions, implizite Konvertierungen, Zuweisungsausdrücke sowie Prä-/Post-Inkrement explizit gemacht
  • Die schwierigste Hürde, die Pointer-Arithmetik, wird in Aufteilungen auf Basis von Rust-Slices und split_at_mut/split_at umgewandelt; außerdem werden Mutabilitätsinferenz, Trait-Ableitung sowie die Wahl zwischen Box und Borrow mitbehandelt
  • Bewertet wurden Teile von Windows SymCrypt, HACL★, der zentrale Kompressionsalgorithmus von bzip2, der EverParse-CBOR-Parser/-Serializer sowie Teile von Microsoft FrodoKEM; dabei wurden während der Übersetzung auch undefined behavior in bzip2 und FrodoKEM gefunden

Der von Scylla gewählte Migrationspfad von C zu Rust

  • Die Motivation, bestehenden C-Code nach Rust zu übertragen, geht von Problemen der Speichersicherheit aus
    • Studien von Google und Microsoft schätzen, dass 70 % der Sicherheitslücken mit fehlerhafter Speicherbehandlung zusammenhängen
    • Unternehmen und Regierungen empfehlen für sicherheitskritische Systeme den Einsatz speichersicherer Sprachen wie Rust
  • Rust bietet klare Vorteile für neuen Code, aber es ist schwierig, bereits getesteten und debuggten industriellen C-Code vollständig neu zu schreiben
  • Bestehende automatische Werkzeuge für C→Rust geben zur Unterstützung des gesamten C-Sprachumfangs meist unsafe Rust aus
    • Sie erlauben C-typische Idiome wie unchecked pointer oder Rust-Transmutation
    • Dadurch entfällt die statische Garantie auf Speichersicherheit, und der Zweck einer sicheren Sprache wird geschwächt
  • Ein typischer Workflow nutzt die Ausgabe in unsafe Rust als Ausgangspunkt und refaktoriert sie anschließend schrittweise zu sicherem Rust
    • Es wurden statische Analysen vorgeschlagen, die raw pointer in sichere Rust-Borrows umwandeln oder Rust-Abstraktionen aus Low-Level-Darstellungen rekonstruieren
    • Refactoring-Werkzeuge sind verstreut, und die Unterstützung für c2rust refactor wurde 2022 eingestellt
  • Statt erzeugtes unsafe Rust zu reparieren, verfolgt Scylla den Ansatz, den ursprünglichen C-Code selbst schrittweise umzustrukturieren, damit er in sicheres Rust kompiliert werden kann

Unterstützte C-Teilmenge und ausgeschlossene Muster

  • Ziel von Scylla ist eine vorhersagbare Transformation und die Erzeugung von Rust-Code, der dem ursprünglichen C nahe bleibt
  • Unterstützt wird eine applicative C-Teilmenge
    • Code zum Manipulieren und Verarbeiten von Daten
    • Code mit Pointer-Arithmetik
    • Code mit strukturiertem Kontrollfluss
    • Portabler Code
  • Codebasen, die auf folgenden Mustern beruhen, werden nicht unterstützt
    • goto
    • Nutzung von Objektrepräsentationen über Integer-Pointer-Casts
    • Pointer-Tricks
    • Bitfelder
    • untagged unions
  • Entwickler können auf den C-Quelltext gezielte Umschreibungen und Annotationen anwenden, damit Scylla ihn versteht
    • Aliasing-Muster, die nicht zum Rust-Borrow-Checker passen, können umgeschrieben werden
    • Es kann Scylla mitgeteilt werden, dass tagged unions in höherstufige ADTs übersetzt werden sollen

Mini-C: eine Zwischensprache zur Reduktion von C-Mehrdeutigkeiten

  • Scylla startet beim AST des Clang-Frontends und übersetzt ihn zunächst in eine Sprache namens Mini-C
  • Mini-C behandelt wie C Verzweigungen, Schleifen, Pointer, Dereferenzierung und Adressnahme, hat aber eine „no-surprises“-Semantik
    • Alle Integer haben feste Breite
    • C-Integer-Promotion und Integer-Konvertierung werden als explizite Casts dargestellt
    • Untyped pointer wie void * sind nicht erlaubt
  • Anders als C ist Mini-C eine Ausdruckssprache
    • Zuweisungen liefern keinen Wert zurück
    • C-Konstrukte wie e1 = e2 = e3 oder p[i++] werden in Mini-C desugart
    • Testausdrücke in Schleifen und Bedingungen müssen vom Typ bool sein, nicht wie in C int
  • Beim Überführen des typisierten Clang-AST in einen typisierten Mini-C-AST werden implizite C-Verhaltensweisen explizit gemacht
    • Bedingungen werden auf bool angepasst
    • Array-Indizes werden auf size_t angepasst
    • Implizite Konvertierungen bei Funktionsargumenten und auf der rechten Seite von Zuweisungen werden in explizite Casts umgewandelt
    • Bei arithmetischen Operationen werden die usual arithmetic conversions des C-Standards berücksichtigt
  • Die Transformation setzt voraus, dass der C-Code portabel ist und nicht vom C-Datenmodell abhängt
    • Es wird also beispielsweise nicht erwartet, dass sich Verhalten danach richtet, ob long 4 oder 8 Byte groß ist
    • Die Implementierung erkennt zur Configure-Zeit das Datenmodell der Zielarchitektur und wandelt unsigned int in Typen fester Breite wie uint32_t um

ADTs und Tupel-Synthese

  • Mini-C bietet höherstufige ADTs, Tupel und Pattern Matching, die C so nicht hat
  • Tagged unions werden über Annotationen in ADTs übersetzt
    • Die Zielform ist eine Struktur wie { int tag; union { t0 case0; ...; tn caseN }}
    • Es wird angenommen, dass die Tag-Werte von 0 bis N reichen und mit der Reihenfolge der Union-Cases übereinstimmen
  • Scylla wandelt annotierte tagged-union-Typen in Variant-Typen um
    • Beim Erzeugen eines Werts wird geprüft, ob .tag = i und .casej = e zusammenpassen
    • Falls ja, wird der entsprechende Constructor-Wert erzeugt
    • Stimmen Payload und Tag nicht überein, wird nicht nach Mini-C übersetzt
  • Zugriff auf Felder einer tagged union ist nur sicher, wenn der aktuelle Tag-Zustand bekannt ist
    • Muster wie if (x.tag == i) { ... x.casei } oder switch werden erkannt
    • Sie werden in eine Form wie match x with | Ci v -> ... umgewandelt
    • Zugriffe auf andere Union-Cases gelten als ungültig und führen zu einem Übersetzungsfehler
  • Auch Tupel können per Annotation synthetisiert werden
    • Eine Struktur mit n Feldern wird in ein n-äres Tupel umgewandelt
    • Feldzugriffe werden zu Tupel-Feldzugriffen
    • Tupel sind strukturell typisiert und profitieren dadurch von Mutabilitäts-Polymorphie

Übersetzung von Mini-C nach sicherem Rust

  • Mini-C bietet eine Darstellung vollständiger C-Programme mit vollständigen Typannotation und wird anschließend in sicheres Rust übersetzt
  • Es gibt drei zentrale Schwierigkeiten
    • Beseitigung der Pointer-Arithmetik von C
    • Explizites Machen von Mutabilität und Aliasing
    • Automatische Bereitstellung rusttypischer Strukturen wie Traits
  • Die Übersetzung von Pointer-Typen ist wegen der unterschiedlichen Pointer-Darstellung in Rust komplex
    • Rust unterscheidet zwischen Box<T> und &T
    • Pointer auf ein einzelnes Element und auf mehrere Elemente werden ebenfalls unterschieden, etwa als &T und &[T]
    • Rust-Arrays sind Werte und zerfallen nicht automatisch wie in C zu Pointern
  • Die Grundstrategie besteht darin, alle Pointer-Typen aus C in Rust zunächst als Slice-Borrow &[T] zu kompilieren
    • Sowohl Stack- als auch Heap-Pointer werden zunächst zu Slice-Borrows
    • Auch Pointer auf einzelne oder mehrere Elemente werden zunächst auf Slice-Borrows abgebildet
    • Mutabilität wird in einem separaten Schritt automatisch inferiert
  • Mit Heuristiken und manuellen Annotationen übersetzt Scylla einige Pointer in Box<T>
    • Funktionen wie T *create(), die offenbar frische Allokationen zurückgeben und keine globalen Referenzen haben, können zu fn create() -> Box<T> werden
    • Diese Analyse wird rekursiv bis in Struct- und Variant-Definitionen hinein per Fixpunktverfahren angewandt
    • Structs, die Borrows enthalten, erhalten Lifetime-Parameter

Einschränkungen bei der Umwandlung von Box, Slice und Array

  • Da Rust zwischen Arrays, Slice-Borrows und Box explizite Konvertierungen verlangt, arbeitet auch die Rust-Übersetzung von Scylla typengetrieben
  • Die Transformationsregeln fügen Coercions ein, um Arrays oder boxed slices in Slice-Borrows umzuwandeln
    • Arrays werden in Formen wie &x[..] zu Slice-Borrows
    • boxed slices können in Borrows umgewandelt werden
  • Auch die Umkehrung ist möglich
    • Slices oder Arrays können auf Heap-Allokationen angehoben und in Box<[T]> umgewandelt werden
  • Diese Umkehrung kann Unterschiede in der Kopiersemantik erzeugen
    • In C können Arrays und Pointer auf denselben Speicher zeigen
    • In Rust kann Box::new(x) eine Kopie von x erzeugen
    • Bei Arrays primitiver Typen wie Integer-Arrays gibt es keine Möglichkeit, sich vom Copy-Trait abzumelden, sodass Rust stillschweigend kopieren kann
  • Wenn solche Transformationen auftreten, entfernt Scylla die ursprüngliche Variable aus der Umgebung und verbietet ihre weitere Verwendung
    • Falls das ursprüngliche C-Programm die Variable weiterverwendet, entsteht ein Übersetzungsfehler
    • Entwickler müssen den C-Quellcode vor der Transformation anpassen, um die Absicht klarer zu machen

Pointer-Arithmetik in Rust-Slice-Aufteilungen umwandeln

  • C-Programme greifen auf Arrays oft nicht nur über einen einzelnen Basis-Pointer zu, sondern teilen Arrays in Chunks auf oder iterieren mit einem aktuellen Positions-Pointer
  • Rust erlaubt keine beliebige Pointer-Arithmetik und bietet stattdessen das Teilen von Slices mit split_at_mut oder split_at
    • split_at_mut ist ein Primitive, das den Besitz des ursprünglichen Slices aufgibt und zwei Sub-Slices liefert
    • So bleibt die Rust-Invariante erhalten, dass veränderbare Daten einen eindeutigen Owner haben müssen
  • Um C-Pointer-Arithmetik an dieses Rust-Modell anzupassen, führt Scylla einen Split-Tree ein
    • Jeder C-Pointer wird auf genau einen Split-Tree abgebildet
    • Ein Split-Tree verändert sich abhängig vom Kontrollfluss
    • Er verfolgt, welcher Rust-Slice-Zugriff an einer bestimmten Programmstelle einen C-Pointer-Zugriff ersetzen muss
  • Da C-Pointer keine Längeninformation tragen, nimmt Scylla an, dass sich Chunks nicht überlappen
    • Falls Überlappung beabsichtigt ist, wird die Rust-Typprüfung fehlschlagen, und Entwickler müssen den C-Code umschreiben
    • Damit die Übersetzung vorhersagbar bleibt, wird Backtracking vermieden und nur vorwärts gearbeitet
  • Im Beispiel wird ein 32-Byte-Array abcd in vier 8-Byte-Limb-Bereiche aufgeteilt
    • In C wird Pointer-Arithmetik in einer nicht strikt links-nach-rechts verlaufenden Reihenfolge genutzt, etwa abcd + 0, abcd + 16, abcd + 8, abcd + 24
    • Die Rust-Übersetzung behält die Historie der split_at_mut-Aufrufe im Split-Tree bei, um den korrekten Sub-Slice zu finden

Evaluierte Ziele und gefundenes undefined behavior

  • Die Scylla-Implementierung verwendet Clang, nimmt bestehenden C-Code als Eingabe und erzeugt sicheres Rust
  • Zur Evaluierung gehörten Teile mehrerer bestehender C-Projekte
    • Teile von Windows SymCrypt
    • Teile der Kryptobibliothek HACL★
    • Kernteile des Kompressionsalgorithmus von bzip2
    • der binäre CBOR-Parser und Serializer der EverParse-Bibliothek
    • Teile der Implementierung von Microsofts Post-Quantum-Kryptoprimitiv FrodoKEM
  • Diese Fälle zeigen, dass Scyllas applicative C-Teilmenge mehrere sicherheitskritische Anwendungen abdecken kann
  • Während der Übersetzung wurde auch undefined behavior im ursprünglichen C-Code von bzip2 und FrodoKEM identifiziert und gemeldet

1 Kommentare

 
GN⁺ 2024-12-22
Kommentare auf Hacker News
  • Wichtig ist, dass sich diese Arbeit auf „bereits formal verifizierte C-Codebases“ bezieht.
    Gewöhnlicher System-C-Code ist nicht formal verifiziert, das ist also eine ziemlich andere Geschichte.

    • Trotzdem scheint das nicht völlig vertrauenswürdig zu sein. Auch Abschnitt 2.2 des Papers sagt, dass die durch die Transformationsregeln eingeführten Zwänge subtile semantische Unterschiede erzeugen können.
      Zum Beispiel könnte ein Pointer auf ein C-Array auf dem Stack in Rust als Pointer übersetzt werden, der mit Box<[u8]> eine neue Heap-Kopie besitzt. Wenn der ursprüngliche Code sich darauf verlassen hat, dass der Pointer tatsächlich auf das Array zeigt, kann der übersetzte Code stillschweigend falsch funktionieren.
      Die Funktion zur automatischen Übersetzung einer speichersicheren C++-Teilmenge in meinem Projekt scpptool hätte das wohl so behandelt, dass Arrays in Ersatztypen und Iteratoren überführt werden, um die ursprüngliche Semantik zu erhalten.
      Das OP-Projekt behandelt vielleicht nur C, das sich leicht in sicheres Rust umwandeln lässt; angesichts der Schwierigkeit des Problems ist die Leistung aber respektabel und ein gewisser Nutzen ist erkennbar.
    • Hier gibt es sehr viel mehr Einschränkungen, und es grenzt fast an Hype.
      Zunächst einmal haben sie kein echtes C übersetzt, sondern Code, der in F* geschrieben wurde, so verändert, dass der C-Compiler Rust ausgibt. Sie hatten es nicht mit komplexem realem C-Code zu tun, sondern allenfalls mit einem eingeschränkten Mini-C, wie es ein Toy-Compiler ausgeben könnte.
      Auch der Originaltext sagt: Wenn das ursprüngliche C-Programm stärker von x abhängt, gibt die Übersetzung einen Fehler aus und verlangt vom Programmierer, den Quellcode zu ändern. Das heißt, man hofft darauf, dass C bereits in einem Stil geschrieben ist, der den Rust-Borrow-Checker zufriedenstellt.
      Das wirkt wie die akademische Formulierung: „Abbildung 4 präsentiert schöne Regeln, aber die tatsächliche Implementierung stützt sich auf zahlreiche Tricks.“
      Noch schlimmer: Statisch unterscheidbare Überlappungen führen zu einem Compile-Fehler, andernfalls kann der Rust-Code zur Laufzeit panicken. Es ist seltsam, ein formal verifiziertes C-Programm in ein Rust-Programm zu verwandeln, das „jetzt auch abstürzen kann“.
      HACL* als bestehende formal verifizierte C-Codebase zu bezeichnen, ist ebenfalls ungenau. HACL* wird zwar nach C kompiliert, ist aber keine C-Bibliothek, sondern in einer völlig anderen Sprache geschrieben.
      Ein ehrlicher Titel hätte eher lauten müssen: „Eine Teilmenge von F* nach teilweise sicherem Rust kompilieren, teilweise formalisiert“.
    • Ist Rust selbst formal verifiziert? Soweit ich weiß, nicht.
    • Ich frage mich, was genau formal verifiziertes C ist und warum es nicht mehr davon gibt.
    • Ich frage mich, worin der entscheidende Unterschied besteht. Kann man die Einhaltung per Compiler-Flag erzwingen?
  • 2002 veröffentlichten Forschende ein Paper zu Cyclone, einem sicheren Dialekt von C, und fanden beim manuellen Portieren von C-Code nach Cyclone Sicherheitsfehler im bestehenden C-Code.
    Solche manuellen oder automatischen C-Transformationen haben das Potenzial, nicht nur die Einführung sichererer Sprachen zu fördern, sondern auch bestehende Bugs offenzulegen.
    [1] https://www.researchgate.net/profile/James-Cheney-2/publicat...

    • Cyclone wird nicht mehr unterstützt, das zentrale Forschungsprojekt ist beendet, und die Entwickler sind zu anderen Dingen weitergezogen.
      Mehrere Ideen aus Cyclone sind in Rust eingeflossen, und mit Aufwand lässt sich der Code zum Laufen bringen, aber auf modernen 64-Bit-Plattformen baut er nicht ohne Weiteres.
      http://cyclone.thelanguage.org
  • Ich habe C2Rust als ersten Schritt verwendet, um einige Projekte, darunter C-Projekte, nach Rust zu portieren, und bin zu ein paar Schlussfolgerungen gekommen:

    1. Wenn man ein C-Programm nach Rust überträgt, werden Bugs oft schnell sichtbar, selbst wenn unsafe enthalten ist, dank der starken Einschränkungen von Rust, etwa Bounds Checks und strikten Signaturen.
    2. Ich glaube nicht, dass automatische C→Rust-Transformation vollständig gelöst werden kann. Das Design von C-Programmen unterscheidet sich grundlegend von Rust, und um sie sicher zu machen, ist erhebliches Redesign nötig.
    3. In manchen Fällen ist es unmöglich, unter exakter Wahrung der Semantik von C nach Rust zu wechseln. Denn Unsicherheit kann dem Design selbst innewohnen.
      Trotzdem sind Tools fürs Portieren unverzichtbar, und je besser die Tools werden, desto reibungsloser wird der Prozess.
    • Eine automatische Umwandlung in „schnelles und sicheres Rust“ ist schwierig, aber eine automatische Umwandlung in gewöhnliches sicheres Rust ist viel einfacher.
      Man kann Speicher als Array darstellen und Pointer als Indizes in dieses Array behandeln. So lassen sich C-Verhalten wie ungeprüfte Pointer-Arithmetik oder Unions ausdrücken, ohne gegen den Borrow-Checker zu kämpfen, und die Semantik bleibt erhalten. Bei C→Java werden ähnliche Techniken schon lange verwendet.
      Der Wert einer solchen Transformation ist natürlich fraglich. Im Grunde ähnelt es dem Kompilieren von C nach wasm, ist aber langsamer; und selbst wenn der resultierende Code technisch „sicher“ ist, bleiben Probleme bestehen: Buffer Overflows können falsche Zustände erzeugen, oder Dangling Pointer können Zugriff auf Daten in Kontexten ermöglichen, in denen das nicht erlaubt sein sollte.
    • Der Aussage, dass „Unsicherheit dem Design innewohnen kann“, stimme ich grundsätzlich zu, und nach meiner Erfahrung fühlt sich das auch stark so an; es wäre aber gut, ein einfaches Beispiel zu haben, um die Diskussion zu konkretisieren.
  • Ich bin der Autor. Es dürfte hilfreich sein, ein paar Punkte aus mehreren Threads zusammenzufassen:

    1. Das ist ein wissenschaftlicher Artikel auf arxiv, keine Ankündigung eines neuen Produkts, das behauptet, das C→Rust-Problem gelöst zu haben. Er wurde bei einer PL-Konferenz eingereicht; Publikum und Erwartungshaltung unterscheiden sich von einem Vortrag auf einer Open-Source-Veranstaltung wie der FOSDEM.
    2. Der Ansatz ist schlicht. Wir gehen von der Einschränkung aus, C in sicheres Rust zu übersetzen, und untersuchen, dass man dafür eine kleine, gut funktionierende C-Teilmenge, Inferenz zur Slice-Aufteilung, Übersetzungen, die fehlschlagen können, sowie Programme, die abbrechen können, braucht. Evaluiert haben wir das an unserem Ziel, nämlich in F* eingebettetem C, und gezeigt, dass es unter diesen Einschränkungen bei großen C-Bibliotheken, die in Mainstream-Software wie Firefox oder Python verwendet werden, recht gut skaliert. Wir behaupten nicht, Firefox automatisch in Rust umschreiben zu können.
    3. Forschung läuft ursprünglich genau so. Man sieht einen interessanten Punkt im Design Space; man behauptet nicht, alle Probleme zu lösen, hält es aber für eine Idee, die weitere Fortschritte bei der C→Rust-Übersetzung ermöglichen kann. Ein bestehendes Tool könnte diesen Ansatz für Code nutzen, der zur Teilmenge passt, und für nicht passende Teile auf unsafe Rust zurückfallen.
    4. Das ist nicht die endgültige Fassung. Wir arbeiten daran, ein echtes C-Frontend mit libclang zu bauen, und untersuchen außerdem, wie sich garantieren lässt, dass das erzeugte Rust keine Zugriffe außerhalb des gültigen Bereichs erzeugt. Denkbar ist zum Beispiel, Verifikationsbedingungen an Z3 auszugeben. Wenn Reviewer meinen, dass mehr Arbeit nötig ist, verbessern wir es und reichen erneut ein; wenn sie entscheiden, dass das Gebiet aktiv ist und andere aus den Ideen Nutzen ziehen können, und den Artikel annehmen, umso besser.
  • Was ich mich wirklich frage, ist, warum man das so machen sollte.
    Wenn es eine Technik ist, mit der sich industrielle Anwendungen wirklich von C nach Rust umwandeln lassen, müsste man bestehende C-Anwendungen doch auch leichter bulletproof machen können. Man müsste nur Analysen entwickeln, die in bestehende Werkzeuge wie statische Analysatoren oder Testgeneratoren einfließen.
    Ebenso könnte man sichere Wrapper erzeugen und neuen Code in Rust neben verifiziertem C schreiben lassen. Der neue Code profitiert von Rust, der bestehende Code wird als sicher bestätigt, und die Schnittstellen werden ebenfalls sicherer.
    Ein vollständiger Übersetzer mag ideal sein, weil es langfristig wünschenswert ist, wenn eine Codebasis nur eine Sprache verwendet. Der größte Bedarf bleibt aber nach wie vor eine Ein-Klick-Sicherheitsverbesserung mit wenigen False Positives für bestehendes C/C++. Auch automatisches Korrigieren schlechter Strukturen innerhalb von C, wie bei Googles Compiler-Tools oder ForAllSecures Mayhem, könnte möglich sein.

    • Manche C-Programme lassen sich nicht sicher machen; daher stimmt die Aussage „Wenn es eine Technik gibt, industrielle Anwendungen nach Rust zu konvertieren, kann man C-Anwendungen leichter bulletproof machen“ nicht.
      Das kann daran liegen, dass sie von undefiniertem oder nicht spezifiziertem Verhalten abhängen, oder daran, dass geeignete Sicherheitsprüfungen den zulässigen Eingabebereich so stark verkleinern würden, dass das Programm nutzlos würde.
      Eine Übersetzung in eine sichere Sprache ist in solchen Fällen objektiv besser, weil sie die Ausdruckskraft der Eingaben erhält und zugleich zur Laufzeit korrektes Verhalten statisch garantieren kann.
      „In der Praxis bewährtes C“ gibt es, wie zahlreiche schwere Sicherheitslücken zeigen, kaum wirklich. Was es tatsächlich gibt, ist C, das oft genug ziemlich gut funktioniert, sodass es nützlich wirkt.
      Bei altem Code nimmt man mit etwas Glück an, dass er sicher ist; bewiesen ist er nicht. „Beweis“ hat, besonders im Kontext eines solchen Papers, eine konkrete Bedeutung, und die überwältigende Mehrheit von C-Code ist nach strengen mathematischen Maßstäben nicht bewiesen. Das Rust-Typsystem hingegen ist mathematisch als korrekt bewiesen.
      Ein vollständiger Übersetzer hängt davon ab, worauf man verzichten kann. Wenn man Performance, Eingabebereich, Ausgabebereich, Lesbarkeit des Codes usw. aufgeben darf, ist es bis zu einem gewissen Grad möglich; problematisch wird es, sobald man in all diesen Dimensionen einen sounden und vollständigen Übersetzer haben will.
  • Wenn man naiv nach Rust übersetzt, bekommt man dann nicht eine Mischung aus sicheren und unsafe Teilen? Dann müsste man manuell nur die Sicherheit der unsafe-Bereiche prüfen. Das wäre ähnlich wie beim Schreiben von Rust von Grund auf.
    Wenn 90 % des Ergebnisses nicht unsafe sind, könnte das ein ziemlich großer Gewinn sein.

    • Das stimmt tatsächlich. Jemand hat OpenJPEG einmal mit c2rust in Low-Level-unsafe Rust umgewandelt.
      Von OpenJPEG war bekannt, dass es bei einem bestimmten Testfall einen Segfault auslöst; als man diesen Test auf der Rust-Version laufen ließ, trat der Segfault an der entsprechenden Stelle im Rust-Code auf. Zumindest war es also kompatibel.
      Dieser Ansatz ist aber eine Sackgasse. Um voranzukommen, muss der Übersetzer gängige C-Idiome erkennen und in eine natürliche Form der Zielsprache anheben. Wenn man nach Rust „kompiliert“, entsteht grauenhaftes Rust voller Aufrufe unsicherer, C-artiger Pointer-Manipulationsfunktionen.
      Das größte Problem beim Anheben hängt meist mit Pointern zusammen. Das vielversprechendste Ergebnis dieses Papers ist, dass es einen Weg gefunden hat, C-Pointer-Arithmetik in Rust-Slices umzuwandeln. Slices können das meiste leisten, was C-Pointer-Arithmetik tut, und nun hat jemand diese Übersetzung automatisiert. Pointer-Arithmetik, die sich nicht übersetzen lässt, sollte man sehr misstrauisch betrachten.
      Es ist hilfreich, sich vorzustellen, dass ein Raw Pointer auf ein Array in C implizit eine Länge hat. Diese Länge erscheint nicht im C-Quelltext, existiert aber irgendwo als Funktion des Programmzustands. Sie kann eine Konstante sein, die Größe einer malloc-Anforderung oder ein Funktionsparameter. Für Wartungsprogrammierer ist es meist nicht allzu schwer, die Array-Länge zu finden.
      Das könnte ein Problem sein, das gut zu LLMs passt: Man fragt „Sieh dir diesen Code an und finde heraus, wie lang das Array foo ist“, und lässt dann einen nicht-LLM-basierten Übersetzer die Rust-Umwandlung daran ausrichten. Wenn das LLM falschliegt, erzeugt Rust einen Indexfehler oder ein zu großes Array, aber es wird nicht unsicher. Die C-Idiome für Arraygrößeninformationen sind ausreichend formalisiert, sodass man sie meist korrekt erkennen können sollte. Insbesondere können LLMs auch Kommentare lesen.
    • Eine naive Übersetzung würde fast vollständig unsafe Rust-Code erzeugen, weil überall Raw Pointer statt Referenzen verwendet würden.
      C-Code wird nicht mit Rusts Alias-Modell und den Einschränkungen des Borrow Checkers im Hinterkopf geschrieben, daher ist es schwierig, ihn in Referenzen zu übersetzen.
  • Es geht nur darum, eine sehr kleine C-Teilmenge zu kompilieren. In der Praxis könnte sie so klein sein, dass sie nutzlos ist.
    An diesen Ansatz habe ich geringe Erwartungen. Er wird zwangsläufig an die Grenzen dessen stoßen, was mit statischer Analyse von C-Code möglich ist. Außerdem wird das Problem durch die Wahl von Rust als Ziel unnötig schwierig. Rusts Ownership-Modell unterscheidet sich zu stark davon, wie reale C-Programme funktionieren.

    • Rusts Ownership-Modell ist einer C-Übersetzung ausreichend ähnlich. Es hat lediglich explizitere und stärkere Typen, sodass der Übersetzer herausfinden muss, was frei geschriebener C-Code erreichen will, und es auf Rust-Idiome abbilden muss.
      Zum Beispiel haben Puffer in C natürlich eine Länge, aber in C ist die Länge nicht explizit an den Pointer gebunden. Daher muss der Übersetzer ableiten, wie das C-Programm die Länge verfolgt, und das in Slices umwandeln. Selbst wenn die Länge eine explizite Variable ist, ist das nicht einfach; noch kniffliger wird es, wenn sie berechnet wird oder als „Pointer hinter das Ende“ dargestellt ist.
      Auch C-Muster wie bool should_free_this_pointer lassen sich in Rust als Owned/Borrowed-Enum übertragen, aber man muss ableiten, welche Allokation mit welchem Boolean verbunden ist und wo der tatsächlich sichere Bereich der geliehenen Variante liegt.
    • Als Interface-Sprache könnte es gut sein. Nützlich für Bindings.
    • Am Ende werden die Leute wohl LLMs darauf ansetzen und sagen, es sei schon okay, wenn sie massenhaft plausibel richtigen Code halluzinieren.
      Trotzdem stimme ich zu, dass es schwierig sein wird, aus beliebigem C idiomatisches Rust zu machen. Es wäre sozusagen nur „ungefähr richtig“.
  • Ich frage mich, wie sich das mit der C-Übersetzungsfunktion von Zig vergleicht.
    Zig scheint hervorragend darin zu sein, eine gemischte Umgebung zu schaffen, in der neuer Code in Zig und alter Code in C bleibt, dabei zu übersetzen oder zu interoperieren und sogar die Rolle eines C-Compilers zu übernehmen.
    Es wird sehr gute Gründe geben, warum die Linux-Kernel-Maintainer Zig nicht statt Rust als C-Ersatz betrachten. Ich weiß nicht genug, um darüber zu spekulieren; es wäre gut, wenn Leute mit mehr Ahnung das erklären könnten.

    • Rust ist weniger ein „Ersatz für C“ als ein zusätzliches Werkzeug neben C. Es ist ein Werkzeug, dessen Wert Torvalds und andere anerkannt und daher im Kernel zugelassen haben; der Großteil des Kernel-Codes wird weiterhin in C geschrieben werden.
      Ich bin kein Kernel-Maintainer, aber wenn ich zwei wichtige Gründe vermuten müsste, warum Rust gegenüber Zig gewählt wurde, dann vielleicht die besseren Compile-Time-Garantien, die die Sprache bietet, und die schnellere Adoption.
      Große Unternehmen der Branche arbeiten viel daran, Rust-nativen Code für APIs oder gepflegte Rust-Bindings bereitzustellen. Auch Windows-Entwickler schreiben Teile ihres eigenen Kernels in Rust neu. Das ist eine Bewegung, die schon ziemlich lange anhält, und ich hoffe, sie hört nicht auf.
      Die Maintainer könnten das Gefühl haben, dass Zig gegenüber C nicht genügend Vorteile bietet. Viele von ihnen sind selbst Rust gegenüber noch ablehnend.
    • Soweit ich es verstehe, wollen die meisten Kernel-Maintainer C durch überhaupt nichts ersetzen.
      Zig hat eine deutlich bessere Interoperabilität mit C als Rust, ist aber nicht speichersicher und noch nicht stabilisiert. Die Zig-Adoption in der C-Welt wird wohl deutlich zunehmen, aber es ist schwer, es als direkten Konkurrenten zu Rust zu sehen.
      In meiner Region führt niemand Rust ein, und C++-Leute bleiben bei C++. Anfangs gab es etwas Interesse an Rust, aber in keinem Unternehmen, das ich kenne, hat es sich etabliert. Das hat wahrscheinlich ähnliche Gründe wie bei Go, das in jungen Unternehmen stark gewachsen ist, aber in traditionelle Java-/C#-Unternehmen kaum hineinkommt. Selbst wenn es technisch sinnvoll ist, ist es eine enorme Change-Management-Aufgabe.
      Zig gewinnt bei Programmen an Schwung, die keine dynamische Speicherallokation brauchen, aber viel mehr auch nicht.
    • Zig ist noch nicht reif genug, um für den Kernel in Betracht gezogen zu werden.
      Es gibt weiterhin regelmäßig Breaking Changes; das ist im Moment gut für Zig, aber nicht gut für eine riesige, langlebige Codebasis wie Linux. Auch Compiler-Bugs treten auf.
      Das sage ich als jemand, der die Richtung von Zig im Großen und Ganzen mag.
    • Zig ist noch nicht 1.0 und bietet keinerlei Abwärtskompatibilitätsgarantie. Es wird fast nirgendwo eingesetzt, und auch wenn einige Teile vielversprechend wirken, hat es seinen Wert noch nicht bewiesen.
    • Es könnte daran liegen, dass Zig nicht speichersicher ist.
  • Ich frage mich, ob ein Tool wie C2Rust dies nutzen könnte, um formal korrekten Code zu erzeugen.
    Außerdem frage ich mich, wie viel Handarbeit die Autoren geleistet haben oder ob sie etwas ausgeführt haben, um Rust-Code zu generieren. Falls ja, weiß ich nicht, wo der Code ist, der Rust erzeugt, und ich sehe auch keinen Link zum Source-Repository.

    • Im Paper steht, dass sie diese Entwicklungsartefakte nach Abschluss des Review-Prozesses, also im Allgemeinen nach der formellen Veröffentlichung des Papers, unter einer Open-Source-Lizenz freigeben wollen.
  • Wenn eine C-Bibliothek funktioniert, also nicht formal als problemfrei bewiesen ist, aber meistens gut läuft, frage ich mich, warum man sie nicht mit unsafe Rust übersetzt.
    Rust hat insgesamt zu wenige Bibliotheken, daher sehe ich darin Wert. Letztlich unterscheidet es sich nicht wesentlich davon, eine in C geschriebene dll/so zu verwenden, die in manchen Situationen unsicher sein kann.