1 points par GN⁺ 11 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Anubis étend la preuve de travail de protection des sites web au-delà de SHA-256 et est en train de concevoir un système où le client et le serveur exécutent la même logique de vérification WebAssembly
  • Pour ne pas exclure les environnements où WebAssembly est désactivé, un chemin de recompilation vers JavaScript a été prévu, mais il est plus lent que WebAssembly et peut l’être encore davantage si le JIT est aussi désactivé
  • La version Linux de wasm2js dans les distributions était obsolète et produisait une sortie différente de celle de Homebrew, ce qui a conduit à embarquer wasm2js compilé avec wasi-sdk pour obtenir des builds reproductibles
  • Dans les builds C/C++, __DATE__, __TIME__, le wasm-opt trouvé via $PATH et l’ordre des pointeurs dans le code de gestion des exceptions peuvent faire varier la sortie octet par octet même avec les mêmes entrées
  • L’implémentation finale obtient une déterminisme intra-architecture grâce à --no-wasm-opt, setarch --addr-no-randomize, une vérification SHA-256 distincte pour x86_64 et arm64, et une validation de rebuild en CI

Preuve de travail WebAssembly d’Anubis et chemin de repli JavaScript

  • Anubis veut ajouter une vérification de proof-of-work basée sur WebAssembly afin que les administrateurs puissent utiliser pour la protection de leur site web un mécanisme de preuve de travail autre que SHA-256
  • L’objectif principal est de ne pas implémenter séparément la logique de vérification côté client et côté serveur, mais de la définir à un seul endroit
    • Le client et le serveur se branchent sur le même WebAssembly pour exécuter la logique de vérification
    • L’architecture vise à vérifier que les deux côtés fonctionnent en lockstep
  • Les clients avec WebAssembly désactivé restent aussi dans le périmètre
    • Il y a la contrainte de ne pas vouloir exclure de fait les utilisateurs du site web
    • Anubis doit trouver un équilibre entre l’expérience utilisateur, l’expérience administrateur et l’expérience développeur
  • Le contournement retenu consiste à recompiler WebAssembly en JavaScript
    • L’idée s’inspire de The Birth and Death of JavaScript
    • Le JavaScript obtenu est plus lent qu’un WebAssembly équivalent
    • Désactiver WebAssembly peut aussi désactiver le JIT JavaScript, ce qui peut encore ralentir l’exécution
    • Il faut encore étudier si cette approche est plus efficace que le JavaScript existant sur du matériel peu puissant

Pourquoi il a fallu embarquer wasm2js

  • L’outil nécessaire est wasm2js du projet binaryen
  • wasm2js existe sous forme de paquet dans les distributions Linux, mais la version fournie par les distributions était trop ancienne et ne produisait pas la même sortie que la version Homebrew utilisée en développement
  • Les builds reproductibles exigent un caractère déterministe de la sortie
    • Pour que les utilisateurs et les packageurs puissent faire confiance au binaire wasm2js commité dans le dépôt Anubis, il faut pouvoir reconstruire la même version soi-même et obtenir exactement les mêmes octets
    • Idéalement, il faudrait aussi obtenir les mêmes octets sur la machine d’une autre personne
  • Pour cela, une copie de wasm2js compilée avec wasi-sdk pour la cible WebAssembly est incluse

Là où la reproductibilité se casse facilement dans les builds C/C++

  • Même avec les mêmes octets source et les mêmes entrées, la sortie du compilateur n’est pas forcément toujours identique octet pour octet
  • En C/C++, de simples macros intégrées comme __DATE__ et __TIME__ suffisent à produire une sortie non déterministe
    • L’exemple hello.cpp est écrit pour afficher la date et l’heure du build
    • Un build a affiché Jun 18 2026 00:00:59, un autre Jun 18 2026 00:01:11
    • Le code source avait les mêmes octets, mais la sortie du compilateur était différente
  • Un petit compilateur pourrait en théorie être déterministe, mais en pratique les compilateurs réels comportent beaucoup plus de variables complexes

