Slide 1

Slide 1 text

図解でわかる SpectreとMeltdown Feb. 17. 2018 Satoru Takeuchi Twitter ID: satoru_takeuchi 1

Slide 2

Slide 2 text

はじめに ● 脆弱性の種類と、この資料の説明範囲 ● キャッシュを使ったサイドチャネルアタックによる不正なデータ読み出し ● ソフトウェアではなくCPUの高速化機能にまつわる脆弱性 ○ それぞれ脆弱性のある CPUのリストが公開されている 2 コードネーム 別名 しくみ 対策 Spectre Variant 1: Bounds Check Bypass ✔ Variant 2: Branch Target Injection Meltdown Variant 3: Rogue data cache load ✔ ✔

Slide 3

Slide 3 text

もくじ ● 前提知識 ● Variant 1のしくみ ● Variant 3のしくみ ● Variant 3の対策 3

Slide 4

Slide 4 text

前提知識 ● サイドチャネルアタック ● Flush+Reload攻撃 ● アウトオブオーダー実行 ● 分岐予測 4

Slide 5

Slide 5 text

サイドチャネルアタック ● 暗号化されたデータを当該データ処理時に発生する物理的な現象の観測によって 読み出す攻撃方法 ○ 処理時間の違い ○ 消費電力の違い ○ 電磁波の違い ● Variant 1-3によって読み出すのは暗号化されたデータではなく、コード上は論理的 にアクセスできないデータ(例: 攻撃対象のメモリ、カーネルのメモリ) 5

Slide 6

Slide 6 text

Flush+Reload攻撃 ● キャッシュメモリを使ったサイドチャネルアタック a. 初期化 b. 抜き取り c. 復元 6

Slide 7

Slide 7 text

Flush+Reload攻撃: 初期化と抜き取り 7 #define PAGE_SIZE 4096 char probe[256 * PAGE_SIZE]; # データ抜き取り用領域 Char init_and_snoop(char *p) { Probe[]のキャッシュをフラッシュ (*1) # 初期化 Dont_use = probe[(*p) * PAGE_SIZE]; # 抜き取り } *1) clflush命令などを使う

Slide 8

Slide 8 text

Flush+Reload攻撃の実行例: 初期化 ● (*p) == 2とすると… 8 #define PAGE_SIZE 4096 char probe[256 * PAGE_SIZE]; Char init_and_snoop(char *p) { probe[]のキャッシュをフラッシュ Dont_use = probe[(*p) * PAGE_SIZE]; }

Slide 9

Slide 9 text

Flush+Reload攻撃: 初期化直後のprobe[] 9 PAGE_SIZE … … 0 * PAGE_SIZE 1* PAGE_SIZE … … 2* PAGE_SIZE 255 * PAGE_SIZE … … On cache Not on cache … 全部 not on cache

Slide 10

Slide 10 text

Flush+Reload攻撃の実行例: 抜き取り ● (*p) = 2とすると… 10 #define PAGE_SIZE 4096 char probe[256 * PAGE_SIZE]; Char init_and_snoop(char *p) { probe[]のキャッシュをフラッシュ Dont_use = probe[(*p) * PAGE_SIZE]; }

Slide 11

Slide 11 text

Flush+Reload攻撃: 抜き取り直後のprobe[] ● 仮定: (*p) == 2 11 PAGE_SIZE … … 0 * PAGE_SIZE 1* PAGE_SIZE … … 2* PAGE_SIZE 255 * PAGE_SIZE … … On cache Not on cache … 1つだけがon cache

Slide 12

Slide 12 text

