Anubis 與可重複 WebAssembly 建置的挑戰
Anubis 專案正在實作基於 WebAssembly 的工作量證明 (PoW) 檢查,以允許管理員使用非 SHA256 方法來保護網站。為了在客戶端和伺服器端維持檢查邏輯的單一事實來源,Anubis 使用了 WebAssembly;然而,為了支援禁用 WebAssembly 的使用者,該專案使用 Binaryen 專案中的 wasm2js 工具將該 WebAssembly 重新編譯為 JavaScript。
實現可重複建置(即相同的輸入位元組始終產生相同的輸出位元組)對於提交至 Anubis 儲存庫的 wasm2js 二進位檔的信任與驗證至關重要。然而,這個過程揭示了現代編譯器工具鏈中的顯著非決定性。
編譯器非決定性的來源
編譯器並不總是其輸入的決定性函數。幾個因素可能導致相同的原始碼在不同的建置或環境中產生不同的二進位輸出。
建置時中繼資料
C/C++ 開發中最常見的非決定性來源之一是使用內建巨集,例如 __DATE__ 和 __TIME__。這些巨集會在二進位檔中蓋上確切的執行時間戳記,確保即使原始碼保持不變,每一次建置都會產生不同的位元組。
隱含的工具鏈依賴
編譯器通常依賴於系統 $PATH 中可用的外部工具。以 wasi-sdk 為例,Clang 可能會在靜默地呼叫 wasm-opt 來優化 WebAssembly 輸出。這會對主機機器上安裝的特定版本的 wasm-opt 產生依賴。例如,在安裝有 wasm-opt 版本 108 的機器上進行建置,與在安裝有版本 130 的機器上進行建置,結果可能會失敗或產生不同的結果,特別是在處理 WebAssembly Exceptions 擴充功能時。
為了減輕這種情況,Anubis 的建置過程在連結步驟中使用 --no-wasm-opt 旗標來移除此外部依賴。
特定架構的二進位檔分歧
即使控制了中繼資料和外部工具,低階程式碼生成也可能根據主機架構的記憶體佈局而有所不同。
位址敏感型程式碼生成
Clang 的異常處理路徑包含位址敏感型程式碼生成,其中原始指標值可能會洩漏到 try_table 區塊的排序中。這導致二進位檔在不同建置之間,或在不同架構(例如 x86_64 與 arm64)上建置時,由於不同的指標迭代順序而產生數個位元組的差異。
為了在單一架構內解決此問題,採取了以下步驟:
- 禁用 ASLR:使用
setarch --addr-no-randomize在建置期間禁用位址空間隨機化。 - 檢查碼驗證:為 x86_64 和 arm64 架構建立已知的正確 SHA256 檢查碼。
- CI 驗證:實作一個 CI 工作,在兩種架構上重新建置模組並根據記錄的檢查碼進行驗證。
社群對可重複性的觀點
對於決定性建置的努力引發了開發者社群中的幾項技術討論:
- Hermetic 建置系統:一些貢獻者建議,像 Nix 這樣的工具可以透過使用沙盒來解決這些問題,沙盒會攔截對時間的呼叫並將其替換為常數(epoch 0)以確保決定性。
- LLVM Bugs:技術分析顯示,LLVM 中對
DenseMap的非決定性迭代可能是指標洩漏問題的根源,建議改用MapVector以保證決定性迭代順序。 - PoW 倫理:一些使用者對強制客戶端執行工作量證明迴圈以防止爬蟲行為所造成的能源消耗和可及性影響提出了疑慮。
"如果 Clang 因為指標位址而產生非決定性輸出,那麼這就是一個 bug... 最常見的情況是某個程式碼路徑正在對一個 DenseMap 進行迭代,而它是非決定性的。"
雖然現在在特定架構內可以實現決定性建置,但實現跨架構的可重複性仍然是 LLVM 上游的挑戰。