Le problème de Clang qui exécutait silencieusement wasm-opt trouvé dans $PATH

  • En plus de wasm2js, binaryen fournit wasm-opt, qui optimise la sortie des compilateurs WebAssembly
  • Clang exécute wasm-opt via un appel externe pendant le build
    • En général, c’est un comportement raisonnable pour améliorer les performances
    • Ici, la différence de version du wasm-opt présent dans $PATH a cassé la reproductibilité
  • Sur la DGX Spark, wasm-opt était /usr/bin/wasm-opt en version 108, tandis que sur la workstation la version Homebrew de wasm-opt était la version 130
  • wasi-sdk et binaryen dépendent de la WebAssembly Exceptions extension
    • Selon Can I use, 93,86 % des utilisateurs de navigateurs utilisent un moteur prenant cela en charge
    • Le C++ utilise beaucoup les exceptions, et la gestion native des exceptions en WebAssembly peut réduire le boilerplate
  • Avec wasmtime et wazero, il faut activer explicitement la prise en charge des exceptions
    • On peut passer -W exceptions=y à wasmtime
    • wazero nécessite un harnais d’exécution personnalisé
  • Sur les machines arm, un ancien wasm-opt s’arrêtait lorsqu’il rencontrait des instructions de gestion des exceptions, ce qui faisait échouer le build
  • Le passage de --no-wasm-opt à l’étape d’édition de liens supprime ce chemin de non-reproductibilité

L’impact de l’agencement des adresses sur la génération du code de gestion des exceptions

  • La version de Clang utilisée montrait une génération de code sensible aux adresses dans le chemin de gestion des exceptions lors de la compilation de wasm2js
  • Les valeurs de pointeurs brutes influençaient l’ordre de sortie de certains blocs try_table
    • Cela provoquait environ 29 octets de différence à chaque build
    • Les calculs restaient presque identiques, mais l’ordre des octets changeait et les références catch aussi
  • Même en compilant la même version figée de wasm2js sur une machine arm64, l’ordre d’itération des pointeurs différait de celui de la workstation, provoquant le même problème
  • Deux contournements ont été mis en place
    • setarch --addr-no-randomize désactive la randomisation de l’espace d’adressage pour ce build
    • Des checksums SHA-256 de référence pour x86_64 et arm64 sont générés sur des machines de confiance
  • La CI exécute ./build.sh depuis ./utils/wasm/wasm2js, puis vérifie les checksums
    • Si cela correspond à shasums.x86_64, le checksum x86_64 est validé
    • Si cela correspond à shasums.arm64, le checksum arm64 est validé
    • Si rien ne correspond, la CI affiche les SHA-256 de wasm-opt_130.wasm et wasm2js_130.wasm puis échoue
  • Cette tâche CI tourne à la fois sur des hôtes x86_64 et arm64
  • La reproductibilité sur l’ensemble des hôtes n’est pas encore acquise, et le problème reste un bug LLVM upstream
  • Dans l’état actuel, au moins à l’intérieur d’une même architecture, le build fonctionne de manière déterministe

