- Der JPG-Farbfehler in SerenityOS sah wie ein Problem mit der Reihenfolge der RGB/BGR-Argumente aus, begann tatsächlich aber damit, dass
JPGLoaderKomponenten, deren Reihenfolge wichtig ist, der Iterationsreihenfolge einerHashTableüberließ - Durch die Einführung von
malloc_good_size()inAK+LibCnutztenVectorundHashTabledie tatsächliche Größe von malloc-Chunks aus; dadurch änderte sich die Anzahl der HashTable-Buckets, wodurch ein verborgener Bug sichtbar wurde - Der bestehende Code las die JPG-Komponenten
Y,Cb,Crzufällig in der richtigen Reihenfolge; weil das Ergebnis vonint_hashund die Bucket-Anzahl zusammenpassten, blieb ein Fehler bei der Verarbeitung des Huffman-Streams verborgen - Die Ursachenforschung begann damit, dass
JPGLoader.cppzuletzt nicht geändert worden war; wegen AK-Änderungen musste beim Bisect über 1000 Commits das etwa 3400 Dateien große OS mehrfach vollständig neu gebaut werden - Die endgültige Korrektur bestand darin, die Komponenten in deterministischer Reihenfolge zu durchlaufen; ein Workaround, der nur die Reihenfolge der Farbargumente änderte, hätte beim nächsten Reihenfolgewechsel dasselbe Problem erneut verursachen können
JPG-Farbfehler, der wie eine RGB/BGR-Verwechslung aussah
- In SerenityOS trat ein Problem auf, bei dem JPG-Bilder mit falschen Farben angezeigt wurden
- Wenn man in
JPGLoader.cppdie Reihenfolge der Argumente für denColor-Konstruktor änderte, sah das Bild wieder korrekt aus- Bisheriger Code: Übergabe in der Reihenfolge
Y,Cb,Cr - Vorübergehende Änderung: Übergabe in der Reihenfolge
Cr,Cb,Y
- Bisheriger Code: Übergabe in der Reihenfolge
- Die letzte Nicht-Revert-Änderung an
JPGLoader.cpplag laut Git jedoch mehr als einen Monat zurück, und es gab die Erinnerung, dass JPG-Hintergrundbilder vor ein bis zwei Wochen noch korrekt angezeigt wurden - Daher wurde es wahrscheinlicher, dass es sich nicht um einen einfachen Fehler in der Reihenfolge der Farbkanäle handelte, sondern dass eine andere Änderung einen bestehenden Bug offengelegt hatte
Durch AK-Änderungen erschwertes Bisect
- SerenityOS verwendet seine eigene Standardbibliothek namens AK (Agnostic Kit)
- AK erfüllt eine ähnliche Rolle wie die C++ STL, wird aber im selben Repository zusammen mit dem Betriebssystemcode geändert
- Wenn sich AK ändert, ist der Wirkungsbereich groß
- Eine Standardbibliothek wird von fast jedem Code eingebunden
- Da C++-Templates in Headern definiert sein müssen, führen Änderungen an AK-Headern zu umfangreicher Neukompilierung
- Jedes Mal, wenn beim Bisect ein Commit mit AK-Änderungen passiert wurde, musste das gesamte Betriebssystem neu gebaut werden
- Zum Zeitpunkt des Artikels etwa 3400 Dateien
- Während des Bisects über einen Bereich von 1000 Commits wurde auf einem Sandy-Bridge-Mobile-Laptop von 2011 vier- bis fünfmal ein vollständiger Build durchgeführt
- Auch
ccachekonnte diesen Fall nicht abfangen, und wegen der hohen Änderungsgeschwindigkeit des SerenityOS-Projekts änderte sich AK ungefähr alle 100 Commits
Das durch malloc_good_size() sichtbar gewordene versteckte Problem
- Nach dem Bisect über 1000 Commits stellte sich heraus, dass die Änderung, die die JPG-Farben kaputtmachte, nicht in
JPGLoader, sondern inAK+LibClag - Der Commit, der das Problem sichtbar machte, war
f89e8fb71a4893911ee5125f34bd5bbb99327d33- Titel:
AK+LibC: Implement malloc_good_size() and use it for Vector/HashTable - Zeitpunkt: 15. Mai 2021
- Titel:
- Dieser Commit implementierte die macOS-API
malloc_good_size()- Sie gibt für eine angeforderte Allokationsgröße die tatsächliche Allokationsgröße zurück
- Wenn zum Beispiel eine Anfrage über 35 Byte intern einen 64-Byte-Chunk verwendet, können die übrigen 29 Byte genutzt werden
- Nach der Änderung nutzten
Vector,HashTableund andere Strukturen den innerhalb eines malloc-Chunks verfügbaren Speicher besser aus - Da JPG-Bilder im unmittelbar vorherigen Commit korrekt angezeigt wurden, ließ sich eingrenzen, dass diese Änderung ein bereits vorhandenes, verstecktes Problem offengelegt hatte
Decoding, das sich auf die Kapazität der HashTable verließ
- Zunächst bestand der Verdacht, dass
JPGLoaderoder Code darüber fälschlicherweise von der Kapazität einesVectorabhing und direkt hineinschrieb - Die betreffende Änderung betraf sowohl
HashTableals auchVector, und beide wurden imJPGLoader-Code verwendet - Als testweise die Zeile entfernt wurde, die
kmalloc_good_size()auf derHashTable-Seite anwendete, und danach neu gebaut wurde, verschwand das Problem- Der entfernte Code passte die neue Bucket-Kapazität an die tatsächliche Allokationsgröße an
- Damit war bestätigt, dass eine Änderung der Bucket-Anzahl der
HashTabledas Ergebnis des JPG-Decodings beeinflusste - Eine
HashTableist kein Container, der wie ein zusammenhängender Datenstrom verwendet wird; ihre Kapazität oder Iterationsreihenfolge darf daher nicht als Grundlage dienen
Wie JPG-Komponenten verarbeitet wurden
- Der bestehende
JPGLoaderlas im Start-of-Frame-Abschnitt der JPG-Datei Komponenteninformationen und speicherte sie in einerComponent-Struktur - Jede
Componentbesaß eineserial_id, die ihre Position innerhalb der JPG-Datei angab- Die Reihenfolge der JPG-Komponenten sollte normalerweise
Y,Cb,Crsein
- Die Reihenfolge der JPG-Komponenten sollte normalerweise
- Diese Komponenten wurden in einer
HashTablegespeichert- Später wurden sie verwendet, um sie mit der Komponentenreihenfolge im Start-of-Scan-Abschnitt zu vergleichen und zu prüfen, ob die erwartete Reihenfolge vorliegt
- In der Decoding-Phase wurden diese Komponenten durchlaufen, um die für die Makroblock-Transformation nötigen Informationen zu verwenden
- Das Problem bestand darin, dass Komponenten, deren Reihenfolge wichtig ist, in eine
HashTablegelegt und mit dem Standard-Iterator durchlaufen wurden
Unterschiedliche Iterationsreihenfolge zwischen kaputtem und funktionierendem Commit
- In dem Commit mit den kaputten Farben durchlief die Debug-Ausgabe die Komponenten in folgender Reihenfolge
021
- Im unmittelbar vorherigen, funktionierenden Commit war die Reihenfolge anders
012
- Dieser Unterschied hing mit dem Ergebnis zusammen, das wie vertauschte Farbkanäle aussah
- Beim manuellen Ändern der Komponentenreihenfolge zusammen mit CxByte trat folgender Fehler auf
Huffman stream exhausted. This could be an error!Failed to build Macroblock 3277
- Dieser Fehler machte sichtbar, dass das JPG-Decoding empfindlich gegenüber der Stream-Reihenfolge ist, und bestätigte, dass die Iterationsreihenfolge der Komponenten die zentrale Ursache war
Zufällig passende HashTable-Reihenfolge
- Die eigentliche Ursache war, dass Objekte, für die eine Reihenfolge erforderlich ist, in einer
HashTablegespeichert und mit dem Standard-Iterator durchlaufen wurden - Der Hash der JPG-Komponenten-IDs wurde über
int_hashzur Auswahl des Buckets verwendet - Zuvor hatten zwei Zufälle gleichzeitig gepasst
- Das Ergebnis von
int_hashfür die Werte0,1,2war stabil - Die Bucket-Anzahl von
AK::HashTablepasste genau so, dass die Komponenten in der richtigen Reihenfolge platziert wurden
- Das Ergebnis von
- Dank dieses Zufalls las
JPGLoaderden Huffman-Stream für jede Komponente in der richtigen Reihenfolge, und der Bug blieb von Anfang an verborgen - Mit der Einführung von
malloc_good_size()änderte sich die Bucket-Anzahl derHashTable, dadurch änderte sich die Komponentenreihenfolge, und es erschienen Bilder mit vertauschten roten und blauen Kanälen
Endgültige Korrektur durch deterministische Iteration
- Nach etwa 10 Stunden Debugging entstand der Fix-Commit
- Der Fix-Commit ist
a10ad24c760bfe713f1493e49dff7da16d14bf39- Titel:
LibGfx: Make JPGLoader iterate components deterministically - Zeitpunkt: 31. Mai 2021
- Titel:
- Der Kern der Korrektur bestand darin, dass
JPGLoaderdie Komponenten in deterministischer Reihenfolge durchläuft - Auch eine bloße Änderung der Argumentreihenfolge für
Colorhätte die Bilder zunächst wieder korrekt aussehen lassen, hätte aber bei einer späteren Änderung der Iterationsreihenfolge erneut brechen können - Ein Problem, das wie ein kleiner Darstellungsfehler aussah, stellte sich als Fall heraus, in dem eine falsche Abhängigkeit von der Iterationsreihenfolge eines Containers zusammen mit einer Änderung der Allokationsgröße sichtbar wurde
1 Kommentare
Hacker-News-Kommentare
Das ist einer der Gründe, warum viele Hash-Table-Implementierungen ein zufälliges Element in den Algorithmus einbauen.
Da sich die Reihenfolge der Elemente bei jeder Ausführung ändert, fallen versehentliche Abhängigkeiten von der Reihenfolge schnell auf.
Wenn der Hash-Algorithmus fest ist, kann man Schlüssel erzeugen, die alle im selben Bucket landen, und das für Denial-of-Service-Angriffe missbrauchen; auch solche Sicherheitsprobleme lassen sich damit ziemlich gut abwehren.
Ich bevorzuge das, weil man dann nicht jedes Mal entscheiden muss, ob man eine sortierte oder eine unsortierte Map braucht.
Ich bin schon öfter davon ausgegangen, dass eine unsortierte Map reicht, und lag dann aus subtilen Gründen falsch.
Andernfalls ist es eine wirklich schlechte Idee, weil es das Debuggen anderer Probleme erheblich erschwert.
Zufälligkeit ist kein Freund, sondern ein Feind.
Vor etwa 20 Jahren gab es bei Angriffen auf Java-Webserver die Methode, URL-Parameter so zu manipulieren, dass sie alle im selben Bucket landeten; das wurde zu einem großen Denial-of-Service-Angriff.
Wenn ich mich richtig erinnere, hatten PHP-Webserver genau dasselbe Sicherheitsproblem.
Behoben wurde es, indem man der Hash Table einen Seed gab, und dieser Seed war natürlich vom Entwickler kontrollierbar. Denn Zufälligkeit ist kein Freund, sondern ein Feind.
Das wirkt wie ein Fall, in dem man Zeit gespart hätte, wenn man etwas mehr debuggt hätte, statt blind eine bisect-artige binäre Suche zu machen.
Logs, die die Komponentenreihenfolge ausgeben, musste man am Ende ohnehin einbauen.
Das Debugging war gut, aber auch die Commit-Message ist hervorragend.
Sie fasst Ursache und Fix in wenigen Absätzen sehr gut zusammen.
Wenn man lange genug wartet, bekommt auch C++ irgendwann ein Gegenstück zu
malloc_good_size.https://github.com/cplusplus/papers/issues/18
Im Titel braucht es [2021].
Das ist nicht Gunnars Schuld. Das Problem liegt bei der Seite, die geordnete Daten in einer Hash-Datei gespeichert hat.
In Jahrzehnten dieser Arbeit habe ich mehrmals erlebt, dass eine geänderte Speicheranordnung versteckte Bugs ans Licht bringt.
Jedes Mal kostet das Debugging Stunden bis Tage.
Wenn Programmieren nicht schwierig wäre, würde man uns nicht brauchen. Allerdings weiß ich nicht, wie lange dieser Satz im Zeitalter großer Sprachmodelle noch Bestand hat.
Gunnar hat etwas verbessert, und dabei ist nur ein Problem in altem kaputtem Code sichtbar geworden.
Als Lohn für diese Mühe bekommt er dann aber so etwas zu hören wie: „Gunnar, I like you, but please don't make me go through this again. :^)“
Soweit ich weiß, gibt es bei SerenityOS Leute, die einander mit Testressourcen oder PCs aushelfen.
SerenityOS auf einem Sandy-Bridge-Mobile-Laptop von 2011 vier- bis fünfmal von Grund auf zu bauen, ist ungefähr so, als wollte man auf einem Computer aus der Zeit zwischen Windows 3.1 und Windows 95 Windows Vista entwickeln.
Seit 2011 haben sich CPUs relativ gesehen nicht so stark verändert, während zwischen Windows 3.1 und Vista x64 verbreitet wurde und Multicore-CPUs üblich wurden.
Vista wurde Anfang 2007 international veröffentlicht; eine zum Release-Zeitpunkt 13 Jahre alte CPU wäre also von 1994, etwa ein Jahr nach dem ursprünglichen Pentium.
Damals nutzten viele noch immer den zuverlässigen 486 DX2-66.
Es ist ziemlich beeindruckend, dass eine 13 Jahre alte CPU heute noch für die Arbeit an einem modernen Projekt brauchbar ist. Damals konnte man das kaum sagen.
Ich hoffe, dass auch heute erscheinende CPUs bis nach 2037 zufriedenstellend nutzbar bleiben.
Visual Studio läuft darauf gut, und auch Photoshop ist nur bei den integrierten KI-Tools ein ganz klein wenig träge.
In Chrome sind vermutlich rund 200 Tabs offen, dazu Slack, WhatsApp und drei Browser zum Testen.
Bei CapCut wünschte ich mir bei 4K-Schnitt etwas mehr Tempo, aber komplexe 2K-Projekte hält es ausreichend gut durch.
Nur bei komplexen After-Effects-Projekten bin ich etwas an Grenzen gestoßen. Die mochte es nicht.
Ich sollte aufrüsten, aber für ein System, das praktisch aus dem Müll gerettet wurde, ist es ziemlich ordentlich.
Bei „Alien Lenna“ hatte ich ein Gefühl von Déjà-vu; tatsächlich war es ein Beitrag, den ich schon früher gesehen und sogar kommentiert hatte.
https://news.ycombinator.com/item?id=27374942 (2021)