1 ポイント 投稿者 GN⁺ 11 시간 전 | 1件のコメント | WhatsAppで共有
  • Anubis は、Web サイト保護用のプルーフ・オブ・ワークを SHA-256 の外へ拡張するにあたり、クライアントとサーバーが 同じ WebAssembly 検査ロジック を実行するよう設計している
  • WebAssembly が無効な環境も排除しないために JavaScript 再コンパイル経路 を用意したが、WebAssembly より遅く、JIT まで無効ならさらに遅くなる可能性がある
  • Linux ディストリビューションの wasm2js が古く、Homebrew 版と出力が異なっていたため、再現可能なビルドのために wasi-sdk でビルドした wasm2js をバンドルすることになった
  • C/C++ ビルドでは、__DATE____TIME__$PATH 上の wasm-opt、例外処理コードのポインタ順序によって、同じ入力でも バイト単位の出力 が揺らぐことがある
  • 最終的な実装では、--no-wasm-optsetarch --addr-no-randomize、x86_64・arm64 別の SHA-256 検証、CI での再ビルド確認により アーキテクチャ内での決定性 を確保した

Anubis の WebAssembly プルーフ・オブ・ワークと JavaScript 代替経路

  • Anubis は、管理者が SHA-256 ではないプルーフ・オブ・ワーク方式を Web サイト保護に使えるよう、WebAssembly ベースの proof-of-work 検査 を追加しようとしている
  • 中核となる目標は、検査ロジックをクライアントとサーバーで別々に実装せず 1 か所だけに定義 すること
    • クライアントとサーバーは同じ WebAssembly に接続して検査ロジックを実行する
    • 両者が lockstep で動作しているか確認する構造を目指している
  • WebAssembly が無効なクライアントも考慮対象
    • ユーザーを事実上 Web サイトから締め出したくないという制約がある
    • Anubis はユーザー体験、管理者体験、開発者体験のあいだでバランスを取る必要がある
  • 選ばれた回避策は、WebAssembly を JavaScript に再コンパイルする方式
    • The Birth and Death of JavaScript から着想を得ている
    • 結果として得られる JavaScript は、同等の WebAssembly より遅い
    • WebAssembly を無効化すると JavaScript JIT も一緒に無効になる場合があり、さらに遅くなる可能性がある
    • 低性能ハードウェアで従来の JavaScript より効率的かどうかは、追加調査が必要

wasm2js をバンドルする必要があった理由

  • 必要なツールは binaryen プロジェクトの wasm2js
  • wasm2js は Linux ディストリビューションでパッケージとして存在するが、ディストリビューション版が古く、開発環境の Homebrew 版と同じ出力を生成できなかった
  • 再現可能なビルドには、出力の 決定性 が必須
    • Anubis リポジトリにコミットされる wasm2js バイナリをユーザーやパッケージャーが信頼するには、同じバージョンを自分でビルドして同じバイト列を得られる必要がある
    • 可能なら、他人のマシンでも同じバイト列が出るべき
  • そのため wasm2jswasi-sdk で WebAssembly 向けにビルドしたコピーを同梱している

C/C++ ビルドで再現性が壊れやすいポイント

  • 同じソースバイトと同じ入力を与えても、コンパイラ出力が常に同じバイト列になるとは限らない
  • C/C++ では __DATE____TIME__ のような組み込みマクロだけでも 非決定的な出力 が生じる
    • 例の hello.cpp は、ビルド時点の日付と時刻を出力するよう書かれている
    • あるビルドは Jun 18 2026 00:00:59 を出力し、別のビルドは Jun 18 2026 00:01:11 を出力した
    • ソースコードのバイト列は同じでも、コンパイラ出力は異なっていた
  • 小規模なコンパイラなら理論上は決定的にできるかもしれないが、実際のコンパイラにはもっと複雑な変数が多い

