2 Punkte von GN⁺ 2024-08-05 | 1 Kommentare | Auf WhatsApp teilen
  • Die für Kryptocode wichtige Eigenschaft der konstanten Laufzeit (constant-time) kann schon allein durch Compiler-Optimierungen zerstört werden; daher läuft ein Experiment, bei dem ein Warn-Patch in LLVM eingefügt wird, um riskante Muster zu finden
  • Compiler-„Optimierungen“ können Teile von Benchmarks schneller machen, doch zentrale Hot Paths stützen sich in der Praxis oft auf Intrinsics und Assembly, und die Kosten von durch Optimierungen verursachten Bugs häufen sich separat an
  • Im Juni 2024 bestätigte Antoon Purnal, dass der Kyber-Referenzcode unter Clang 15 und neuer bei bestimmten Optimierungsoptionen in eine bedingte Verzweigung auf Basis geheimer Werte umgewandelt wird und damit Timing-Angriffe ermöglichen kann
  • TIMECOP 2 prüft innerhalb von SUPERCOP als konstantzeitlich deklarierte Compiler-Ergebnisse, hat aber Grenzen bei von Valgrind unterstützten Befehlen und bei Datenflüssen, die erst in tatsächlichen Testläufen sichtbar werden
  • Praktische Gegenmaßnahmen bestehen darin, mit Funktionen wie crypto_{int,uint}{8,16,32,64}.h zu verhindern, dass der Compiler 1-Bit-Ergebnisse als bool sieht, oder zu verifiziertem Assembly, sicherheitsorientierten Sprachen oder spezialisierten Compilern zu wechseln

Die Verantwortungslücke durch Compiler-„Optimierung“

  • In den Änderungsverläufen aktueller Versionen von LLVM und GCC tauchen fortlaufend „Optimierungen“, „Optimierungs“-Tests, Testanpassungen und Bugfixes für „Optimierungen“ auf
  • Wenn Code, der vor dem Kompilieren gut funktionierte, sich nach einer Compiler-Änderung anders verhält, wird die Verantwortung in vielen Fällen dem Programmierer zugeschoben, der „undefined behavior“ ausgelöst habe
  • Solche „language standards“ werden von Compiler-Autoren erstellt; dadurch entsteht eine Struktur, in der der Code von Millionen Programmierern mehr Verantwortung trägt als die Änderungen einer kleinen Gruppe von Compiler-Autoren
  • Als Beispiel aus Kryptocode ist die avx2-Implementierung von kyber768 in mehreren CPU-Benchmarks etwa 4-mal schneller als portabler Code, der mit einem „optimierenden“ Compiler kompiliert wurde

Grenzen der Messung von Optimierungsleistung

  • Todd A. Proebsting formulierte 2000 in Proebsting's Law, dass „Compiler-Fortschritte die Rechenleistung alle 18 Jahre verdoppeln“, und kam zu dem Schluss, dass Compiler-Optimierungen nur einen randständigen Beitrag leisten
  • Arseny Kapoulkine fasste 2022 in einem Benchmark zusammen, dass LLVM 11 für optimiertes Kompilieren doppelt so lange braucht wie LLVM 2.7 und der ausgeführte Code im Allgemeinen 10 bis 20 % schneller ist
  • Beide Diskussionen verfehlen die Performance-Messung, die tatsächliche Nutzer wahrnehmen
    • In Hotspots, in denen sich Performance konzentriert, stecken häufig Intrinsics und Assembly
    • FFmpeg enthält gemessen an .asm- und .S-Dateien 160.000 Zeilen Assembly
    • Je mehr Daten Computer und Netzwerke verarbeiten, desto stärker entfällt die tatsächliche CPU-Zeit auf solche Hotspots
  • Auch Sicherheitskosten wachsen in der Optimierungsdebatte separat an
    • Deloitte berichtete, dass IT-Sicherheitsbudgets 2023 0,5 % des Unternehmensumsatzes ausmachten
    • Zusammen mit der Zahl, dass der weltweite Gesamtumsatz von Unternehmen 2022 bei über 48 Billionen Dollar lag, könnte die Gesamtdimension bei Hunderten Milliarden Dollar liegen
    • Allerdings gibt es den Vorbehalt, dass Deloittes 0,5 % ein einfacher Durchschnitt über Unternehmen sein könnten und nicht alle Unternehmen an der Umfrage teilgenommen haben

