CVEの70%はメモリ安全性の欠陥である。バッファオーバーフロー、use-after-free、二重解放。不正なJPEGからroot権限への横展開を可能にする、そういった種類の脆弱性だ。
1975年から、ハードウェアでこれらの大半を阻止する方法は知られていた。ケンブリッジのCAPコンピュータはケイパビリティベースのアドレス指定を使用した。カーネギー・メロンのHydraも同様だった。アイデアは単純だった。すべてのメモリアクセスは独自の許可証を携行する。ハードウェアがそれを強制する。許可証がなければ、アクセスはできない。
そしてケイパビリティ・ハードウェアは、その後の40年間、フラット・メモリ・モデルに敗北し続けた。ARMのMorelloボードとCHERI命令セットの登場により、ケイパビリティ・アーキテクチャがようやく信じられる復活を遂げようとしている。
問題は、ケイパビリティが機能するかどうかではない。機能する。問題は、なぜ業界が40年もかけてようやくそれに注目するようになったのかということだ。
ケイパビリティ・ポインタは単なるアドレスではなく、改ざん不可能なチケットである
通常のポインタは整数だ。アドレスである。長さもなく、権限もなく、出所もない。アドレスを推測できれば、アクセスできる。それは機能ではない。CがPDP-11から受け継いだ設計上の欠陥だ。
対照的に、ケイパビリティ・ポインタはハードウェアによって強制されるタプルである。仮想アドレス、許可領域のベースと長さ、読み取り・書き込み・実行権限、そして1ビットの有効性タグを含む。タグは通常のメモリの外側、専用のハードウェアメタデータに格納されるため、ケイパビリティは改ざん不可能となる。整数から有効なケイパビリティを構築することはできない。すでに権限を持つものからのみ受け取ることができる。
CHERIでは、64ビットマシン上でこれは128ビットのポインタのように見える。追加の64ビットが境界と権限を運ぶ。タグ・ビットは実装によって、シャドウテーブルまたは予備のECCビットに格納される。
以下は、CHERIシステム上でケイパビリティを縮小する実際の例である:
#include <cheriintrin.h>
#include <stdio.h>
int main(void) {
char buffer[64];
// In the purecap ABI, &buffer[0] is already a capability
// carrying 64-byte bounds and read/write permissions.
char *cap = buffer;
printf("Original length: %zu\n", cheri_length_get(cap));
// Narrow authority to the first 16 bytes.
char *narrow = cheri_bounds_set(cap, 16);
printf("Narrowed length: %zu\n", cheri_length_get(narrow));
// This access is safe and hardware-authorized.
narrow[15] = 'x';
// This would trap with a CHERI bounds violation:
// narrow[20] = 'y';
return 0;
}
攻撃者がケイパビリティ・ポインタを破損させた場合、タグ・ビットはクリアされる。ハードウェアは次の逆参照時に例外を発生させる。攻撃チェーンは最初のホップで死ぬ。
これは、ASLRやスタックキャナリのような速度低下策よりも劇的に優れたセキュリティ・モデルである。
iAPX 432は一世代の井戸を毒した
では、なぜ私たちは40年間、ポインタは単なる整数だと見なしてきたのだろうか。
インテルは1981年にiAPX 432を出荷した。ハードウェア・ケイパビリティ、オブジェクト指向メモリ・セグメント、さらにはハードウェア支援ガベージ・コレクションを備えていた。一方で、8086よりもおおよそ5倍から10倍遅く、非常に複雑であり、既存のすべてのコンパイラと非互換だった。
IBMはSystem/38でより幸運だった。これはシングルレベル・ストアにケイパビリティを使用した。機能した。安定していた。しかし、新興のオープン・エコシステムへの道のない、独自の中規模システムでもあった。UNIX革命が到来したとき、それはフラット・アドレス空間、Cポインタ、そしてポインタはレジスタに収まるという仮定をもたらした。その仮定が業界全体のABIとなった。
1990年代までには、ケイパビリティ・ハードウェアは研究上の珍品となっていた。商業上の急務は後方互換性と生の性能であった。メモリ安全性は、もし問題視されるなら言語レベルの懸念事項だった。CとC++は、単にそれをプログラマの問題であると宣言した。
インターネットがすべてのバッファオーバーフローをリモートから悪用可能にするまでは、それは十分に機能していた。
CHERIのハイブリッド・モードが互換性の行き詰まりを破る
現代のケイパビリティ・ハードウェア、特にCHERIは、iAPX 432の教訓を学んだ。全面移行か全面拒否かを強制しない。
CHERIは、従来の整数ポインタとケイパビリティ・ポインタが同じプロセス内で共存するハイブリッドABIをサポートする。ネットワーク・パーサやサンドボックス化されたライブラリだけをケイパビリティ付きでコンパイルし、アプリケーションの残りは通常のポインタを使用できる。オペレーティング・システム(CheriBSD、またはCHERI Linux移植版)が移行を管理する。
これが重要なのは、真の障壁はシリコン面積では決してなかったからだ。ソフトウェアの慣性だった。数十億行のCおよびC++コードが、sizeof(void *) == 8を仮定している。それらすべてを書き換えることを要求するケイパビリティ・アーキテクチャは、出荷と同時に死亡する。漸進的に採用できるものにこそ、チャンスがある。
ハードウェア機構は優雅である。ケイパビリティ対応のロードとストアは、タグと境界をアドレス変換と並行してチェックする専用命令を使用する。整数ポインタはチェックを完全にバイパスする。安全性の対価は、使用する場所でのみ支払われる。
真のコストはTLB圧力とポインタ膨張であり、サイクル数ではない
ケイパビリティはタダではない。オーバーヘッドは3つの区分に分かれる。
第一に、ポインタ・サイズ。purecap ABIでは、すべてのポインタは128ビットである。ポインタ重視のデータ構造に対するキャッシュ圧力が倍増する。連結リスト、木、仮想関数テーブルすべてが太る。
第二に、境界粒度。CHERIはバイト粒度で境界を強制するため、メモリ・サブシステムはすべての逆参照時に境界をチェックしなければならない。チェック自体は高速だが、細かいケイパビリティを多数作成すると、TLBやキャッシュの圧力が増大する可能性がある。
第三に、そして最も重要なのは、ソフトウェア変更である。コンパイラはケイパビリティ対応のプロローグを生成しなければならない。ABIが変わる。メモリ・アロケータは、生のポインタではなく境界付きケイパビリティを返さなければならない。デバッガは128ビット・レジスタを理解する必要がある。
CHERIハードウェア上で測定されたランタイム・オーバーヘッドは、ほとんどのワークロードで通常一桁パーセントの範囲であり、ポインタ重視のベンチマークでは10〜15%に達する。これは、メモリ・タギングやサンドボックス化のような多くの最新緩和策よりも少なく、より強力な保証を提供する。
トレードオフは性能ではない。トレードオフはエコシステムの激変である。
開発ボードですぐに実行できる
ケイパビリティ・ハードウェアを試したいのであれば、タイムマシンや研究助成金は必要ない。
ARMのMorelloボードは、Neoverse N1コア上でCHERI拡張を実装している。CheriBSDはそのまま動作する。ハードウェアがない場合は、QEMU-CHERIが完全なケイパビリティ・アーキテクチャをエミュレートする。
ツールチェーンは標準のLLVMである。cheri-clangと-march=morello+cheriフラグでコンパイルする。FreeBSDおよびLinuxの移植版が存在する。LLVM統合アセンブラ、リンカ、デバッガはすべてケイパビリティを理解する。
一つの関数から始めよう。パーサやデシリアライゼーション・ルーチンを縮小されたケイパビリティでラップする。ファザーでテストを実行する。境界外書き込みがヒープを破損させていたはずの場所で、代わりにクリーンなハードウェア例外が得られるだろう。
ケイパビリティ・ハードウェアは、メモリ安全性への理論的な修正ではない。インセンティブが間違っていたために業界が40年間無視してきた、動作し出荷されているテクノロジーだ。フラット・メモリは構築が速く、移植が容易で、リモート悪用がデフォルトの脅威モデルになるまでは十分に良かった。
シリコンは動作する。コンパイラは動作する。オペレーティング・システムは動作する。残されているのは、コードベースのどの部分を最初に保護するかを決めることだけだ。