Clang が $PATH 上の wasm-opt を密かに実行した問題

  • binaryen には wasm2js のほかに、WebAssembly コンパイラ出力を最適化する wasm-opt がある
  • Clang はビルド中に wasm-opt を shell out で実行する
    • 通常は性能向上のための合理的な動作
    • 今回は $PATH 上にある wasm-opt のバージョン差が再現性を壊した
  • DGX Spark の wasm-opt/usr/bin/wasm-opt の version 108 で、ワークステーションの Homebrew wasm-opt は version 130 だった
  • wasi-sdk と binaryen は WebAssembly Exceptions extension に依存している
    • Can I use によると、ブラウザユーザーの 93.86% がこれをサポートするブラウザエンジンを使っている
    • C++ は例外を多用する言語なので、WebAssembly ネイティブ例外処理はボイラープレート削減に役立つ
  • wasmtime と wazero では例外サポートを明示的に有効化する必要がある
    • wasmtime には -W exceptions=y を渡せる
    • wazero にはカスタムランナーハーネスが必要
  • arm マシンの古い wasm-opt は例外処理命令に遭遇すると終了し、ビルドが失敗した
  • リンク段階に --no-wasm-opt を渡し、この 非再現な経路 を除去した

アドレス配置が例外処理コード生成に与えた影響

  • 使用中の Clang バージョンは、wasm2js コンパイル過程の例外処理経路でアドレスに敏感なコード生成を示した
  • 生ポインタ値が一部の try_table ブロックの出力順序に影響した
    • ビルドごとに約 29 バイトの差が発生した
    • 計算自体はほぼ同じだが、バイト順序が変わり、catch 参照も変わる
  • arm64 マシンで同じ固定バージョンの wasm2js をビルドしても、ポインタの反復順序がワークステーションと異なり、同じ問題が発生した
  • 回避策は 2 つある
    • setarch --addr-no-randomize でそのビルドの アドレス空間配置ランダム化 を無効にする
    • 信頼できるマシンで x86_64 と arm64 それぞれの known-good SHA-256 チェックサムを生成する
  • CI は ./utils/wasm/wasm2js./build.sh を実行したあと、チェックサムを検証する
    • shasums.x86_64 と一致すれば x86_64 チェックサム通過として扱う
    • shasums.arm64 と一致すれば arm64 チェックサム通過として扱う
    • どちらとも一致しなければ wasm-opt_130.wasmwasm2js_130.wasm の SHA-256 を出力して失敗する
  • この CI ジョブは x86_64 と arm64 の両ホストで実行される
  • ホスト全体をまたぐ再現性はまだ確保されておらず、この問題は upstream LLVM バグとして残っている
  • 現時点では少なくとも アーキテクチャ内 ではビルドが決定的に動作する