Flush+Reload攻撃: 復元コード ● “On cache”と”not on cache”のアクセスレイテンシの差を利用して(*a)の値を復元 12 int restore() { For (int i = 0; i < 256; i++) { Begin = 現在時刻測定(*1) Dont_use = probe[i * PAGE_SIZE]; End = 現在時刻測定 If (end - begin < 閾値) # trueならon cache, falseならnot on cache Return i; } Return -1; # 失敗 } *1) rdtsc命令などを使う

Slide 13

Slide 13 text

アクセスレイテンシの概念図 13 アクセスレイテンシ probe[]のインデックス 0 (255 * PAGE_SIZE) (2 * PAGE_SIZE) 閾値

Slide 14

Slide 14 text

probe[]のサイズが256ではない理由 ● あるデータのキャッシュへのロードが隣のデータも影響を与える ● CPUはキャッシュライン(サイズは64バイト, 128バイトなど)単位でメモリを読み出す ○ Probe[0]をキャッシュに入れると probe[1]もキャッシュに乗る ● キャッシュラインにシーケンシャルアクセスすると次のラインをプリフェッチすること がある ○ プリフェッチはページをまたがない ● probe[256 * PAGE_SIZE]のほうがデータを抜ける可能性が高い 14

Slide 15

Slide 15 text

アウトオブオーダー実行 ● CPUの各命令の所要時間には大きな違いがある ○ 例: キャッシュに乗っていないデータのロードは非常に遅い (演算処理の数十倍) ● 連続する複数の処理間に依存性が無いことがある ● 依存関係が無い命令列の並列実行によって高速化可能 ● 命令の実行順序が入れ替わりうるからOut-of-Order(あるいはOoO)実行と呼ぶ 15

Slide 16

Slide 16 text

OoO実行の流れ: 初期状態 16 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ *) 実装詳細が気になる人のための検索ワード : 「リオーダバッファ」「レジスタリネーミング」 命令実行 ユニット RBX: 1

Slide 17

Slide 17 text

OoO実行の流れ: 命令読み出し 17 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1

Slide 18

Slide 18 text

OoO実行の流れ: 作業領域を割り当て 18 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1

Slide 19

Slide 19 text

OoO実行の流れ: 実行開始 19 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1 $(100)のデータほ しい $(200)のデータ ほしい

Slide 20

Slide 20 text

OoO実行の流れ: 後の命令が先に完了 20 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1 3 $(100)のデータま だ? $(200)のデータ 来た

Slide 21

Slide 21 text

OoO実行の流れ: 先の命令が後に完了 21 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 on cache 値は3 On cache CPU 2 RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) RBX: 1 3 $(100)のデータ 来た

Slide 22

Slide 22 text

OoO実行の流れ: 前から順番に反映(リタイア) 22 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 on cache 値は3 On cache CPU 2 RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 1 3

Slide 23

Slide 23 text

OoO実行の流れ: 前から順番に反映(リタイア) 23 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 on cache 値は3 On cache CPU RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 3 3

Slide 24

Slide 24 text

OoO実行の流れ: 完了 24 Mov RAX, ($100) Mov RBX, ($200) コード 値は2 on cache 値は3 On cache CPU RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 3

Slide 25

Slide 25 text

分岐予測 ● 分岐命令発生時、分岐先を予測して投機的に実行 ○ 予測成功: 投機実行の結果をそのまま利用。高速 ○ 予測失敗: 作業領域の結果を捨てて正しい分岐先からやり直し。低速 ● 予測方法の例 ○ 過去数回の分岐結果をもとに、最頻ルートを予想 25

Slide 26

Slide 26 text

分岐予測の流れ: 初期状態 26 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; … (b) Return ret; } コード ● 内部的には以下2つの流れを並列実行 a. Lenをロード -> (x < len)の評価 -> 分岐先決定 b. A[x]をロード -> retにa[x]をストア # 投機的実行 CPUはx < lenをtrueと予測

Slide 27

Slide 27 text

● foo(x)を呼ぶと… 分岐予測の流れ: 分岐予測成功の場合 27 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; .. (b) Return ret; } コード 1. 流れbが先行 a. len(not on cache)をロード中… b. A[x](on cache)をロード

Slide 28

Slide 28 text

● foo(x)を呼ぶと… 分岐予測の流れ: 分岐予測成功の場合 28 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; .. (b) Return ret; } コード 1. 流れbが先行 a. len(not on cache)をロード中… b. A[x](on cache)をロード完了 2. 流れaが完了。分岐予測成功 a. Lenをロード完了 -> “x(=10) < len(=100)”がtrue b. retにa[x]をストア 3. a, bの順にリタイア

Slide 29

Slide 29 text

● foo(x)を呼ぶと… 分岐予測の流れ: 分岐予測成功の場合 29 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; .. (b) Return ret; } コード 1. 流れbが先行 a. len(not on cache)をロード中… b. A[x](on cache)をロード完了 2. 流れaが完了。分岐予測成功 a. Lenをロード完了 -> “x(=10) < len(=100)”がtrue b. retにa[x]をストア完了 3. a, bの順にリタイアしてから先に進む

