Unveiling WasmCov — Noah Jelich | Independent Security Researcher
三句話摘要
針對 WebAssembly 智能合約整合測試的覆蓋率盲點,工程師從 LLVM 編譯器內部挖出解法,打造出 wasm-cov 工具。 wasm 覆蓋率的本質問題在 LLVM→wasm 管道,繞過它的辦法是保留 IR、跨編譯至 x86、並用 regex 抹除 wasm 記憶體操作——wasm-cov 把這條艱難路徑包裝成四行指令,讓智能合約開發者終於能看到整合測試的真實覆蓋率。 本地測試 100% 覆蓋率是假象:Near、Solana 等鏈的 Rust 智能合約使用 wasm-bindgen 巨集,這些巨集只在編譯至 wasm32 時生效,導致本地單元測試完全繞過真實執行路徑,開發者以為覆蓋全了,部署後才發現認證函數根本無法呼叫。
重點整理
重點- 1
本地測試 100% 覆蓋率是假象:Near、Solana 等鏈的 Rust 智能合約使用 wasm-bindgen 巨集,這些巨集只在編譯至 wasm32 時生效,導致本地單元測試完全繞過真實執行路徑,開發者以為覆蓋全了,部署後才發現認證函數根本無法呼叫。
- 2
LLVM IR 到 wasm 這段管道是壞的:rust→IR 沒問題,但 IR→wasm 這步會遺失 llvm covermap 資料,導致 llvm-cov 無法生成報告。根本原因是 wasm runtime 生態極度碎片化(wasmer、wasi、wasmtime 等幾十種),標準化進度落後,開發者體驗無人顧及。
- 3
跨編譯繞過是關鍵突破:既然 IR 是正確的,就不走 wasm 而改編譯至 x86。再用 regex 將 wasm 特有的記憶體操作(`memory.grow`/`memory.shrink` 等)全部替換成 `unreachable`,排除指標相容性問題後,覆蓋率報告即可正常輸出。
- 4
UX 優先讓工具可落地:作者刻意將二十幾個手動步驟(LLVM 版本對齊、IR 修改、regex 替換、重新編譯)封裝成單一 CLI,讓使用者只需 `setup → build → run tests → finalize` 四步,並提供標準化的 VM patch 介面,降低各鏈移植門檻。
實用技巧與重點
乾貨- 受影響平台:Near Protocol、Solana、CosmWasm(皆用 Rust + wasm32)
- 真實案例:Sweatcoin 審計中手動發現認證函數失效漏洞,該專案已被硬編碼進 Near Protocol 節點
- 工具鏈:`minicov`(embedded 環境 profraw 擷取)、`llvm-cov`、`llvm` IR 跨編譯
- Rust coverage 穩定版起點:約 rustc 1.40+(從 `-Z` 轉為 `-C instrument-coverage`)
- 修復關鍵 regex:將 wasm 記憶體操作替換為 `unreachable`
- 已知坑:Link Time Optimization(LTO)會導致工具失效,需關閉
- 報告內容:function / line / region 覆蓋率,含所有相依套件(可手動過濾)
- 採用方:radix(完整整合)、wasm-bindgen、wasm-pack、Cloudflare Workers(參考實作)
- 開發耗時:約一個半月兼職工作(含 Near SDK v4→v5 遷移導致重寫 2.5 次)
- wasm 檔案 IO 標準化時間:2024 年初才剛完成
結論
結論“wasm 覆蓋率的本質問題在 LLVM→wasm 管道,繞過它的辦法是保留 IR、跨編譯至 x86、並用 regex 抹除 wasm 記憶體操作——wasm-cov 把這條艱難路徑包裝成四行指令,讓智能合約開發者終於能看到整合測試的真實覆蓋率。”
完整解析
詳細WebAssembly 智能合約生態存在一個幾乎被忽視的測試盲區:開發者可以在本地用 Rust 工具鏈跑出 100% 測試覆蓋率,但這份數字是假的。以 Near Protocol 為例,合約使用 wasm-bindgen 衍生的巨集來處理「payable」、「主鑰匙驗證」等鏈上邏輯,而這些巨集只在目標為 wasm32 時才會展開。本地單元測試跑在 x86,整合測試跑在自定義 wasm VM 上,兩者都量不到真實的執行路徑。Sweatcoin 審計便是活生生的例子——審計員手動才找到認證函數完全失效的漏洞,而該專案已被硬編碼進 Near 節點,影響層級極高。
作者決定從根本解決問題,而不是多派兩個 QA 人工補測。調查後發現,問題卡在 LLVM 工具鏈:rust→LLVM IR 這段正常,IR 中有完整的 covermap 資料,但 IR→wasm 這步會把 covermap 丟掉,llvm-cov 因此找不到任何覆蓋率資料。wasm 生態碎片化(wasmer、wasi、wasmtime 等十幾種 runtime 並行)是根本原因,沒有人有餘力修這條編譯管道。他向 LLVM 提交了 bug report,但短期內無解。
突破口是「跨編譯繞過」:IR 是好的,wasm 是壞的,那就別走 wasm,改把 IR 編譯成 x86。第一個最小程式驗證成功,但稍微複雜的程式立刻爆掉——wasm 的記憶體模型用 `memory.grow`/`memory.shrink`,x86 用指標,兩者根本不相容。解法是用 regex 把所有 wasm 特有的記憶體操作全部替換成 `unreachable`,讓跨編譯器看不到這些程式碼,覆蓋率報告就能正常生成。此外還有一個隱藏地雷:Link Time Optimization 會破壞整個流程,必須關閉。
最終 wasm-cov 把這二十幾個手動步驟封裝成四個 CLI 指令:`setup`、`build contracts`、`run integration tests`、`finalize`,同時提供標準化的 VM patch 介面讓各鏈移植。工具支援輸出 function / line / region 三層覆蓋率,報告格式與標準 llvm-cov 一致。目前 radix 已完整整合,wasm-bindgen 與 wasm-pack 也已採用,Cloudflare Workers 則參考此方法在自家 wasm 環境中實作覆蓋率支援。
關鍵時刻
Pipeline v2帶時間戳的重點,會在逐字稿層級分析上線後產生。目前請先透過原始影片觀看。
事實查核
Pipeline v2說法查證是下一次管線升級的一部分。KeyFrame 只會顯示它真正能驗證的內容。

