投機実行とサイドチャネル(Spectre/Meltdown)のOS対策
性能のために投機実行したCPUが、捨てたはずの結果をキャッシュに痕跡として残す。MeltdownとSpectreの原理から、KPTI・retpoline・IBRSなどOSの緩和策とそのコストまで筋道立てて理解できます。
- 1.投機実行は分岐確定前に命令を先走り実行する。結果はロールバックされるが、投機中にアクセスしたデータがキャッシュに残り、キャッシュタイミング測定で値を漏らす。
- 2.Meltdownは権限チェックより先に投機ロードが進む実装欠陥で、KPTI(カーネルページテーブルをユーザー空間から分離)で塞ぐ。Spectreは分岐予測器を汚染して被害者コードに投機ロードさせる攻撃。
- 3.Spectre対策はretpoline(間接分岐を予測不能なret迂回に置換)やIBRS/IBPB(予測器を隔離するMSR)で、いずれもCPUサイクルや権限切替コストを上乗せする。
投機実行はなぜ「捨てた結果」を漏らすのか
現代のCPUは、分岐の行き先や条件が確定する前から後続命令を投機実行します。予測が当たれば前倒しで仕事が進み、外れれば投機結果をロールバックして何事もなかったかのように戻します。アーキテクチャ状態(レジスタやメモリの可視値)から見れば、捨てられた投機の痕跡は残らない――というのが設計上の建前です。
問題は、ロールバックがアーキテクチャ状態しか巻き戻さない点にあります。投機実行中に発生したマイクロアーキテクチャ状態の変化、とりわけキャッシュへのデータ充填は元に戻りません。あるアドレスに投機的に触れると、そのキャッシュラインが残り、後でそのアドレスへのアクセスが速いか遅いかを測れば「投機中に触れたか」が分かります。これが投機実行サイドチャネルの核心で、捨てたはずの値をタイミングという裏口から読み出します。
測定の骨子(Flush+Reload)
1. probe[256][4096] を用意し、全ラインをキャッシュから追い出す(flush)
2. 何らかの方法で「秘密の値 v」を投機中に取得させ、probe[v*4096] を読ませる
3. 投機はロールバックされるが probe[v*4096] のラインはキャッシュに残る
4. probe[i*4096] を i=0..255 で順に読み、最速だった i が秘密の値 v
ページサイズ刻み(4096)で配列を引くのは、プリフェッチや隣接ラインの巻き込みを避け、1値が1ラインに対応するようにするためです。
Meltdown:権限チェックを投機が追い越す
Meltdown(Rogue Data Cache Load)は、一部CPUの実装欠陥を突きます。ユーザーモードからカーネルアドレスをロードすると本来は保護違反ですが、該当CPUでは権限チェックの結果が確定する前に、投機的なロードがキャッシュからデータを読み、そのバイトを使った後続のメモリアクセス(前述のprobe参照)まで投機実行してしまいます。
// ユーザーから実行(kernel_addr は本来アクセス不可)
char v = *(char*)kernel_addr; // 投機中に値が取れてしまう
temp = probe[v * 4096]; // v に依存したラインがキャッシュに残る
// この後で例外が確定し命令列はロールバックされるが、痕跡は残る
例外(保護違反)は最終的に発生しますが、それは命令がリタイアする段階での話で、投機段階のキャッシュ汚染には間に合いません。攻撃者は例外をハンドリングまたは抑止しつつ、Flush+Reloadでカーネルメモリを1バイトずつ吸い出せます。カーネル/ユーザーモードの境界がアーキテクチャ上は守られていても、マイクロアーキテクチャ上は素通りされていたわけです。
KPTI:ページテーブルそのものを分離する
ソフトウェア対策の決定版がKPTI(Kernel Page Table Isolation)です。原理は単純で、漏らせない情報をそもそもユーザーモードのページテーブルに載せない。従来はカーネルとユーザーが1つのアドレス空間を共有し、カーネル領域を「特権ページ」属性で隠していましたが、Meltdownはその属性チェックを投機で迂回しました。KPTIはユーザー実行時に使うページテーブルからカーネルのマッピングを物理的に取り除くため、投機ロードしようにも変換できる物理アドレスが存在しないのです。
KPTIではユーザー用とカーネル用の2組のページテーブルを持ち、システムコールや割り込みでカーネルへ入るたびにCR3(ページテーブルベース)を切り替えます。CR3の書き換えは原則TLBフラッシュを伴うため、TLB再充填コストが境界越えごとに乗ります。PCID/ASID対応CPUではフラッシュを抑えられ、影響はワークロード次第で数%程度に収まりますが、システムコール多発系では無視できません。
Spectre:分岐予測器を「教育」して撃たせる
SpectreはMeltdownと別系統で、CPUの実装欠陥というより投機実行という機構そのものを悪用します。攻撃者は自分のメモリを読むのではなく、被害者コードを誤った投機経路へ誘導し、被害者の権限で秘密に触れさせます。
- Variant 1(境界チェックの迂回 / BCB):
if (i < arr_len) { y = arr2[arr1[i] * 4096]; }のような境界チェック付きコードで、予測器に「条件は真」と学習させた後、わざと範囲外のiを渡す。CPUはチェック確定前に投機実行し、範囲外のarr1[i]を使ったロードでキャッシュを汚す。 - Variant 2(分岐ターゲット注入 / BTI): 間接分岐(関数ポインタ呼び出し等)の予測先を格納する**BTB(分岐ターゲットバッファ)**を攻撃者が訓練し、被害者の間接分岐を攻撃者の選んだ「ガジェット」へ投機的に飛ばす。
被害者自身の権限で動くため、KPTIのようなアドレス空間分離では防げません。対策は予測の流れ自体に介入します。
retpoline:間接分岐を予測不能にする
retpoline(return + trampoline)は、Variant 2を狙ったコンパイラ/カーネルの対策です。jmp *%raxのような間接分岐を、ret命令を使った定型コードへ置換し、BTBによる投機先注入を無効化します。
; 元の間接呼び出し call *%rax を以下に置換
call set_up_target ; 戻りアドレスをスタックへ
capture_speculation:
pause ; 投機が走っても
lfence ; ここで足踏みさせる
jmp capture_speculation
set_up_target:
mov %rax, (%rsp) ; 本当の飛び先で戻りアドレスを上書き
ret ; ret は RSB を使い、誤予測先は無害なループ
要は間接分岐をすべてretに変換し、CPUがretの飛び先を予測する際に使う**RSB(リターンスタックバッファ)**へ安全な値を仕込むことで、攻撃者が訓練したBTBの予測先が使われないようにします。投機が暴走しても無害なpause; lfenceループに閉じ込められます。
IBRS/IBPB/STIBP:ハードウェアで予測器を隔離する
CPUベンダはマイクロコード更新でMSR(モデル固有レジスタ)による制御を追加しました。OSはこれらを適切な契機で書き込みます。
| 機構 | 意味と効果 | OSが使う契機 |
|---|---|---|
| IBRS | 特権上昇後、下位特権の訓練した予測を使わせない | カーネル入口で有効化 |
| IBPB | 予測器の状態に障壁を置き履歴を一掃する | プロセス切替時に発行 |
| STIBP | 兄弟ハイパースレッド間の予測器共有を遮断 | 信頼境界をまたぐ実行時 |
これらは強力ですが高コストです。とくに初期IBRSはカーネル入口ごとに書き込むと重く、後に「予測を境界越しに使わせない」状態を保つEnhanced IBRSが導入され、入口ごとの書き込みが不要になりました。IBPBはプロセス切替のたびに予測器を捨てるため、切替直後は分岐予測が外れやすくなる代償があります。
Spectre/Meltdown以降、ラインフィルバッファやストアバッファといったCPU内部の一時バッファから投機的にデータが漏れるMDS(Microarchitectural Data Sampling)系も判明しました。対策はVERW命令でカーネルからユーザーへ戻る際にバッファをフラッシュする方式が中心で、これもまた境界越えごとのコストとして積み上がります。
コストと運用:何をどこまで効かせるか
緩和は一律ではなく、脅威モデルとハードウェア世代で要否が変わります。新しいCPUはMeltdownを設計段階で修正済み(権限チェックを投機ロードより前に効かせる)なので、KPTIを無効化しても安全な場合があります。逆にハイパースレッドを共有するクラウドのマルチテナント環境では、STIBPやコアスケジューリング(信頼境界の異なるスレッドを同一物理コアに同居させない)まで踏み込む必要があります。
「ロールバックされるのはアーキテクチャ状態だけで、キャッシュ等のマイクロアーキテクチャ状態は戻らない」が全体の鍵。MeltdownはKPTIで防ぐ実装欠陥、Spectreは予測器汚染でretpoline/IBRSが必要、の対応関係を取り違えないこと。KPTIのコストはCR3切替に伴うTLBフラッシュであり、PCID/ASIDが効くと覚える。
まとめ
- 投機実行はアーキテクチャ状態をロールバックするが、キャッシュ充填などのマイクロアーキテクチャ状態は残り、タイミング測定で秘密を漏らす。
- Meltdownは権限チェックを投機ロードが追い越す実装欠陥で、KPTIがカーネルのマッピングをユーザー用ページテーブルから外して塞ぐ(コストはCR3切替=TLBフラッシュ)。
- Spectreは分岐予測器を汚染し被害者の権限で投機ロードさせる攻撃で、retpoline(間接分岐の無害化)やIBRS/IBPB/STIBP(予測器の隔離)で緩和する。
- いずれの対策もCPUサイクル・権限切替・予測精度の形でコストを払う。脅威モデルとCPU世代に応じて取捨選択するのが実務の判断軸。
仕組みの土台としてカーネル/ユーザーモードとMMU/TLBの内部も押さえておくと、各緩和策がどこに効いているかが立体的に見えてきます。
OS Article
投機実行とサイドチャネル(Spectre/Meltdown)のOS対策を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
投機実行
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
Meltdownは権限チェックより先に投機ロードが進む実装欠陥で、KPTI(カーネルページテーブルをユーザー空間から分離)で塞ぐ。Spectreは分岐予測器を汚染して被害者コードに投機ロードさせる攻撃。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「投機実行 / Spectre」に近いか確認する。
- 強みである「投機実行は分岐確定前に命令を先走り実行する。結果はロールバックされるが、投機中にアクセスしたデータがキャッシュに残り、キャッシュタイミング測定で値を漏らす。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。