Slide 30

Slide 30 text

分岐予測が効く典型的なコード 30 For (i = 0; i < 1000; i++) Sum += i; ● 1000回の評価中999回はtrue ● i < 1000 == trueという予測はほぼ成功

Slide 31

Slide 31 text

● X = 1000とすると… 分岐予測の流れ: 分岐予測失敗の場合 31 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) ... (a) ret = a[x]; … (b) Return ret; } コード 1. 流れbが先行 a. lenをロード中… b. A[x]をロード

Slide 32

Slide 32 text

● X = 1000とすると… 分岐予測の流れ: 分岐予測失敗の場合 32 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) ... (a) ret = a[x]; … (b) Return ret; } コード 1. 流れbが先行 a. lenをロード中… b. A[x]をロード完了 2. 流れaが完了。分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”がfalse b. retにa[x]をストア中…

Slide 33

Slide 33 text

● X = 1000とすると… 分岐予測の流れ: 分岐予測失敗の場合 33 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) ... (a) ret = a[x]; … (b) Return ret; } コード 1. 流れbが先行 a. lenをロード中… b. A[x]をロード完了 2. 流れaが完了。分岐予測失敗を検出 a. Lenをロード完了 -> “x(=1000) < len(=100)”がfalse b. retにa[x]をストア中… 3. 流れbの実行を捨てて先に進む

Slide 34

Slide 34 text

Variant 1のしくみ ● 脆弱性 ● 攻撃方法 34

Slide 35

Slide 35 text

問題のあるコード 35 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } ● 以下2つの流れが並列実行 a. Lenをロード -> (x < len)の評価 -> 分 岐 b. A[x]をロード -> probe[a[x] * PAGE_SIZE]をロード -> retに probe[...]をストア CPUはx < len == trueと予想

Slide 36

Slide 36 text

● foo(1000)を呼ぶと… 分岐予測が外れた場合 36 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE]をロード

Slide 37

Slide 37 text

● foo(1000)を呼ぶと… 分岐予測が外れた場合 37 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE] をロード 2. 流れaにおいて分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”は false b. ret にprobe[...]をストア中…

Slide 38

Slide 38 text

● foo(1000)を呼ぶと… 分岐予測が外れた場合 38 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE] をロード 2. 流れaにおいて分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”は false b. ret にprobe[...]をストア中… 3. 流れaをリタイア。流れbの実行結果は捨てる

Slide 39

Slide 39 text

● foo(1000)を呼ぶと… 分岐予測が外れた場合 39 Char a[100]; Int len = 100; Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE] をロード 2. 流れaにおいて分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”は false b. ret にprobe[...]をストア中… 3. 流れaをリタイア。流れbの実行結果は捨てる 論理的に読んでないはずの A[1000]をprobe[]から復元可能

Slide 40

Slide 40 text

Variant 1を利用したFlush+Reload攻撃 ● できること a. Fooの引数変更によって、攻撃対象プロセスがアクセス可能な任意のメモリを読み出せる b. カーネル内で実行すればカーネル内のメモリも読み出せる ● 処理の流れ a. 初期化: Probe[]をキャッシュフラッシュ b. 教育: foo()を何度も呼び出して if文におけるCPUの分岐予測先をtrueにする c. 抜き取り: foo()を実行してif文の分岐予測を失敗させる -> 取りたいデータをprobe[]に抜く d. 復元 40

Slide 41

Slide 41 text

現実的な攻撃方法 ● 攻撃対象バイナリ上に元々存在する or 動的生成したコードをgadgetとして利用し て、ROPなどの他の方法と合わせて攻撃 ● 現実世界で使うのは大変 41

Slide 42

Slide 42 text

Variant 3のしくみ ● 脆弱性 ● 攻撃方法 42

Slide 43

Slide 43 text

linuxの仮想アドレス空間 ● 各プロセスの仮想アドレス空間に全物理メモリをマップしている(*1) ○ 目的は実装の単純化と高速化 ○ マップした領域をカーネル領域と呼ぶ 43 仮想アドレス空間 物理アドレス空間 物理メモリ プロセスAのメモリ カーネル領域 *1) 実際のメモリマップはもっと複雑です