1 commentaires

 
Avis sur Lobste.rs
  • C’est la première fois que j’apprends que clang exécute discrètement wasm-opt depuis le $PATH, et ça me paraît complètement absurde
    J’ai vérifié si cela affectait aussi zig cc, mais heureusement ce n’est exécuté que lorsqu’on utilise clang comme pilote de l’éditeur de liens, donc ce n’était pas concerné
    Si clang détermine l’ordre en dépendant de la disposition des adresses, personnellement je considérerais ça comme un bug, et si c’est toujours reproductible dans la dernière version, je le signalerais comme tel

    • Xe a dit qu’il le signalerait en amont ailleurs, et c’est clairement un bug de déterminisme de LLVM
      Des efforts sont en cours depuis des années pour éliminer ce genre de problème
    • Essayer d’utiliser clang.exe de façon fiable comme compilateur croisé sous Windows rendrait encore plus fou
      clang suppose d’environ 500 façons qu’il va construire pour le système natif
  • Ce n’est pas pour critiquer, et je respecte le fait que ce soit open source et que l’OP fournisse gratuitement un service populaire
    Mais je déteste vraiment voir le web évoluer ainsi. Il devient courant d’arriver sur un site et de voir surgir un écran de chargement Anubis ; je ne sais pas si on veut vraiment d’un web où chaque site populaire affiche un écran de preuve de travail
    Je ne sais pas non plus quelle est l’alternative avec l’afflux permanent de crawlers IA, mais je me demande aussi s’il existe des preuves que la preuve de travail bloque réellement les crawlers IA. Ils disposent de financements énormes et effectuent déjà bien plus de calcul pour lire les pages, donc le coût de résolution de la preuve de travail semble minime

    • Il existe des preuves que la preuve de travail bloque les crawlers IA. Plusieurs billets à ce sujet ont déjà été publiés ici
      Dans le pilote Anubis, cela a clairement été un moyen de dissuasion efficace contre le trafic indésirable, et avec des règles proches du minimum, environ 90 % des requêtes vers trois applications ont continué à être bloquées. DDR était à 71,0 %, ArcLight à 94,6 % et Catalog à 92,4 %
      Le 30 mai, le trafic de bots a explosé, rendant le catalogue pratiquement indisponible jusqu’au déploiement d’Anubis le 3 juin ; au pic du 1er juin, on a atteint 3,4 millions de requêtes HTTP depuis 2,1 millions d’IP uniques, avec des temps de chargement dépassant 70 secondes. Après le déploiement d’Anubis le 4 juin, le service est redevenu accessible aux utilisateurs ; le nombre total de requêtes traitées par l’application est tombé à 125 000 et le temps de chargement des pages s’est amélioré à 2,12 secondes
      https://lobste.rs/s/ncyfcp/anubis_pilot_project_report_june_2025
      Dans un autre cas, le problème a été résolu immédiatement après le déploiement d’Anubis ; le moment exact était visible dans la supervision, et il n’y a ensuite eu plus aucune alerte. L’attaque continuait, mais la charge serveur était au plus bas ; Anubis semble donc avoir servi non seulement à bloquer les scrapers IA, mais aussi de protection DDoS
      https://lobste.rs/s/67ijih/day_anubis_saved_our_websites_from_ddos
    • La preuve de travail n’est pas forcément nécessaire. J’ai déployé sur https://shithub.us un bloqueur sans état et sans JS ; pour qu’un scraper le contourne, cela lui coûtera probablement au moins autant qu’une simple preuve de travail
      https://orib.dev/tmp/bandwidth.png
    • Personne ne sait vraiment ce que sont exactement ces crawlers ni à quoi ils servent, mais beaucoup semblent assez paresseux et gèrent mal les traitements inhabituels
      Certaines personnes les bloquent juste avec une balise meta refresh ou un bouton sur lequel il faut cliquer. Donc Anubis fonctionne, mais l’essentiel n’est pas la preuve de travail en elle-même : c’est le comportement inattendu
    • Je ne veux clairement pas d’un web comme ça
      C’est devenu encore plus pénible que lorsqu’on utilisait le web avec un navigateur JavaScript désactivé. J’aimerais que le web reste simplement centré sur les documents, mais maintenant il faut partout passer par Cloudflare, Anubis et des barrières de CAPTCHA
    • Le fait que n’importe quel APT puisse accélérer le calcul des réponses Anubis était connu dès le départ. C’est même écrit dans la preuve de concept de l’an dernier
      Les bots finissent toujours par trouver un moyen de contourner les WAF, tandis que les vrais utilisateurs gaspillent des cycles CPU sur un écran de chargement
  • C’est regrettable, mais pas surprenant. Les toolchains de compilation ont une très longue histoire de dépendances implicites absurdes du genre « le contexte local doit simplement être correct »
    Cela dit, LLVM fait partie de ceux qui ont mené la charge pour éliminer ce type de dépendances, donc voir ça dans clang est assez étrange. C’est d’ailleurs ce qui a permis, par exemple, au compilateur Rust d’exister sans notion distincte de compilateur croisé
    Cela saute immédiatement aux yeux quand on essaie de bootstrapper un OS sans s’appuyer sur les outils de build existants. Construire un noyau, puis une libc et un compilateur pour ce noyau, les exécuter, puis tout reconstruire sur le nouvel OS est un processus ridiculement complexe et fragile, rempli d’hypothèses implicites
    Comme c’est un problème rare qui ne concerne surtout que les développeurs d’OS et de compilateurs, il existe très peu de bons outils ou de bonnes pratiques, et pour chaque combinaison compilateur+OS il doit y avoir à peine cinq personnes dans le monde qui comprennent réellement l’ensemble

    • Ça me surprend de l’entendre. Je pensais que, contrairement à GCC, LLVM était conçu en abstrahant l’hôte et la cible, avec le cross-compiling en tête
      Je pensais aussi que la toolchain Zig tirait une partie de cette capacité de LLVM, même si je comprends bien sûr qu’ils ont beaucoup travaillé pour séparer tout cela plus proprement. Je me demande même s’ils utilisent encore LLVM aujourd’hui
      Mais si clang a les mêmes problèmes, alors j’ai l’impression que LLVM n’a peut-être pas transmis une architecture aussi propre que je le pensais
  • Il me semblait que tu utilisais Nix ; je me demande pourquoi tu n’as pas mentionné ou utilisé Nix pour au moins réduire une partie de la variabilité de l’environnement
    Par exemple, pour un problème comme wasm-opt dans le $PATH, Nix aurait sans doute pu l’atténuer ; ou bien tu l’as utilisé et je l’ai raté ?

  • J’imaginais naïvement que convertir du wasm vers asm.js serait « facile », mais aujourd’hui j’ai appris quelque chose

    • Moi aussi je le pensais. Malheureusement, en pratique, c’est bien plus complexe qu’on ne l’imagine
  • Le titre du blog ressemble à du clickbait, mais le contenu est bon
    Je déteste vraiment le clickbait