Timing-Leaks und der Kyber-Fall

  • Sicherheitsprobleme durch „optimierende“ Compiler umfassen nicht nur klassische Bugs, sondern auch Timing-Leaks, bei denen geheime Informationen über die Ausführungszeit austreten
  • Das EuroS&P-2018-Paper von Laurent Simon, David Chisnall und Ross Anderson warnt, dass Compiler-Upgrades ohne Vorwarnung Timing-Kanäle in zuvor sicheren Code öffnen können
  • Das im Paper von 2018 hervorgehobene Beispiel war Code, der mit bool einen von zwei Werten auswählt; bool veranlasst den Compiler, bedingte Sprünge zu erzeugen
    • In Krypto-Implementierungen ist es üblich, dies zu vermeiden, indem bool aus kritischem Code entfernt und separate Vergleichsfunktionen mit konstanter Laufzeit geschrieben werden
    • OpenSSL wird mit 37 Funktionen zitiert, die dafür deklariert werden
  • Der Fall curve25519-donna und MSVC 2015 aus dem Jahr 2015 wird im Text als Missverständnis eingeordnet
    • Tatsächlich wurden int64-Operationen beim Kompilieren für 32-Bit-x86 in Aufrufe von Microsofts 32-Bit-int64-Bibliothek llmul.asm übersetzt
    • Das Timing-Leak entstand durch datenabhängige Verzweigungen in llmul.asm, und diese Bibliothek sollte ebenfalls unter den vernünftigen Begriff von Quellcode fallen
  • Im Juni 2024 bestätigte Antoon Purnal, dass der Kyber-Referenzcode unter Clang 15 und neuer bei bestimmten Optimierungsoptionen Timing-Angriffe ermöglichen kann
    • Die problematische Form war (-((x>>j)&1))&y; diese Berechnung ergibt y, wenn das j-te Bit von x gesetzt ist, andernfalls 0
    • Clang wandelt das entsprechende Bit per Bit-Test-Befehl in ein bool um und erzeugt anschließend eine bedingte Verzweigung auf Basis dieses bool
    • Intern in LLVM verarbeitet combineShiftAnd1ToBitTest in lib/CodeGen/SelectionDAG/DAGCombiner.cpp diese „Optimierung“
    • Diese Funktion wurde im September 2019 von Sanjay Patel hinzugefügt und später von mehreren Personen geändert
  • Auch bei GCC gibt es ähnliche Grenzüberschreitungen
    • Ein GCC-Patch von ARM aus dem November 2021 wandelt (-x)>>31 in -(x>0) um
    • Im April 2024 wurde davor gewarnt

TIMECOP und Prüfung auf konstante Laufzeit

  • TIMECOP 2 ist in das Kryptotest-Framework SUPERCOP eingebettet und prüft automatisch von geheimen Werten abgeleitete bedingte Verzweigungen in kompiliertem Code, der als konstantzeitlich deklariert ist
  • Geprüft werden neben bedingten Verzweigungen auch Array-Indizes, die aus geheimen Werten abgeleitet sind
    • Das KyberSlash-Paper beschreibt außerdem einen Patch, der aus geheimen Werten abgeleitete Divisionen prüft
  • TIMECOP 1 war ein Werkzeug, das Moritz Neikes durch Änderungen an SUPERCOP erstellt hatte und das den Ansatz von Adam Langleys ctgrind automatisierte
  • TIMECOP 2 erweitert den bisherigen Ansatz in mehreren Punkten
    • RNG-Ausgaben werden automatisch als geheime Werte markiert
    • „Declassification“ wird unterstützt
    • Die Angabe von „public inputs“ wird unterstützt
    • Es läuft auf mehreren Cores
  • TIMECOP hat klare Grenzen
    • Es kann nur Befehle behandeln, die Valgrind unterstützt, und bricht daher etwa bei AMD-XOP-Befehlen ab
    • Es prüft nur Datenflüsse, die in tatsächlichen Testläufen sichtbar werden
  • Die Arbeit an Werkzeugen zur Prüfung von Constant-Time-Verhalten geht weiter; eine Liste einschlägiger Tools findet sich unter ct-tools
  • Eine TIMECOP-entsprechende Prüfung wurde in die Testsuite von libmceliece aufgenommen und könnte sich auch auf andere Bibliotheken ausbreiten