Slide 44

Slide 44 text

アクセス権限 ● 権限チェック: プロセスからカーネル領域にアクセスすると例外発生 ● カーネル動作中(システムコール実行中、割込み処理中)のみアクセス可能 44 仮想アドレス空間 物理アドレス空間 プロセスAのメモリ OK NG

Slide 45

Slide 45 text

プロセスとカーネルの仮想アドレス空間 45 OK NG OK OK syscall発行/割込・例外発生 復帰 プロセス カーネル

Slide 46

Slide 46 text

問題のあるコード 46 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE]; Dont_use = probe[(*p) * PAGE_SIZE]; ● 内部的には以下2つの流れを並列実行 a. pへのアクセス権限チェック -> 無ければ流 れbの実行結果を捨てる -> 例外発生 b. (*p)をロード -> probe[(*p) * PAGE_SIZE] をロード

Slide 47

Slide 47 text

問題のあるコードの実行過程 47 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE]; Dont_use = probe[(*p) * PAGE_SIZE]; 1. 流れbが先に完了 a. pへのアクセス権限チェック中 … b. (*p)をロード-> probe[(*p) * PAGE_SIZE] をロード

Slide 48

Slide 48 text

問題のあるコードの実行過程 48 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE]; Dont_use = probe[(*p) * PAGE_SIZE]; 1. 流れbが先に完了 a. pへのアクセス権限チェック中 … b. (*p)をロード-> probe[(*p) * PAGE_SIZE]を ロード 2. 流れaが後で終了 a. 権限がないことがわかる -> 流れbの実行 結果を捨てる -> 例外発生 b. 流れaの完了待ち

Slide 49

Slide 49 text

問題のあるコードの実行過程 49 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE]; Dont_use = probe[(*p) * PAGE_SIZE]; 1. 流れbが先に完了 a. pへのアクセス権限チェック中 … b. (*p)をロード-> probe[(*p) * PAGE_SIZE]を ロード 2. 流れaが後で終了 a. 権限がないことがわかる -> 流れbの実行 結果を捨てる -> 例外発生 b. 流れaの完了待ち 論理的に読んでいないはずの (*p)をprobe[]から復元可能

Slide 50

Slide 50 text

Variant 3を利用したFlush+Reload攻撃の例 ● メインルーチン a. SIGSEGVのシグナルハンドラを登録 b. setjmp()。登録時は処理cへ。longjmp()による復帰時は処理 eへ c. 初期化: probe[]をnot on cacheへ d. 抜き取り。SIGSEGV発生 e. 復元 ● SIGSEGVハンドラ a. longjmp()でメインルーチンの処理 bへ 50

Slide 51

Slide 51 text

現実的な攻撃方法 ● 攻撃対象システムから一般ユーザ権限でプロセスを実行できればOK ● 任意の物理メモリを読み出せる ○ ファイルの中身 ○ 他のユーザのメモリ ● 激ヤバ ● ユーザ貸し、コンテナ貸しのようなマルチテナントシステムでとくにヤバい 51

Slide 52

Slide 52 text

Variant 3への対策 ● Kernel Page Table Isolation(KPTI) ● KPTIの有無によるカーネル処理の違い ● 性能劣化要因 ● PCID 52

Slide 53

Slide 53 text

Kernel Page Table Isolation (KPTI) ● プロセスのアドレス空間にカーネルのメモリをマップしない ● Linux v4.15に取り込まれた & 各種distroのカーネルにもバックポートされた 53 仮想アドレス空間 物理アドレス空間 プロセスAのメモリ OK NG(マップされてないので触りようがない )

Slide 54

Slide 54 text

プロセスとカーネルの仮想アドレス空間(KPTIあり) 54 カーネルの仮想アドレス空間 プロセスの仮想アドレス空間 カーネル領域 syscall発行/割込発生 復帰

Slide 55

Slide 55 text

KPTIが無い場合 ● ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ○ ページテーブルを切り替えない ○ TLBはフラッシュしない ● コンテキストスイッチ ○ ページテーブルを切り替える ○ TLBはフラッシュしない ● カーネル→ユーザへの遷移 ○ ページテーブルを切り替えない ○ コンテキストスイッチ (これ以降CSと記載)したときはユーザ空間の TLBをフラッシュ 55