1件のコメント

 
Lobste.rsの意見
  • clang がこっそり $PATH 上の wasm-opt を実行するなんて初めて知ったし、本当にありえないと感じた
    このせいで zig cc にも影響があるのか確認したが、幸い clangリンカードライバ として使うときだけ実行されるので該当しなかった
    clang がアドレス配置に依存して順序を決めるのなら、個人的には バグ だと見なすし、最新リリースでも再現するならそう報告すると思う

    • Xe が別の場所で upstream に報告すると言っていたし、これは間違いなく LLVM の決定性バグ
      こういう問題をなくそうとする取り組みは何年も続いてきた
    • Windows で clang.exeクロスコンパイラ として安定して使おうとすると、さらに気が狂いそうになる
      clang にはネイティブシステム向けにビルドする前提のやり方が 500 個くらいある
  • 批判したいわけではなく、オープンソースで OP が人気のあるサービスを無料で提供している点は尊重している
    それでも Web がこう変わっていくのは本当に嫌だ。Web サイトに入るたびに Anubis のローディングページ が派手に表示されるのが当たり前になってきたが、人気サイトごとにプルーフ・オブ・ワークのスプラッシュ画面を見せられる Web を望んでいるのか分からない
    AI クローラが押し寄せ続けているので代案が何かも分からないが、プルーフ・オブ・ワークが本当に AI クローラを防げるという証拠があるのかも疑わしい。彼らは莫大な資金を持っており、ページを読むためにすでにずっと多くの計算をしているのだから、プルーフ・オブ・ワークを解くコストはごく小さいように見える

    • プルーフ・オブ・ワークが AI クローラを防ぐという証拠はある。ここにも関連投稿が何度も上がっていた
      Anubis パイロットでは望ましくないトラフィックに対して確かに効果的な抑止手段であり、ほぼデフォルトに近いルールだけで 3 つのアプリケーションへのリクエストの約 90% を継続的に遮断したという。DDR は 71.0%、ArcLight は 94.6%、Catalog は 92.4% だった
      5 月 30 日にボットトラフィックが急増し、6 月 3 日に Anubis を適用するまで Catalog は事実上サービス不能になっていた。6 月 1 日のピーク時には 210 万のユニーク IP から 340 万件の HTTP リクエストがあり、ページ読み込み時間は 70 秒超まで跳ね上がった。6 月 4 日に Anubis を適用した後は再びユーザーにサービスを提供できるようになり、アプリケーションが処理した総リクエスト数は 12.5 万件、ページ読み込み時間は 2.12 秒に改善した
      https://lobste.rs/s/ncyfcp/anubis_pilot_project_report_june_2025
      別の事例でも Anubis をデプロイした直後に問題が解決し、モニタリングで正確な時点を確認でき、その後はアラートが 1 件もなかったという。攻撃は続いていたがサーバー負荷は最低水準で、Anubis は AI スクレイパーの遮断だけでなく DDoS 防御 としても機能したと見られている
      https://lobste.rs/s/67ijih/day_anubis_saved_our_websites_from_ddos
    • 必ずしもプルーフ・オブ・ワークは必要ではない。https://shithub.usステートレスで JS 不要のブロッカー をデプロイしたが、スクレイパーが回避しようとすれば単純なプルーフ・オブ・ワークと同程度のコストがかかる可能性が高い
      https://orib.dev/tmp/bandwidth.png
    • クローラが正確に何で、何のためのものかは誰にも分からないが、多くのクローラはかなり怠惰で、変わった処理をうまくこなせないようだ
      meta refresh タグやクリックしなければならないボタンだけで防げる場合もある。だから Anubis は機能するのだが、要点は プルーフ・オブ・ワーク そのものではなく、想定外の動作にある
    • こういう Web はまったく望まない
      JavaScript を無効にしたブラウザで Web を使っていた頃よりも、さらに苦しいものになっている。Web はただ文書中心であってほしいのに、今ではどこでも Cloudflare、Anubis、CAPTCHA の関門を通らなければならない
    • どんな APT でも Anubis の応答計算を高速化できることは最初から知られていた。昨年の 概念実証 にもそう書かれている
      ボットは常に WAF を回避する方法を見つけ、実際のユーザーはローディング画面で CPU サイクルを無駄にすることになるという内容だ
  • 残念だが驚きではない。コンパイラのツールチェーンは、「ローカルな文脈がただ正しくなければならない」というような、ばかげた暗黙の依存に頼ってきた長い歴史がある
    ただ、LLVM はそうした依存を取り除く先頭に立ってきた側なので、clang でこういうものを見るのは奇妙だ。そのおかげで、たとえば Rust コンパイラは別個の クロスコンパイラ という概念なしでも成り立つようになった
    既存のビルドツールに頼らずに OS をブートストラップしてみると、すぐに明らかになる。カーネルを作り、そのカーネル向けの libc とコンパイラを作って実行し、新しい OS 上でもう一度すべてをビルドし直す過程は、暗黙の前提だらけの途方もなく複雑で繊細な作業だ
    OS とコンパイラの開発者にしか主に関わらない珍しい問題なので、良いツールやベストプラクティスもほとんどなく、コンパイラ + OS の組み合わせごとに実際に全体を理解している人は世界に 5 人くらいしかいない気がする

    • この話を聞いて驚いた。LLVM は GCC と違ってホストとターゲットを抽象化し、クロスコンパイル を念頭に置いて設計されたのだと思っていた
      Zig のツールチェーンもそうした機能の一部を LLVM から得ているのだと思っていたし、もちろんそれをさらにきれいに分離するために多くの作業をしたのは理解している。今はもう LLVM を使っていないのかも気になる
      しかし clang にも同じ問題があるなら、LLVM からもっとクリーンな構造を受け継いだわけではなかったのかと思ってしまう
  • Nix を使っているはずだが、環境の変動性を少しでも減らすために Nix に触れたり使ったりしなかった理由が気になる
    たとえば $PATH 上の wasm-opt 問題のようなものは Nix で緩和できた気がするが、使っていて自分が見落としているだけだろうか?

  • 素朴には wasm を asm.js に移すのは「簡単」だろうと思っていたが、今日新しく学んだ

    • 私もそう思っていた。残念ながら実際には思っていたよりずっと複雑だ
  • ブログのタイトルは クリックベイト っぽいが、内容は良い
    クリックベイトは本当に嫌いだ