Methoden zum Umschreiben in konstante Laufzeit

  • Nachdem Codefragmente mit variabler Laufzeit gefunden wurden, braucht es eine Methode, sie ohne Bugs in konstante Laufzeit umzuschreiben
  • In einem Vortrag im Juli 2024 wurden einige Constant-Time-Funktionen vorgestellt, die libmceliece und SUPERCOP bereitstellen
    • Die Dateinamen lauten crypto_{int,uint}{8,16,32,64}.h
    • Diese Dateien können in andere Projekte kopiert und dort verwendet werden
  • Die Beispielfunktion crypto_uint32_bitmod_mask(x,j) hat dieselbe Wirkung wie -((x>>(j&31))&1), verhindert aber, dass der Compiler das 1-Bit-Ergebnis sieht
  • Als komplexeres Beispiel gibt es auch crypto_uint32_max(x,y)
  • Das Paper von 2018 behandelt einen Tweak, der Clang/LLVM um die Constant-Time-Funktion __builtin_ct_choose(bool cond, x, y) ergänzt
    • Das Paper behauptet fälschlich, dass diese eine Funktion ausreichend sei
    • Diese Funktion könnte irgendwann in Compiler aufgenommen werden, aber es kann lange dauern, bis Projekte sich darauf verlassen können
    • Die Implementierungsweise wird als fragiler eingeschätzt als crypto_{int,uint}{8,16,32,64}.h

Probleme im Voraus vermeiden

  • Wenn Tests vor der Auslieferung kompilierter Bibliotheken vom Compiler eingeführte Timing-Leaks finden, kann während der Code umgeschrieben wird für die Auslieferung die vorherige Compiler-Version verwendet werden
    • Dieser Ansatz ist eine vorübergehende Gegenmaßnahme, die Nutzer weiterhin schützt
  • Eine Lösung besteht darin, Bibliotheken als Assembly auszuliefern
    • Der RWC-2024-Vortrag Adoption of high-assurance and highly performant cryptographic algorithms at AWS stellt schnelle X25519-Software vor, für die bewiesen ist, dass sie X25519 für alle Eingaben korrekt berechnet
    • Die Implementierung ist in Assembly in zwei Versionen für 64-Bit-Intel/AMD-CPUs und zwei Versionen für 64-Bit-ARM-CPUs geschrieben
    • Die Korrektheitsaussage ist ein Theorem über den Maschinencode, den Nutzer tatsächlich ausführen, und der Beweis wird mit dem Theorembeweiser HOL Light verifiziert
  • Bei Kryptosoftware, die dieses Niveau nicht erreicht, bleibt allerdings weiterhin das Problem der schwierigen Auditierbarkeit von Assembly
  • Für in C, C++ und ähnlichen Sprachen geschriebenen Code werden auch schnelle Wege untersucht, eine „Impfung“ gegen Timing-Leaks einzubauen

Experiment mit dem clang-vs-clang-Patch

  • x&1 und x>>31 haben gemeinsam, dass es nur zwei mögliche Ergebnisse gibt
    • x&1 ist 0 oder 1
    • x>>31 bei uint32 ist 0 oder 1
    • x>>31 bei int32 ist 0 oder -1
  • Solche Formen machen es Autoren von Compiler-„Optimierungen“ leicht, ein 1-Bit-Ergebnis in bool zu stecken
  • Es gibt die Empfehlung, immer mit -fwrapv zu kompilieren, damit GCC und Clang Zweierkomplement-Arithmetik annehmen
  • Schon ein einfacher Scan des Quellcodes nach &1, 1&, >>31 usw. liefert viele Beispiele, doch hier wurde anders gescannt, indem direkt ein Patch in den LLVM-„optimizer“ eingefügt wurde
  • Der Patch beginnt bei LLVM-Commit 68df06a0b2998765cb0a41353fcf0919bbf57ddb, sucht nach &1 und >>31 und gibt folgende Warnung aus
    • please take this away before clang does something bad
  • Ein Beispiel für den Kompilierbefehl ist clang -Rpass-analysis=clang-vs-clang -O -c x.c
  • Die Testfunktion lautet wie folgt