Slide 56

Slide 56 text

KPTIがある場合 ● ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ○ ページテーブルを切り替えない => 切り替える ○ TLBはフラッシュしない => する ● コンテキストスイッチ ○ ページテーブルを切り替える ○ TLBはフラッシュしない => する ● カーネル→ユーザへの遷移 ○ ページテーブルを切り替えない => 切り替える ○ CSしたときはユーザ空間の TLBをフラッシュ => CSしたときは全TLBをフラッシュ 56

Slide 57

Slide 57 text

性能劣化要因 ● ページテーブル切り替え増加 => sysが増加 ● TLBフラッシュ増加(とくにコンテキストスイッチせずカーネル空間からユーザ空間に 復帰した場合) => TLBミス増加 =>ユーザ空間で起きればuser増加、カーネル空 間で起きればsys増加) ● その他諸々の追加処理の実行によるコスト => sys増加 ● 上記一連の処理によるキャッシュ使用量増加 = キャッシュミス増加 => ユーザ空間 で起きればuser増加、カーネル空間で起きればsys増加) 57

Slide 58

Slide 58 text

Process Context IDentifier(PCID) ● TLBエントリごとに存在するコンテキスト(通常はプロセス)識別用タグ ● 特定プロセス用のTLBエントリのみフラッシュ可能 ○ コンテキストスイッチごとの TLB全フラッシュが不要に ● Sandy Bridge(2011年)から追加 ○ Haswell(2013年)より前はINVPCID命令が無いため、あまり役に立たない 58

Slide 59

Slide 59 text

PCIDを使ったKPTIによる性能劣化の緩和 ● ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ○ ページテーブルを切り替ない => 切り替える ○ TLBはフラッシュしない => する => しない ● コンテキストスイッチ ○ ページテーブルを切り替える ○ TLBはフラッシュしない => する => しない ● カーネル→ユーザへの遷移 ○ ページテーブルを切り替えない => 切り替える ○ CSしたときはユーザ空間の TLBをフラッシュ => CSしたときは全TLBをフラッシュ => CSしたときは 切り替え前のプロセス用 TLBエントリをフラッシュ 59

Slide 60

Slide 60 text

みなさんのマシンでの具体的な性能インパクト ● 個々のシステムにおける各処理の性能を要計測 ● マイクロベンチを取って個々の要因によってどれだけ劣化しうるかの基礎データを 持っていると分析しやすい ○ カーネルブートパラメタ ”pti”によって有効/無効を切り替えられる ○ マイクロベンチだけ採取して「最大ナントカ %性能劣化!以上!」と騒ぐのはナンセンス ● Intel曰く、新しい石ほど影響は小さい 60

Slide 61

Slide 61 text

おしまい 61

Slide 62

Slide 62 text

参考文献1: 概要を掴むために ● Reading privileged memory with a side-channel ○ Google Project ZeroによるSpectreとMeltdownの概要。まずはこれを読むといい ○ https://googleprojectzero.blogspot.jp/2018/01/reading-privileged-memory-with-side.html ● Exploiting modern microarchitectures: Meltdown, Spectre, and other attacks ○ SpectreとMeltdownの概要説明。ハードの実装について若干書いてくれている ○ https://fosdem.org/2018/schedule/event/closing_keynote/attachments/slides/2597/export/event s/attachments/closing_keynote/slides/2597/FOSDEM_2018_Closing_Keynote.pdf ● KPTI/KAISER Meltdown Initial Performance Regressions, Brendan Gregg ○ Meltdownの性能影響についてのマイクロベンチ採取例 ○ http://www.brendangregg.com/blog/2018-02-09/kpti-kaiser-meltdown-performance.html 62

Slide 63

Slide 63 text

参考文献: 深く知るために ● Spectre Attacks: Exploiting Speculative Execution ○ Spectreの原著論文。完全理解したければこれを読みましょう ○ https://spectreattack.com/spectre.pdf ● Meltdown ○ Meltdownの原著論文。完全理解したければこれを ○ https://meltdownattack.com/meltdown.pdf ● linuxカーネルソースのDocumentation/x86/pti.txt ○ KPTIの目的、実装、性能インパクトについて書いている 63