你手上有一份龐大到無法用 Rust rewrite、卻又關鍵到不能放任緩衝區溢位風險的 C codebase。CHERI capability 硬體承諾在 CPU 層級攔截記憶體安全違規,但網路上不斷有人告訴你 CHERI 指標是 128 位元,而你的程式碼假設它是 64 位元。
好消息是,CHERI 並不強迫你做出全有或全無的遷移。Hybrid ABI 讓你用最少的改動編譯現有 C 程式碼,在真實的 CHERI 硬體上執行,並在最重要的環節逐步加強安全性。你不需要先移植一百萬行程式碼,才獲得第一個受保護的指標。
為什麼現在可以逐步移植
早期的 capability 架構毫不妥協。iAPX 432 要求每個指標都必須是 capability,沒有例外。這導致所有現有的編譯器和作業系統都無法運作,計畫最終失敗。
CHERI 從那次失敗中學到了教訓。它支援三種編譯模式:baseline(僅使用傳統整數指標)、hybrid(整數指標與 capability 指標共存),以及 purecap(每個指標都是 capability)。Hybrid ABI 就是其中的橋樑。它讓你幾乎不需改動現有程式碼,同時選擇性地在最具價值的地方引入 capability。
在 hybrid 模式下,void* 仍然是 64 位元。Capability 指標是一個獨立的型別 void* __capability,需要你明確選用。你的結構體、連結串列和雜湊表都不會改變大小,除非你主動要求。這不是一個相容層,而是硬體與編譯器原生支援的一級 ABI。
編譯成 CHERI hybrid 時,實際上會出什麼問題
第一步是嘗試用 CHERI 工具鏈編譯你的程式碼,看看哪裡會爆炸。大多數行為良好的 C 程式碼可以不經修改就通過編譯。問題通常集中在一小撮 CHERI 會視為非法的模式上——而這正是重點所在。
指標轉整數。 將指標轉型為 uintptr_t、遮罩某些位元、再轉回指標的程式碼會失敗。在 CHERI 上,uintptr_t 是一個 capability 型別,而不是純整數。對 capability 進行位元運算會剝除 tag bit,而產生的值在解參照時會觸發 trap。
假設指標大小。 硬編碼 sizeof(void*) == 8 或將指標序列化為 8 位元組數值的程式碼,在 purecap 模式下會出問題。在 hybrid 模式下這大致可行,但只要你把 capability 指標和整數指標混在同一個結構體裡,就會變成麻煩。
跨不相關物件的指標運算。 C 程式設計師有時會計算兩個任意指標之間的距離,或比較來自不同配置的指標。CHERI capability 帶有邊界後設資料,對來自不同配置的 capability 做減法運算是未定義行為。編譯器會拒絕,或是硬體會觸發 trap。
內嵌組語。 任何以手寫組合語言在整數暫存器中搬運指標的程式碼都需要更新。CHERI 有專用的 capability 暫存器,編譯器需要知道你碰了哪些暫存器。
CHERI LLVM 工具鏈會提供非常清楚的診斷訊息。你不會看到神祕的連結器錯誤,而是會看到像是「不允許將 capability 轉型為整數」或「對來自不同配置的 capability 做運算」這樣明確的訊息。修好第一個錯誤、重新編譯、然後繼續追下一個。
具體範例:用 capability 包裝一個解析器
假設你有一個網路封包解析器,它會把不受信任的輸入讀進緩衝區,然後解析表頭。這正是那種能從硬體邊界檢查中獲益的程式碼。以下示範如何在不 rewrite 解析器本身的前提下包裝它。
首先,用 hybrid 模式編譯解析器。CHERI 工具鏈就是標準的 LLVM,只是目標平台不同:
# 用 hybrid ABI 編譯單一檔案
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-mno-relax \
-c parser.c -o parser.o
-mabi=lp64d 這個旗標讓整數指標維持 64 位元。你現有的 void* 和 char* 型別保持不變。解析器可以直接編譯通過。
現在在另一個檔案中加入支援 capability 的包裝層:
#include <cheriintrin.h>
#include <stddef.h>
#include <stdint.h>
// 來自 parser.c 的既有解析器
extern int parse_packet(const char *data, size_t len);
// 支援 capability 的進入點
int parse_packet_safe(const char * __capability data, size_t len) {
// 將 capability 縮窄到恰好等於緩衝區長度。
// 即使呼叫者傳入了一個更大的配置區塊,
// 解析器也無法讀取超過 'len' 位元組的資料。
const char * __capability narrowed =
cheri_bounds_set(data, len);
// 在 hybrid 模式下,我們把純指標傳給舊版解析器。
// 編譯器會在這裡自動插入 capability 轉整數的轉換。
// 如果該 capability 無法塞進 64 位元(對於 large-capability
// 位址來說不可能塞得下),這裡會產生警告或錯誤。
//
// 若要遷移到 purecap,你應該把 parse_packet
// 修改成接受 capability 指標。
return parse_packet((const char *)narrowed, len);
}
在這個範例中,parse_packet 仍然使用傳統的整數指標。包裝層會將呼叫者的 capability 縮窄到輸入資料的確切長度,然後再轉換回去。這個縮窄步驟意味著,即使呼叫者不小心傳入了一個 4KB 的緩衝區,但他其實只想傳 64 位元組,硬體也會在縮窄後的 capability 上強制執行 64 位元組的邊界限制。
這不是最終狀態,而是一塊踏腳石。你今天就能在 API 邊界獲得 bounds enforcement,並在後續 refactor 中逐步將 parse_packet 遷移到 purecap。
從 hybrid 遷移到 purecap 的路徑
Hybrid 模式是起點,不是終點。它提供相容性,但無法帶來 capability 的完整安全效益。長期目標是 purecap,讓每個指標都攜帶邊界資訊。
實務上的遷移看起來像這樣:
-
用 hybrid 模式編譯並修復編譯錯誤。 這通常意味著修復指標轉整數的轉型,以及
sizeof(void*)的假設。先不要改動你的資料結構,只要拿到一個乾淨的編譯結果。 -
找出高價值目標。 網路解析器、檔案格式解碼器,以及反序列化常式,都是最適合 capability enforcement 的候選對象。它們處理不受信任的輸入,而且正是大多數記憶體安全漏洞棲身之處。
-
在這些目標周圍用縮窄的 capability 包裝。 在信任邊界使用
cheri_bounds_set建立受限的 capability,然後傳入你現有的程式碼。 -
將葉節點函式遷移到 purecap。 從那些配置並回傳指標的輔助函式開始。將它們的簽章改為使用
__capability,並用-mabi=purecap編譯它們。然後沿著呼叫鏈一路往上推進。 -
最終將整個 module切換到 purecap。 當一個編譯單元裡已經沒有整數指標時,就用
-mabi=purecap編譯它,並與其他 hybrid 程式碼連結。CHERI 工具鏈支援混合 ABI 連結。
對於大型codebase來說,這不是一個週末就能完成的專案。但它也不是 rewrite。你可以在幾天內保護好最脆弱的程式碼路徑,並在後續維護時逐步遷移其餘部分。
實際的權衡是什麼
Hybrid ABI 有真實的成本,在投入之前你應該先了解。
首先,混合 ABI 會讓你的建置流程變複雜。現在你的二進位檔裡會混雜以不同指標大小編譯出來的目標檔。連結器必須同時處理 capability 和整數的重定位。CHERI 工具鏈支援這件事,但你的建置系統可能還不知道。你需要教會它。
其次,在 hybrid 邊界上的 capability 轉整數轉換是有損的。如果你先縮窄了一個 capability,再將它轉型為純指標,就會失去邊界資訊。底層記憶體仍然受到你原始 capability 的保護,但舊版函式接收到的指標不會有 hardware enforcement。這就是為什麼 purecap 才是目標:呼叫鏈中的每個指標都攜帶自己的邊界資訊。
第三,除錯方式會改變。CHERI 上的 GDB 懂得 capability 暫存器,可以印出邊界後設資料。LLDB 的支援也在持續改善中。如果你目前的除錯流程仰賴檢視原始指標數值,那你需要學習 CHERI 暫存器的名稱。
Hybrid 模式的效能開銷,對於主要使用整數指標的程式碼來說通常可以忽略不計。Purecap 的開銷對大多數工作負載而言通常是個位數百分比,對於指標密集的資料結構則會上升到 10–15%。這與 ASAN 這類軟體緩解措施的效能損失相當,而且不像 ASAN,CHERI 在生產環境中是以全速執行的。
你今天就可用 QEMU 開始實驗
你不需要 Morello 開發板才能嘗試。CHERI 專案維護了 QEMU 支援,也有預先安裝好 LLVM 工具鏈的 Docker 映像檔。
# 拉取 CHERI 工具鏈 Docker 映像檔
$ docker run --rm -it ctsrd/cheri-sdk:latest
# 在容器內,為 RISC-V CHERI 編譯你的程式碼
$ clang --target=riscv64-unknown-freebsd \
-march=rv64imafdcxcheri \
-mabi=lp64d \
-o myapp myapp.c
先從用 hybrid 模式編譯你的專案,然後數一數有多少錯誤開始。這個數字會告訴你前方有多少工作。現代、符合標準的 C 程式碼第一次就乾淨編譯過去並不常見,但也不是不可能。對於有大量指標轉型的老舊codebase,出現幾百個錯誤是常態。
按照這個順序修復錯誤:先處理整數轉指標的轉型,然後是跨配置的指標運算,最後是內嵌組語。每一類都有機械式的修復方法。CHERI 專案發布了一份移植指南,裡面有最常見模式的前後對照範例。
Capability 硬體不是理論上的未來。它是一套可運作的工具鏈、一個受支援的 ABI,以及一條不需要燒掉你整個codebase的遷移路徑。從一個檔案、一個函式、一個縮窄的 capability 開始。硬體會完成剩下的工作。