Ich hasse Compiler
(xeiaso.net)- Anubis plant, den Proof of Work zum Schutz von Websites über SHA-256 hinaus zu erweitern, und ist so konzipiert, dass Client und Server dieselbe WebAssembly-Prüflogik ausführen
- Um Umgebungen ohne aktiviertes WebAssembly nicht auszuschließen, wurde ein JavaScript-Recompile-Pfad vorbereitet, der jedoch langsamer als WebAssembly ist und bei deaktiviertem JIT noch langsamer werden kann
- Das
wasm2jsder Linux-Distributionen war veraltet und erzeugte andere Ausgaben als die Homebrew-Version, weshalb für reproduzierbare Builds ein mitwasi-sdkgebauteswasm2jsgebündelt wird - C/C++-Builds können selbst bei gleicher Eingabe wegen
__DATE__,__TIME__,wasm-optaus$PATHund der Pointer-Reihenfolge im Exception-Handling-Code bei der bytegenauen Ausgabe schwanken - Die finale Implementierung sichert mit
--no-wasm-opt,setarch --addr-no-randomize, SHA-256-Prüfung pro x86_64 und arm64 sowie CI-Rebuild-Checks die Deterministik innerhalb derselben Architektur
WebAssembly-Proof-of-Work von Anubis und der JavaScript-Ausweichpfad
- Anubis will eine WebAssembly-basierte Proof-of-Work-Prüfung hinzufügen, damit Administratoren für den Schutz ihrer Websites auch andere Verfahren als SHA-256 einsetzen können
- Das zentrale Ziel ist, die Prüflogik nicht getrennt für Client und Server zu implementieren, sondern nur an einer Stelle zu definieren
- Client und Server binden dieselbe WebAssembly ein und führen damit die Prüflogik aus
- Die Struktur ist darauf ausgelegt zu prüfen, ob beide Seiten im Lockstep arbeiten
- Auch Clients mit deaktiviertem WebAssembly werden berücksichtigt
- Es gibt die Vorgabe, Benutzer nicht faktisch von der Website auszuschließen
- Anubis muss zwischen User Experience, Admin Experience und Developer Experience abwägen
- Der gewählte Umweg ist, WebAssembly erneut nach JavaScript zu kompilieren
- Inspiriert von The Birth and Death of JavaScript
- Das resultierende JavaScript ist langsamer als gleichwertige WebAssembly
- Wenn WebAssembly deaktiviert wird, ist manchmal auch das JavaScript-JIT abgeschaltet, was es noch langsamer machen kann
- Ob es auf leistungsschwacher Hardware effizienter ist als bestehendes JavaScript, muss weiter untersucht werden
Warum wasm2js gebündelt werden musste
- Das benötigte Tool ist
wasm2jsaus dem binaryen-Projekt wasm2jsist zwar als Paket in Linux-Distributionen vorhanden, doch die Distributionsversion war veraltet und konnte nicht dieselbe Ausgabe erzeugen wie die Homebrew-Version in der Entwicklungsumgebung- Für reproduzierbare Builds ist Deterministik der Ausgabe essenziell
- Damit Nutzer und Paketierer dem im Anubis-Repository eingecheckten
wasm2js-Binary vertrauen können, müssen sie dieselbe Version selbst bauen und dieselben Bytes erhalten können - Wenn möglich, sollten auch auf anderen Maschinen dieselben Bytes entstehen
- Damit Nutzer und Paketierer dem im Anubis-Repository eingecheckten
- Deshalb wird eine mit wasi-sdk für das WebAssembly-Ziel gebaute Kopie von
wasm2jsmitgeliefert
Wo Reproduzierbarkeit bei C/C++-Builds leicht zerbricht
- Selbst mit denselben Source-Bytes und derselben Eingabe erzeugt ein Compiler nicht immer dieselben Bytes als Ausgabe
- In C/C++ können schon eingebaute Makros wie
__DATE__und__TIME__zu nichtdeterministischer Ausgabe führen- Das Beispiel
hello.cppist so geschrieben, dass es Datum und Uhrzeit des Build-Zeitpunkts ausgibt - Ein Build gab
Jun 18 2026 00:00:59aus, ein andererJun 18 2026 00:01:11 - Der Source-Code bestand aus denselben Bytes, aber die Compiler-Ausgabe war unterschiedlich
- Das Beispiel
- Ein kleiner Compiler könnte theoretisch deterministisch sein, in realen Compilern gibt es aber weit mehr komplexe Variablen
Das Problem, dass Clang stillschweigend wasm-opt aus $PATH ausführte
- Neben
wasm2jsenthält binaryen auchwasm-opt, das die Ausgabe von WebAssembly-Compilern optimiert - Clang ruft während des Builds
wasm-optper Shell-Out auf- Normalerweise ist das ein vernünftiges Verhalten zur Leistungsverbesserung
- Hier haben Versionsunterschiede des
wasm-optin$PATHdie Reproduzierbarkeit zerstört
- Das
wasm-optauf dem DGX Spark war/usr/bin/wasm-optin Version 108, während das Homebrew-wasm-optauf der Workstation Version 130 war wasi-sdkund binaryen hängen von der WebAssembly Exceptions extension ab- Laut Can I use verwenden 93,86 % der Browsernutzer eine Browser-Engine mit Unterstützung dafür
- C++ ist eine Sprache, in der Exceptions stark genutzt werden, daher kann native Exception-Behandlung in WebAssembly Boilerplate reduzieren
- Bei wasmtime und wazero muss Exception-Support explizit aktiviert werden
- An wasmtime kann
-W exceptions=yübergeben werden - Für wazero ist ein benutzerdefinierter Runner-Harness nötig
- An wasmtime kann
- Auf ARM-Maschinen schlug der Build fehl, weil ein altes
wasm-optbeim Auftreffen auf Exception-Instruktionen abstürzte - Durch Übergabe von
--no-wasm-optim Link-Schritt wurde dieser nicht reproduzierbare Pfad entfernt
Wie die Adressbelegung die Erzeugung von Exception-Handling-Code beeinflusste
- Die verwendete Clang-Version zeigte im Exception-Pfad des
wasm2js-Compile-Prozesses eine adresssensitive Code-Erzeugung - Rohe Pointer-Werte beeinflussten die Ausgabereihenfolge einiger
try_table-Blöcke- Pro Build entstand dadurch eine Differenz von etwa 29 Bytes
- Die Berechnung selbst war nahezu identisch, aber die Byte-Reihenfolge änderte sich und damit auch die Catch-Referenzen
- Auch wenn auf einer arm64-Maschine dieselbe festgelegte Version von
wasm2jsgebaut wurde, war die Iterationsreihenfolge der Pointer anders als auf der Workstation, wodurch dasselbe Problem auftrat - Der Workaround bestand aus zwei Maßnahmen
- Mit
setarch --addr-no-randomizewurde die Adressraum-Layout-Zufallsauswahl für diesen Build deaktiviert - Auf vertrauenswürdigen Maschinen wurden jeweils bekannte, gültige SHA-256-Prüfsummen für x86_64 und arm64 erzeugt
- Mit
- In CI wird in
./utils/wasm/wasm2jsnach Ausführung von./build.shdie Prüfsumme verifiziert- Bei Übereinstimmung mit
shasums.x86_64gilt die x86_64-Prüfsumme als bestanden - Bei Übereinstimmung mit
shasums.arm64gilt die arm64-Prüfsumme als bestanden - Wenn beides nicht passt, werden die SHA-256-Werte von
wasm-opt_130.wasmundwasm2js_130.wasmausgegeben und der Lauf schlägt fehl
- Bei Übereinstimmung mit
- Dieser CI-Job läuft sowohl auf x86_64- als auch auf arm64-Hosts
- Reproduzierbarkeit über alle Hosts hinweg ist noch nicht erreicht; das Problem besteht weiterhin als Upstream-LLVM-Bug
- Der aktuelle Stand ist, dass die Builds zumindest innerhalb derselben Architektur deterministisch laufen
1 Kommentare
Lobste.rs-Kommentare
Ich habe heute zum ersten Mal erfahren, dass
clangheimlichwasm-optaus dem$PATHausführt, und das wirkt auf mich völlig absurd.Deshalb habe ich geprüft, ob das auch
zig ccbetrifft, aber zum Glück passiert es nur, wennclangals Linker-Treiber verwendet wird, also nicht in diesem Fall.Wenn
clangdie Reihenfolge anhand der Adressanordnung festlegt, würde ich das persönlich als Bug ansehen und es so melden, falls es sich auch in den neuesten Releases reproduzieren lässt.Seit Jahren wird daran gearbeitet, solche Probleme auszumerzen.
clang.exeunter Windows zuverlässig als Cross-Compiler zu verwenden, wird es noch verrückter.Es gibt gefühlt 500 Arten, auf die clang annimmt, dass es für das native System bauen wird.
Das ist nicht als Kritik gemeint, und ich respektiere, dass es Open Source ist und dass OP einen populären Dienst kostenlos anbietet.
Trotzdem hasse ich es wirklich, dass sich das Web in diese Richtung entwickelt. Es wird immer üblicher, dass beim Betreten einer Website erst eine Anubis-Ladeseite aufblitzt, und ich weiß nicht, ob wir wirklich ein Web wollen, in dem jede populäre Website einen Proof-of-Work-Splashscreen zeigt.
Ich weiß auch nicht, was die Alternative ist, wenn weiterhin AI-Crawler in Massen ankommen, aber ich frage mich, ob es überhaupt Belege dafür gibt, dass Proof of Work AI-Crawler tatsächlich stoppt. Diese Akteure sind extrem gut finanziert und betreiben ohnehin schon weitaus mehr Rechenaufwand, um Seiten zu lesen, daher wirken die Kosten zum Lösen von Proof of Work eher vernachlässigbar.
Im Anubis-Pilotprojekt war es eindeutig ein wirksames Abschreckungsmittel gegen unerwünschten Traffic, und mit Regeln, die fast den Standardeinstellungen entsprachen, wurden weiterhin etwa 90 % der Requests an drei Anwendungen blockiert. DDR lag bei 71,0 %, ArcLight bei 94,6 % und Catalog bei 92,4 %.
Am 30. Mai schoss der Bot-Traffic in die Höhe, und bis zur Einführung von Anubis am 3. Juni war der Katalog praktisch nicht mehr benutzbar. Auf dem Höhepunkt am 1. Juni stieg das auf 3,4 Millionen HTTP-Requests von 2,1 Millionen eindeutigen IPs, mit Seitenladezeiten von über 70 Sekunden. Nach der Einführung von Anubis am 4. Juni war der Dienst für Nutzer wieder erreichbar, die Gesamtzahl der von der Anwendung verarbeiteten Requests sank auf 125.000 und die Seitenladezeit verbesserte sich auf 2,12 Sekunden.
https://lobste.rs/s/ncyfcp/anubis_pilot_project_report_june_2025
In einem anderen Fall war das Problem direkt nach dem Rollout von Anubis gelöst, der genaue Zeitpunkt war im Monitoring sichtbar, und danach gab es keinen einzigen Alarm mehr. Der Angriff lief weiter, aber die Serverlast blieb minimal, sodass Anubis nicht nur gegen AI-Scraper half, sondern auch als DDoS-Schutz funktionierte.
https://lobste.rs/s/67ijih/day_anubis_saved_our_websites_from_ddos
https://orib.dev/tmp/bandwidth.png
Manche Leute blocken sie schon mit einem
meta refresh-Tag oder einem Button, den man anklicken muss. Deshalb funktioniert Anubis, aber der Kern ist nicht Proof of Work selbst, sondern dass das Verhalten unerwartet ist.Es wird schon schlimmer als damals, als man das Web noch mit einem Browser ohne JavaScript nutzte. Das Web sollte einfach dokumentenzentriert sein, stattdessen muss man inzwischen überall erst durch Cloudflare, Anubis und Captcha-Schleusen.
Bots finden immer einen Weg, WAFs zu umgehen, und echte Nutzer verschwenden am Ende nur CPU-Zyklen auf dem Ladebildschirm.
Leider nicht überraschend. Compiler-Toolchains haben eine sehr lange Geschichte, sich auf absurde implizite Abhängigkeiten zu verlassen, nach dem Motto „der lokale Kontext muss einfach genau stimmen“.
LLVM war allerdings eigentlich eine der treibenden Kräfte dabei, solche Abhängigkeiten zu beseitigen, deshalb wirkt es seltsam, so etwas in clang zu sehen. Dadurch wurde es zum Beispiel überhaupt erst möglich, dass der Rust-Compiler ohne separates Cross-Compiler-Konzept auskommt.
Das sieht man sofort, wenn man versucht, ein OS ohne vorhandene Build-Tools zu bootstrappen. Einen Kernel zu bauen, dann eine libc und einen Compiler für genau diesen Kernel zu erzeugen und auszuführen und anschließend auf dem neuen OS alles noch einmal neu zu bauen, ist ein lächerlich komplexer und empfindlicher Prozess voller impliziter Annahmen.
Es ist ein seltenes Problem, das meist nur OS- und Compiler-Entwickler betrifft, deshalb gibt es kaum gute Tools oder Best Practices, und pro Compiler- plus OS-Kombination gibt es weltweit wahrscheinlich nur etwa fünf Menschen, die das Gesamtsystem wirklich verstehen.
Ich hatte auch angenommen, dass die Zig-Toolchain einen Teil dieser Fähigkeiten von LLVM bekommt, wobei ich natürlich verstehe, dass Zig viel Arbeit investiert hat, um die Trennung sauberer zu machen. Jetzt frage ich mich sogar, ob sie LLVM inzwischen gar nicht mehr verwenden.
Wenn clang aber dieselben Probleme hat, scheint LLVM diese sauberere Struktur vielleicht doch nicht so vollständig vererbt zu haben.
Soweit ich weiß, wird Nix verwendet, deshalb frage ich mich, warum Nix nicht erwähnt oder eingesetzt wurde, um wenigstens einen Teil der Umgebungsvariabilität zu reduzieren.
So etwas wie das
wasm-opt-Problem im$PATHhätte sich mit Nix zum Beispiel vielleicht entschärfen lassen. Wurde es verwendet und ich habe es nur übersehen?Ich hatte naiv gedacht, dass es „einfach“ wäre, wasm nach asm.js zu übertragen, aber heute habe ich dazugelernt.
Der Blogtitel wirkt wie Clickbait, aber der Inhalt ist gut.
Ich hasse Clickbait wirklich.