Anubisと再現可能なWebAssemblyビルドの課題
Anubisプロジェクトは、管理者がSHA256以外の手法を用いてウェブサイトを保護できるようにするため、WebAssemblyベースのプルーフ・オブ・ワーク(PoW)チェックを実装しています。クライアントとサーバーの両方でチェックロジックの単一の信頼できる情報源(single source of truth)を維持するために、AnubisはWebAssemblyを使用しています。しかし、WebAssemblyを無効にしているユーザーをサポートするために、プロジェクトはBinaryenプロジェクトのwasm2jsツールを使用して、そのWebAssemblyをJavaScriptに再コンパイルしています。
同じ入力バイトが常に同じ出力バイトを生成するという「再現可能なビルド」を実現することは、Anubisリポジトリにコミットされるwasm2jsバイナリの信頼性と検証において極めて重要です。しかし、このプロセスにより、現代のコンパイラツールチェーン内における重大な非決定性が明らかになりました。
コンパイラの非決定性の原因
コンパイラは、必ずしも入力に対する決定論的な関数ではありません。いくつかの要因により、同じソースコードであっても、異なるビルドや環境によって異なるバイナリ出力が生成されることがあります。
ビルド時のメタデータ
C/C++開発において最も一般的な非決定性の原因の一つは、__DATE__や__TIME__のような組み込みマクロの使用です。これらのマクロは、実行時の正確な時刻をバイナリに刻印するため、ソースコードが変更されていなくても、すべてのビルドが異なるバイトを生成することを保証してしまいます。
暗黙的なツールチェーンの依存関係
コンパイラは、しばしばシステムの $PATH で利用可能な外部ツールに依存します。wasi-sdkの場合、ClangはWebAssembly出力を最適化するために、バックグラウンドでwasm-optを呼び出すことがあります。これにより、ホストマシンにインストールされているwasm-optの特定のバージョンへの依存関係が生じます。例えば、wasm-optバージョン108を搭載したマシンでのビルドは、特にWebAssembly Exceptions拡張を扱う場合、バージョン130を搭載したマシンでのビルドとは異なる結果を生じさせるか、あるいは失敗する可能性があります。
これを軽減するために、Anubisのビルドプロセスでは、リンクステップ中に--no-wasm-optフラグを使用し、この外部依存関係を排除しています。
アーキテクチャ固有のバイナリの乖離
メタデータや外部ツールが制御されている場合でも、低レベルのコード生成はホストアーキテクチャのメモリレイアウトに基づいて変化することがあります。
アドレスに敏感なコード生成
Clangの例外処理パスには、ポインタの生の値がtry_tableブロックの順序に漏れ出すことのある、アドレスに敏感なコード生成が含まれています。これにより、ポインタの反復順序の違いにより、ビルド間、あるいは異なるアーキテクチャ(例:x86_64対arm64)でビルドする場合に、バイナリが数バイト異なる結果となります。
これを単一のアーキテクチャ内で解決するために、以下の手順が取られました:
- ASLRの無効化: ビルド中にアドレス空間のランダム化を無効にするために、
setarch --addr-no-randomizeを使用する。 - チェックサム検証: x86_64とarm64の両方のアーキテクチャに対して、既知の正常なSHA256チェックサムを作成する。
- CI検証: 両方のアーキテクチャでモジュールを再ビルドし、記録されたチェックサムと照合して検証するCIジョブを実装する。
コミュニティの視点による再現性について
決定論的なビルドのための苦闘は、開発者コミュニティの間でいくつかの技術的な議論を巻き起こしました:
- Hermetic Build Systems(密閉型ビルドシステム): 一部のコントリビューターは、Nixのようなツールが、時刻への呼び出しをキャッチして定数(epoch 0)に置き換えるサンドボックスを使用することで、これらの問題を解決できると提案しました。
- LLVMのバグ: 技術的な分析によれば、LLVM内の
DenseMapに対する非決定論的な反復がポインタ漏洩問題の根本原因である可能性があり、MapVectorへの移行を提案しています。決定論的な反復順序をを確保するために。 - PoWの倫理: 一部のユーザーは、スクレイピングを防ぐためにクライアントにプルーフ・オブ・ワークのループを実行させることによる、エネルギー消費とアクセシビリティへの影響について懸念を表明しました。
"Clangがポインタアドレスによって非決定論的な出力を生成するのであれば、それはバグです... これが起こる最も一般的な方法は、何らかのコードパスが非決定論的なDenseMapを反復している場合です。"
現在、ビルドは特定のアーキテクチャ内では決定論的になっていますが、クロスアーキテクチャの再現性を実現することは、依然としてアップストリームのLLVMの課題として残っています。