七成 CVE 是記憶體安全性錯誤。buffer overflow、釋放後使用、重複釋放。這類漏洞讓攻擊者得以從一張損毀的 JPEG 取得 root 權限。

我們自 1975 年起就知道如何在硬體層面阻止絕大多數這類問題。劍橋 CAP 電腦採用了 capability 定址。卡內基美隆大學的 Hydra 亦然。概念很單純:每次記憶體存取都自帶一張權限憑證。由硬體強制執行。沒有憑證,就沒有存取權。

接下來的四十年,capability 硬體卻持續敗給扁平記憶體模型。直到現在,藉由 ARM 的 Morello 開發板與 CHERI 指令集,capability 架構才正展開一次可信的回歸。

問題不在於 capability 是否有效。它確實有效。問題在於產業為何花了四十年才開始重視。

Capability 指標是一張無法偽造的票券,而不只是位址

一般的指標是一個整數。一個位址。它沒有長度、沒有權限、也沒有來源資訊。只要你能猜到一個位址,就能存取它。這不是功能。這是 C 語言從 PDP-11 繼承來的設計缺陷。

相較之下,capability 指標是一種由硬體強制執行的元組。它包含虛擬位址、授權區域的基底與長度、讀寫執行權限,以及一個單位元的有效性標籤。這個標籤存放在正常記憶體之外,位於專屬的硬體中繼資料區,因此 capability 無法偽造。你無法從一個整數建構出有效的 capability。你只能從已經擁有權限的來源接收它。

在 CHERI 上,這在 64 位元機器上表現為 128 位元指標。額外的 64 位元承載邊界與權限。標籤位元則存放於影子表格或備用的 ECC 位元中,視實作而定。

以下是在 CHERI 系統上縮窄 capability 的實際範例:

#include <cheriintrin.h>
#include <stdio.h>

int main(void) {
    char buffer[64];

    // 在 purecap ABI 中,&buffer[0] 本身就是一個 capability,
    // 帶有 64 位元組的邊界與讀寫權限。
    char *cap = buffer;

    printf("Original length: %zu\n", cheri_length_get(cap));

    // 將權限縮窄至前 16 位元組。
    char *narrow = cheri_bounds_set(cap, 16);
    printf("Narrowed length: %zu\n", cheri_length_get(narrow));

    // 這次存取安全且經硬體授權。
    narrow[15] = 'x';

    // 以下將觸發 CHERI 邊界違規例外:
    // narrow[20] = 'y';

    return 0;
}

如果攻擊者竄改了一個 capability 指標,標籤位元就會被清除。硬體會在下次解參照時觸發例外。整條利用鏈在第一跳就會中斷。

這比起 ASLR 和堆疊金絲雀是徹底更優的安全性模型,後者不過是減速丘。

iAPX 432 為一整個世代留下了惡名

那麼,為何我們花了四十年假裝指標只是整數?

Intel 在 1981 年推出 iAPX 432。它具備硬體 capability、物件導向的記憶體區段,甚至還有硬體輔助的垃圾回收。但它也大概比 8086 慢上五到十倍,極度複雜,且與所有現有編譯器不相容。

IBM 的 System/38 運氣稍好,它為單層級儲存使用了 capability。它運作正常。它很穩定。但它也是一套專屬的中階系統,沒有進入新興開放生態系的途徑。當 UNIX 革命來臨時,它帶來了扁平位址空間、C 語言指標,以及「指標可以塞進暫存器」的假設。這項假設後來成為了整個產業的 ABI。

到了 1990 年代,capability 硬體已成為研究上的奇珍。商業上的當務之急是向下相容性與原始效能。記憶體安全性頂多是語言層級的議題,如果它還算個議題的話。C 和 C++ 乾脆把它丟給程式設計師自己處理。

在網際網路讓每個buffer overflow都能被遠端利用之前,這樣姑且還行得通。

CHERI 的混合模式打破了相容性僵局

現代的 capability 硬體,特別是 CHERI,吸取了 iAPX 432 的教訓。它並不強迫全面遷移。

CHERI 支援混合 ABI,讓傳統的整數指標與 capability 指標能在同一個程序中共存。你可以只把網路剖析器或sandboxed函式庫用 capability 編譯,而應用程式的其餘部分繼續使用一般指標。作業系統(CheriBSD 或 CHERI Linux 移植版)負責管理這個過渡。

這之所以重要,是因為真正的障礙從來不是晶片面積。而是軟體慣性。有數十億行 C 和 C++ 程式碼假設 sizeof(void *) == 8。一種要求全部重寫的 capability 架構,出場即死亡。一種可以漸進採納的,則還有機會。

硬體機制相當優雅。具備 capability 感知的載入與儲存指令會使用專屬指令,在進行位址轉譯的同時平行檢查標籤與邊界。整數指標則完全繞過檢查。你只在需要安全性的地方付出代價。

真正的成本是 TLB 壓力與指標膨脹,而非週期數

Capability 並非免費。額外負擔可分為三類。

第一,指標大小。在 purecap ABI 中,每個指標都是 128 位元。這讓指標密集的資料結構的快取壓力倍增。連結串列、樹狀結構、虛擬函式表全都變得更肥大。

第二,邊界粒度。CHERI 以位元組粒度強制執行邊界,這表示記憶體子系統必須在每次解參照時檢查邊界。檢查本身很快,但若是建立許多細小的防護區域,細粒度的 capability 可能會增加 TLB 與快取壓力。

第三,也是最重要的,軟體變更。編譯器必須產生具備 capability 感知的前導程式。ABI 會改變。記憶體配置器必須回傳有邊界的 capability,而非原始指標。除錯器需要理解 128 位元暫存器。

在 CHERI 硬體上測得的執行期額外負擔,大多數工作負載通常在個位數百分比範圍,而指標密集的效能測試則可能達到十到十五個百分點。這比許多現代緩解措施(如記憶體標記或sandboxed)的額外負擔還低,卻提供更強的保證。

要取捨的不是效能。要取捨的是生態系動盪。

你今天就可以在開發板上執行這個

如果你想嘗試 capability 硬體,你不需要時光機或研究經費。

ARM 的 Morello 開發板在 Neoverse N1 核心上實作了 CHERI 擴充功能。CheriBSD 可以開箱即用地在上面運作。如果你沒有硬體,QEMU-CHERI 可以模擬完整的 capability 架構。

工具鏈是標準 LLVM。你用 cheri-clang 搭配 -march=morello+cheri 旗標編譯。FreeBSD 與 Linux 移植版均已存在。LLVM 的整合式組譯器、連結器與除錯器都懂 capability。

從單一函式開始。用縮窄後的 capability 包裝一個剖析器或反序列化常式。對它執行模糊測試。當越界寫入原本會汙染堆積時,你會得到一個乾淨的硬體例外,而非無聲的崩潰。

Capability 硬體不是記憶體安全性的理論性修補。它是一項已經運作、已經出貨的技術,產業只因為誘因錯誤而忽略了四十年。扁平記憶體蓋起來更快、移植更容易,而且在遠端利用成為預設威脅模型之前都還算夠用。

晶片運作正常。編譯器運作正常。作業系統運作正常。唯一剩下的問題,是決定你的程式碼庫中哪些部分值得優先保護。