int sra31(int x)
    {
      x >>= 31;
      return x;
    }
  • Dass dieselbe Warnung mehrfach erscheint, ist nicht überraschend
    • Der Compiler versucht immer weiter, „Optimierungen“ anzuwenden, bis kein Fortschritt mehr erzielt wird
  • Die Ausgabe von clang-vs-clang unterscheidet bei Shifts zwischen signed und unsigned
    • Dieser Unterschied ist für manuelles oder automatisches Umschreiben auf Basis von crypto_{int,uint}{8,16,32,64}.h wichtig
    • Als eine Möglichkeit zur Automatisierung von Source-Transformationen wird clang-tidy genannt
  • Code, der durch #ifdef ausgeschlossen ist oder vor dieser „Optimierungs“-Phase entfernt wurde, erzeugt keine clang-vs-clang-Warnung

SUPERCOP-Ausführungsergebnisse und gefundene Fälle

  • SUPERCOP 20240716 wurde auf einem Dual-EPYC 7742 mit ./data-do-biglittle ausgeführt
    • Overclocking wurde deaktiviert
    • Die SUPERCOP-Compilerliste wurde so angepasst, dass clang-vs-clang verwendet wird, indem den clang-Zeilen in okcompilers/{c,cpp} -Rpass-analysis=clang-vs-clang hinzugefügt wurde
  • Die Ergebnisse lagen nach 3 Stunden vor
    • Die Clang-Ausgabe umfasste insgesamt 675.752 Zeilen
    • Die Originalgröße betrug 210.786.494 Byte
    • Das komprimierte Ergebnis ist 20240803-fromclang.txt.gz mit 3.595.199 Byte
  • Die Ausgabe enthält viel Rauschen, das dadurch entsteht, dass Quellcode-Verzweigungen auf Basis öffentlicher Daten intern in Clang &1 erzeugen
  • Ein eindeutig vorab änderbares Beispiel ist folgendes
a0 += (a0>>15)&106;
  • Ein Beispiel, das sich per einfachem Source-Scan nur mit C-Parsing-Aufwand finden ließe, ist folgendes
    • Das Makro ONE8 ist als ((uint8_t)1) definiert
*pk2^=(((* pk_cp)>>ir)&ONE8)<<jr;
  • Ein noch schwieriger zu findendes Beispiel stammt aus einem AVX2-Intrinsic-basierten Makro
    • signmask_x16(x) ist als _mm256_srai_epi16((x),15) definiert
    • Dies verschiebt jedes signed 16-Bit-Stück innerhalb eines 256-Bit-Vektors um 15 Bit nach rechts
mask = signmask_x16(sub_x16(x,const_x16((q+1)/2)));
  • Dieser AVX2-Fall hat keine hohe Priorität
    • Damit eine Vektoroperation in eine bedingte Verzweigung umgewandelt wird, müsste mit AVX-512 kompiliert werden, und der Compiler müsste die merkwürdige Entscheidung treffen, ein vektorisiertes bool in serielle bedingte bool-Verzweigungen umzuwandeln
    • TIMECOP verwendet Valgrind, und Valgrind unterstützt AVX-512 nicht
    • Derzeit wird nicht empfohlen, für AVX-512 zu kompilieren

