你手上有一份龐大到無法用 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,讓每個指標都攜帶邊界資訊。

實務上的遷移看起來像這樣:

  1. 用 hybrid 模式編譯並修復編譯錯誤。 這通常意味著修復指標轉整數的轉型,以及 sizeof(void*) 的假設。先不要改動你的資料結構,只要拿到一個乾淨的編譯結果。

  2. 找出高價值目標。 網路解析器、檔案格式解碼器,以及反序列化常式,都是最適合 capability enforcement 的候選對象。它們處理不受信任的輸入,而且正是大多數記憶體安全漏洞棲身之處。

  3. 在這些目標周圍用縮窄的 capability 包裝。 在信任邊界使用 cheri_bounds_set 建立受限的 capability,然後傳入你現有的程式碼。

  4. 將葉節點函式遷移到 purecap。 從那些配置並回傳指標的輔助函式開始。將它們的簽章改為使用 __capability,並用 -mabi=purecap 編譯它們。然後沿著呼叫鏈一路往上推進。

  5. 最終將整個 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 開始。硬體會完成剩下的工作。