工程趣聞 2026 年 10 月 10 日

2026-10-10 — 無分支的恆定時間程式,在 RISC-V 上被編回分支

primary=https://00f.net/2026/10/09/llvm-compiles-branch-free-code-into-branches-on-risc-v/

寫成無分支的恆定時間程式,在 RISC-V 上被 LLVM 編回分支

Frank Denis 的部落格 · 2026-10-09

密碼學程式用位元遮罩寫成「沒有 if」的 select,Clang 在 RISC-V 上卻照樣吐出條件分支,等於把原作者刻意避開的時序側通道又編了回來。Frank Denis 在 2026-10-09 的文章中,用 clang --target=riscv32 -march=rv32imac -mabi=ilp32 -O2 重現了這件事,並比對了 Clang 23、Zig、GCC 16.1 與多個目標架構。

原本的問題

恆定時間(constant-time)程式的前提是:執行路徑與耗時不能依賴祕密值。常見寫法是不用分支,改用遮罩運算選值。但 C 語言沒有任何「不准產生分支」的語意,編譯器只保證結果正確,並不保證指令形狀,所以安全性其實押在後端 codegen 的現狀上。

x86 與 AArch64 有 cmov 這類條件搬移指令,編譯器把「看起來像分支」的運算降成它們,問題不容易浮現。RISC-V 基礎指令集沒有條件搬移,後端就比較容易退回分支。

兩個重現案例

第一個是 128-bit 加法。用 typedef unsigned _BitInt(128) u128; 寫 return a + b;,在 RV32 上 LLVM 會拆成四個 32-bit 加法並傳遞進位。文章指出,第二個字的進位邏輯是「若 sum1 == b1 則沿用前一個進位,否則取 sum1 < b1」,輸出裡因此出現 beq。

第二個是位元遮罩 select。

uint32_t mask = -(bit & 1);
return (a & mask) | (b & ~mask);

Clang 23 在 RV32 輸出 andi、beqz、mv,64-bit RISC-V 同樣出現分支。依文章所述,這個行為隨版本反覆:Clang 15 有分支,16 與 17 沒有,18 到 23 又有。Zig 的同型程式碼(-target riscv32-freestanding -O ReleaseFast)同樣產生 beq 與 beqz,作者並表示 Rust 也有同樣問題。

各目標的實測

下表是文章以 Clang 23、-O2 統計的條件分支數(含條件 return)。

目標addct_select64-bit <
x86_64、x86、AArch64、32-bit ARM、Cortex-M3、MIPS32、LoongArch64、WebAssembly000
Cortex-M0011
32-bit PowerPC012
RV32111
RV64010
RV32/RV64 加 Zicond000

GCC 16.1 在 RISC-V 上,兩個範例都沒有分支,進位用 sltu。但只要把遮罩改成由比較產生,例如 -(uint32_t)(x < y),GCC 在 RV32 與 RV64 都會吐出 bgeu。所以換編譯器並不能根治。

可用的緩解方式

  • 空的 inline asm 屏障:算出遮罩後加 __asm__("" : "+r"(mask));,告訴編譯器這個值可能被改動,文章回報 RV32 與 RV64 上都不再出現分支。
  • Zicond 擴充:它提供 czero.eqz 與 czero.nez,加上 -march=rv32imac_zicond 後,遮罩 select 會變成 czero.eqz、czero.nez 與 or,文章中所有案例都無分支。
  • 手寫進位鏈:祕密資料不要用超過兩個暫存器寬的整數型別,在 RV32 上以 32-bit 字自己寫進位。作者說 Clang 23 目前保持無分支,但未來版本可能改變。

加法的屏障實驗只有部分成功:對輸入做 memory barrier 沒有消除分支,對每個 32-bit 字做暫存器屏障則讓每個進位都變成 sltu。作者明說不會把這兩種實驗當成正式修法。

影響範圍

受影響的是在 RV32/RV64 上自行編譯密碼學程式的人:嵌入式韌體、Zig 或 Rust 寫的 crypto crate、用 _BitInt 或 128-bit 整數處理祕密值的程式碼。要檢查的是實際輸出的組語,找 beq、beqz、bgeu 一類指令是否出現在處理祕密的函式裡,而且編譯器版本或旗標一變就要重查。

Zicond 並非通解。文章指出它屬於 RVA23 profile,但許多核心(尤其微控制器)沒有;而且 Zicond 規格要搭配 Zkt 擴充,才保證資料無關的執行時間。文章未引用任何 LLVM issue、PR 或 commit,因此上游是否已有修補計畫,原文並未說明。

原始來源:Frank Denis,Branches in branch-free code


End of article
0
Would love your thoughts, please comment.x
()
x