int128 und eine breitere Richtung für Gegenmaßnahmen

  • Der interessanteste Fund war ein Fall, in dem ein 64-Bit-Rechtsshift von int128 eine >>-Warnung auslöste
  • Eine int128-Implementierung kann intern einen 63-Bit-Rechtsshift verwenden, um das Vorzeichen des oberen 64-Bit-Worts zu ermitteln
  • Wenn Clang wie GCC Unterstützung dafür hinzufügt, einen 63-Bit-Rechtsshift in bool und anschließend in eine bedingte Verzweigung umzuwandeln, könnte viel int128-Code plötzlich variable Laufzeit haben
    • In diesem Fall wäre die Lage ähnlich wie im Titel des Papers von 2015 behauptet, nur dass es diesmal tatsächlich auch ohne bool im Quellcode passiert
  • Der einfachste Schutz auf Source-Ebene besteht darin, die bestehende int128-Implementierung des Compilers zu vermeiden und crypto_int128-Funktionen zu verwenden
    • Anders als int128 von GCC und Clang kann crypto_int128 auch auf kleinen 32-Bit-Plattformen funktionieren
  • Die Idee, geheime Datentypen in GCC und Clang zu ergänzen, wirkt gut, doch angesichts der Struktur beider Compiler ist kein klarer Weg erkennbar, sie robust zu machen
  • Mehr Hoffnung wird in Compiler gesetzt, die von Anfang an für Sicherheit entworfen wurden
    • Zu den sicherheitsorientierten Compilern, die eine neue Eingabesprache verlangen, gehören FaCT und das aktiv entwickelte Jasmin
    • Zwar gibt es Bedenken wegen des Aufwands für Code-Umschreibungen, doch angesichts der Art, wie aktuelle Compiler bestehenden Code behandeln, sind in irgendeiner Form Maßnahmen nötig

