How to diagnose a faulty compiler: the LLVM bug | DSS Monthly Webinar (September 23rd)
三句話摘要
Aave 協議在 zkSync 部署時發現的 LLVM 編譯器最佳化器漏洞:魔術常數架構不匹配導致用戶配置位清除錯誤。 在跨架構編譯場景中,不能假設任何工具(包括業界标准編譯器)的正確性,必須在實際目標虛擬機上進行端到端測試和模糊測試。 防線的失效層級:儘管部署前做了充分測試(單元測試、集成測試、正式驗證、代碼審查),編譯器層的 bug 仍然繞過所有防線,說明需要不信任編譯器正確性的假設,特別是跨架構時。
重點整理
重點- 1
防線的失效層級:儘管部署前做了充分測試(單元測試、集成測試、正式驗證、代碼審查),編譯器層的 bug 仍然繞過所有防線,說明需要不信任編譯器正確性的假設,特別是跨架構時。
- 2
最佳化的隱藏風險:LLVM 為減小代碼大小而做的旋轉左移最佳化邏輯本身正確,但應用到非預期的位寬架構(256 位而非 64 位)時,魔術常數值導致完全不同的語義行為——旋轉出大量 0 位而非預期的 1 位。
- 3
跨鏈部署的脆弱性:同樣代碼在以太坊等其他網路完美運行,卻在 zkSync 異常,證明問題出在編譯鏈中針對 zkSync 的部分,而非應用邏輯,強調了對新環境底層工具的謹慎態度。
實用技巧與重點
乾貨- 編譯流程:Solidity → Solc → UEL → zkSol → LLVM → ZK EVM bytecode
- 問題函數:`setUserBorrowAsCollateral()`,管理 256 位的 user configuration map,每個儲備用 2 位表示(1 bit 借用標誌 + 1 bit 抵押品標誌)
- LLVM 最佳化:
- 原邏輯:左移 1 到目標位置 → 按位取反 → 按位與
- 最佳化後:預先計算按位取反結果作為魔術常數 → 用 rotate-left 把常數的 0 位旋轉到目標位置
- 魔術常數值:
- 設計值(64 位):0xFFFFFFFFFFFFFFFE(63 個 1 後跟 1 個 0)
- 實際值(256 位):0x00...00FFFFFFFFFFFFFFFE(左側隱式補 192 個 0)
- Rotate-left 行為對比:
- 64 位預期:旋轉出 63 個 1,把 1 個 0 旋轉到底部 ✓
- 256 位實際:旋轉出 63 個 1 和 192 個 0,把這批 0 旋轉到底部 ✗
- 影響:索引 3 的借用位清除時,索引 0、1、2 的所有位都被清除
- 漏洞發現工具與方法:
- 最小化測試合約隔離問題
- 編譯並查看 ZK EVM 汇編代碼
- grep 搜尋 `shl` 和 `roll` 指令
- 模式比對發現異常常數
- LLVM 修復:改用 `GetSignConstant()` 為架構生成正確填充的常數(已上游合併,修復於上月)
- 測試工具集:Foundry Suite、Cur Properties Suite、自定義差異監控、正式驗證(EVM 層)、模糊測試
結論
結論“在跨架構編譯場景中,不能假設任何工具(包括業界标准編譯器)的正確性,必須在實際目標虛擬機上進行端到端測試和模糊測試。”
完整解析
詳細Aave 協議是跨鏈借貸平台,為了在 zkSync 這個新的 Layer 2 方案上部署,需要通過 zkSync 自有的虛擬機 ZK EVM 編譯相同的 Solidity 代碼。在激活前,團隊進行了全面測試:單元測試全數通過、集成測試模擬真實場景、代碼審查確認邏輯完全一致、參數配置標準無異常。激活時的治理提案在五天投票期內順利通過,看起來是個常規流程。
但激活後不久,負責用户界面測試的團隊發現了異常現象:執行特定組合操作時,系統表現出完全違反協議邏輯的行為——用户提供兩種抵押品、借入兩種資產、只偿还其中一种的部分金額後,系統竟允許其提取所有抵押品而借款仍未結清。在沒有公開披露的情況下,團隊立即暫停了該資金池。
根本原因搜尋從排除假設開始。由於同樣代碼在以太坊等其他網路表現正常,且部署前驗證了代碼與已上線的其他實例完全一致,問題絕非應用邏輯缺陷。團隊系統地排除了配置問題、輔助合約問題,最終將異常隔離到 `setUserBorrowAsCollateral()` 函數——這個管理用户配置位向量的核心函數。
Sor 的安全研究員應邀深入編譯器層面調查。他們採用了高效的隔離策略:創建一個最小化測試合約,只包含問題函數,編譯後生成簡潔的汇編代碼。在這段短小的汇編中,他們發現了可疑的 `roll`(旋轉左移)指令配合一個看似隨意的魔術常數 0xFFFFFFFFFFFFFFFE。
問題的關鍵在於 LLVM 的編譯器優化策略。為了減小智能合約的部署成本(代碼越小 gas 費越低),LLVM 將原本的三步操作——左移 1、按位取反、按位與——優化成一步:預先將按位取反應用於常數,再用旋轉左移把常數中的 0 位旋轉到目標位置。這個優化在 64 位架構上完美無缺。但 zkSync 的 EVM 使用 256 位字。當 LLVM 接收到這個為 64 位設計的常數時,會自動在左側補充 192 個零位。結果是旋轉操作將這批零位也旋轉了出來,清除比預期更多的位——所有索引小於目標儲備的借用和抵押位都被意外清零。
這個事件最值得反思的地方在於防線層級的失效。正式驗證方法針對 EVM 代碼有效,但未涵蓋 ZK EVM。LLVM 本身是生產級編譯器,被 Rust 等頂級項目廣泛使用。代碼部署前的所有測試都未發現異常。然而,編譯鏈中的架構特性不匹配——64 位常數應用於 256 位運算——造成了漏洞。問題已在 LLVM 上游修複,團隊改用架構感知的常數生成函數。zkSync 隨後更新了編譯器版本,Aave 最終成功激活。
關鍵時刻
Pipeline v2帶時間戳的重點,會在逐字稿層級分析上線後產生。目前請先透過原始影片觀看。
事實查核
Pipeline v2說法查證是下一次管線升級的一部分。KeyFrame 只會顯示它真正能驗證的內容。