1 Kommentare

 
GN⁺ 2024-08-05
Meinungen auf Hacker News
  • Code, der undefiniertes Verhalten auslöst, als Compiler-Bug zu bezeichnen, nur weil er nicht wie gewünscht funktioniert, ist nicht richtig.
    Das ist ungefähr so, als würde man dd mit falschen Argumenten ausführen, dabei Daten löschen und dann behaupten, dd habe einen Bug.

    • Der Autor scheint hier implementierungsdefiniertes Verhalten mit undefiniertem Verhalten zu verwechseln. Die Beispiele im Artikel sind größtenteils gültiger Code; das eigentliche Problem ist, dass Compiler-Optimierungen, die bitweise Arithmetik in Branches umwandeln, in Kryptocode Timing-Angriffe ermöglichen.
      Es ist schwer, das als Bug im Quellcode oder im Compiler zu betrachten; treffender ist, dass der C-Standard nach den Maßstäben des Autors zu wenig spezifiziert ist und dadurch auf manchen Targets Sicherheitsbugs erzeugt.
      Letztlich können die Autoren des C-Standards nicht auch noch das Verhalten der Hardware definieren, sondern nur die Sprachsemantik; daher wird die Kryptografie zwangsläufig unter Bugs leiden, die der Hardware anzulasten sind.
    • Das Problem ist, dass es in C und C++ extrem viel undefiniertes Verhalten gibt und es außerordentlich schwierig ist, alles davon zu vermeiden.
      Einer der Vorteile von Rust ist, dass potenziell undefiniertes Verhalten auf unsafe-Blöcke beschränkt wird. Trotzdem hat Rust zwar vieles definiert, was in C undefiniertes Verhalten wäre, aber sobald man in unsafe-Code geht, ist es sehr leicht, versehentlich in subtile Fälle von undefiniertem Verhalten zu geraten.
    • Für Nutzer von Compilern gibt es nur zwei nützliche Modelle für undefiniertes Verhalten: Entweder die Kompilierung wird abgelehnt, wenn etwas eine schlechte Idee ist, oder es passiert etwas Vernünftiges und Stabiles.
      Ein drittes Modell, das still fehlschlägt und unvorhersehbaren Code erzeugt, ist nur für Compiler-Autoren nützlich. Sich hinter der Spezifikation zu verstecken, bringt echten Nutzern keinen Vorteil.
    • Russ Cox’ Artikel C and C++ Prioritize Performance over Correctness behandelt dieses Thema gut: https://research.swtch.com/ub
    • Diese Erwiderung kommt einem Strohmann-Argument ziemlich nahe. Der Kern ist, dass Compiler-Autoren selbst festlegen, was undefiniertes Verhalten ist, und den Standard so definieren, dass sie mehr Spielraum für Optimierungen bekommen.
      Diese Optimierungen machen Code kaputt, der zuvor gut funktioniert hat. Compiler-Autoren könnten Abwärtskompatibilität priorisieren, tun es aber nicht.
      Außerdem verbessern solche Optimierungen die Performance realer Programme nicht in nennenswertem Maße; man müsste also das Argument entkräften, dass dieser Trade-off, Code kaputtzumachen, es nicht wert ist.
  • Ich mag Bernstein, aber manchmal verrennt er sich und wird radikal; dieser Artikel ist ein gutes Beispiel dafür. Am Ende des Textes räumt er das selbst halb ein.
    Ein großer Teil des Artikels dreht sich um den Nebenaspekt, wie groß die Optimierungsgewinne sind; selbst mit Daten hängt diese Bewertung vom jeweiligen Use Case ab.
    Die zentrale Beschwerde ist, dass C-Compiler Semantik nicht berücksichtigen, die sich in der Sprache nicht ausdrücken lässt — was nicht überraschend ist.
    Am Ende sagt er: „Verwende eine Sprache, in der du die benötigte Semantik ausdrücken kannst.“ Der ganze Artikel hätte durch diesen einen Satz ersetzt werden können.

    • Entscheidend ist, dass diejenigen, die die Semantik von C und C++ definieren, viel zu viele Verhaltensweisen in den Topf „undefiniertes Verhalten“ werfen.
      Bei einem beträchtlichen Teil davon ist die Begründung fragwürdig, und es macht das Schreiben korrekter Programme schwieriger.
    • Der Teil darüber, dass Optimierungsgewinne vom Use Case abhängen, war hilfreicher Kontext und ziemlich erhellend.
    • Hier war DJB wenig überzeugend. Es kam viel unbegründeter elitärer Glaubenseifer zum Vorschein.
  • C und C++ sind ungeeignet, um Algorithmen mit Constant-Time-Garantien zu schreiben.
    Im Standard gibt es kaum ein Konzept von Echtzeit, und auch Compiler bieten keine zusätzlichen Garantien als Erweiterungen.
    Aber den Compiler-Entwicklern dafür die Schuld zu geben, geht in die falsche Richtung.

    • Wenn man Maschinencode erzeugen will, der unabhängig von Branches immer in konstanter Zeit arbeitet, muss man eine Sprache verwenden, die das ausdrücken kann. C unterstützt das nicht.
    • Ich frage mich, welche Sprache geeignet ist, um Algorithmen mit Constant-Time-Garantien zu schreiben.
  • Auf Intel-CPUs kann clang oder irgendetwas anderes im User Mode keinen korrekten Code erzeugen. Denn korrekter Code existiert von vornherein nicht.
    https://www.intel.com/content/www/us/en/developer/articles/t...
    Wenn man sich DOITM in dem Dokument ansieht, ist es für eine Kryptobibliothek im Userspace schlicht unmöglich, das benötigte Bit zu setzen.

    • Auch User-Mode-Code kann im richtigen Modus ausgeführt werden. Er kann den Schalter zum Ein- und Ausschalten dieses Modus nur nicht selbst bedienen.
      Sobald er eingeschaltet ist, funktioniert er auch im Userspace gut; möglich wäre also etwa ein prozessweites Flag, das über den Systemaufruf prctl aktiviert wird, wobei der Scheduler beim Task-Wechsel das MSR anpasst.
    • Kann man nicht per Systemaufruf in den Kernel gehen, das Flag setzen und dann in diesem Zustand in den User Mode zurückkehren?
  • Schon der Satz „Wann immer möglich, weigern sich Compiler-Autoren, Verantwortung für die von ihnen erzeugten Bugs zu übernehmen“ zeigt, wie selten ein Blogbeitrag fachlich so schnell auseinanderfällt.
    Wenn man sogar dem Link folgt, geht es nur um ganz grundlegendes C-Wissen: Undefined Behavior bedeutet nicht, dass ein „beliebiger Wert“ entsteht.

    • Offenbar nennen die Beteiligten unterschiedliche Dinge einen „Bug“. Die eine Seite meint einen Bug im Quellcode, die andere einen Bug im erzeugten Programm.
      Auch bei Undefined Behavior ist der Quellcode zwar fehlerhaft, das erzeugte Programm aber oft weiterhin korrekt. Wenn ein Compiler-Autor später eine neue Optimierung einbaut und auf Grundlage dieses Undefined Behavior ein fehlerhaftes Programm erzeugt, beginnt die Schuldzuweisung.
      Was man ungern anerkennt: Die Verantwortung gegenüber den Nutzern verteilt sich auf alle Seiten. Wenn bei einer CRUD-App wegen einer NULL-Dereferenzierung der Akku in Brand gerät, würde kein vernünftiger Mensch nur dem App-Entwickler vorwerfen, einen NULL-Check vergessen zu haben.
      Auch Compiler-, Betriebssystem- und Hardwarehersteller müssen Verantwortung für verantwortungslos konzipierte Produkte übernehmen; mit dem ISO-Standardbegriff „Undefined Behavior“ ist die Sache nicht erledigt. Alle Mitglieder der Lieferkette teilen die Verantwortung, vorherzusehen, wie ein Produkt missbraucht werden kann, und damit angemessen umzugehen.
    • Ich denke, der Autor weiß sehr wohl, was Undefined Behavior ist. Er betrachtet lediglich das Gesamtsystem kritisch.
      Undefined Behavior existiert, um Wert zu schaffen. Man kann Sprachen auch ohne so etwas bauen; der Grund, warum es trotzdem existiert, sind Portabilität und die Flexibilität für Compiler-Autoren.
      Der Kern des Beitrags ist die Frage, ob diese Flexibilität im Vergleich zur Schwierigkeit, Programme ohne Undefined Behavior zu schreiben, ihren Wert hat.
      Der Autor ist der Ansicht, dass das durch Bugs verlorene Geld größer wirkt als das durch schnelleren Bytecode eingesparte Geld, und dass der Einfluss der Compiler-Autoren bei der Festlegung von Sprachstandards groß ist, weshalb der Wille zur Korrektur gering ist.
  • Nebenbei: clang hat das Attribut clang::optnone, mit dem sich alle Optimierungen pro Funktion abschalten lassen, und GCC hat das hervorragende Attribut gnu::optimize, mit dem man Optimierungen per Name hinzufügen oder entfernen oder unabhängig von Compiler-Flags ein Optimierungslevel festlegen kann.
    gnu::optimize(0) ähnelt diesem clang-Flag. In clang gibt es außerdem clang::no_builtins, das speziell Optimierungen für memcpy und memset abschaltet.

  • Für die Ziele, die Leute aus der Kryptografie wollen, etwa Constant-Time Evaluation und das Verbergen geheimer Werte, habe ich durchaus Verständnis.
    Aber ein Allzweck-Compiler denkt die meiste Zeit nicht an solche Dinge, daher dürfte es kaum mehr sein als ein Hack, der meistens funktioniert.
    Wenn man es ernst meint, braucht man wohl einen eigenen Spezial-Compiler oder muss weiter auf Assembly setzen.

  • Irgendwann werden wir die heutige Zeit wohl als die schlechten alten Zeiten betrachten und uns von C wegbewegt haben, hin zu Sprachen mit deutlich weniger undefiniertem Verhalten
    In C ist es viel zu einfach, Ausdrücke zu schreiben, die zwar kompilieren, bei denen der Compiler aber unmöglich erkennen kann, was beabsichtigt war
    In Python kann man zum Beispiel Code wie result = [something(value) for value in set_object] schreiben. Da ein set-Objekt ungeordnet ist, ist klar, dass die Reihenfolge der Verarbeitung der Elemente und die Reihenfolge des Ergebnisses nicht wichtig sind; das eröffnet auf Sprachebene viele Optimierungen, ohne dass der Compiler die Absicht des Autors erraten muss
    Ähnlicher Code in anderen Sprachen mit unveränderlichen Daten geht noch einen Schritt weiter: Da something(value1) something(value2) nicht beeinflussen kann, lässt sich das parallel ausführen, ob in Threads oder Prozessen
    Ein großer Teil der Optimierung von C-Compilern besteht darin, sich Code-Muster anzusehen und Wege zu finden, das, was der Autor vermutlich gemeint hat, schneller zu machen. C kann Absichten im Vergleich zu modernen Sprachen nur unzureichend ausdrücken, sodass es zwar Spielraum für Vermutungen gibt, aber für ordentliche Performance muss man solche Schlüsse ziehen
    Trotzdem könnte es ein verkappter Segen sein, ähnlich wie die Sache, dass das Hubble-Teleskop eine Brille brauchte. Um Einschränkungen zu überwinden, wurden hervorragende Techniken entwickelt, und nachdem das Problem behoben war, lieferten diese Techniken eine viel höhere Performance als ursprünglich erwartet. Wenn man Optimierungen von C-Compilern auf Nicht-C-Sprachen anwendet, könnten sie wie Superkräfte wirken

    • Der Nachteil des Python-Beispiels ist, dass sich Leute auch dann auf bestimmte Eigenschaften verlassen können, wenn die Reihenfolge nicht spezifiziert ist, und der Code brechen kann, wenn der Optimierer die Reihenfolge ändert
      Im Grunde ähnelt das undefiniertem Verhalten, nur dass es sich nicht unmittelbar als Sicherheitsproblem zeigt, sondern als falsches Ergebnis. Natürlich kann ein falsches Ergebnis später zu einem Sicherheitsproblem führen
      Anders als bei undefiniertem Verhalten ist es praktisch unmöglich, einen „Sanitizer“ zu bauen, der überprüft, ob der Code für alle möglichen set-Reihenfolgen funktioniert
      gcc und clang haben viele Low-Level-Hinweise, die es in anderen Sprachen oft nicht gibt. Dazu gehören __builtin_expect/__builtin_unpredictable, __builtin_unreachable/__builtin_assume, #pragma clang loop vectorize(assume_safety)/#pragma GCC ivdep sowie Pragmas zum Abschalten von Loop-Unrolling oder Vektorisierung oder zum Auswählen bestimmter Werte
      Was meiner Ansicht nach am deutlichsten fehlt, ist eine Optimierungsbarriere, die explizit verhindert, dass der Compiler aus der Herkunft eines Werts Schlüsse zieht. Mit __asm__ geht das bis zu einem gewissen Grad, hat aber unerwünschte Nebenwirkungen und erfordert plattformspezifische Namen für Registertypen
      Es gibt eindeutig auch Potenzial für High-Level-Optimierungen auf Basis von Absichten. Denkbar wäre etwa, vor n-maligem push in einer Schleife Platz in einer Array-Liste zu reservieren, Hashmap-Lookups wie contains→get→put mit demselben Schlüssel zusammenzufassen oder globale Allokationsvorgänge lokal herzuleiten, um Objekte und Allokationen zu eliminieren
    • Theoretisch ergibt das Sinn, aber in der Praxis hat niemand gezeigt, dass es schneller als C ist
      C ist nah genug an realer Hardware, dass Programmierer einfach sagen können, was passieren soll; der Compiler muss die Absicht der Programmierer daher nicht erraten
    • Es stimmt, dass es Spielraum für semantikbasierte Optimierung gibt, aber der Beobachtung nach liegen solche Optimierungen meist im Umfeld der Speicherallokation
      Sprachen, die solche Speicheroptimierungen implementieren, sind meist Java-artig und haben von Anfang an eine aggressive vorauseilende Pessimierung, wodurch überhaupt erst der Anreiz für solche Optimierungen entsteht. Aber selbst mit diesen Optimierungen wird der Verlust nicht wettgemacht
      Der Punkt ist: C ist auch nicht toll, aber die anderen sind schlechter
  • Wenn einem die Semantik von C nicht gefällt, sollte man nicht auf Compiler-Ingenieure wütend sein, sondern eine andere Programmiersprache verwenden

    • Ehrlich gesagt weiß ich nicht, ob djb irgendetwas außer seinem eigenen qhasm ertragen kann. Nicht einmal Zig. Diese Einschätzung von ihm ist daher nicht besonders überraschend
  • Ein erfrischender Text, der eine Perspektive vermittelt, die man nicht oft hört. Ebenfalls lesenswert: https://gavinhoward.com/2023/08/the-scourge-of-00ub/