SpectreとeBPF
新年早々巷にいろいろと賑わいをもたらしたSpectreとMeltdownですが,Google Project Zero (GPZ)が公表したSpectreの攻撃コード例の中でeBPFが使用されていました. 安全を謳っているものの昨年もいくつか脆弱性が発見されていたので,「またeBPFか」と思った方もいるかもしれません. といっても今回の件をeBPFの脆弱性と言ってしまうのは可哀想というか,ちょっと違うかなとは思いますが.
若干今さら感ありますがここでは簡単にeBPFがどう攻撃に使われたか簡単に書こうと思います. ちなみに後述するようにすでに対応策のパッチが存在しますので, 現在eBPFでSpectreを利用して攻撃するのは現実的にはかなり難しいのではないかと思います.
Variant1
SpectreのVariant1 (CVE-2017-5753)の基本となる問題は,以下のようなコードがあったとき,
if(index < max_index){ // 何か処理 }
index >= max_index
であっても投機的実行によりif文の中が実行されてしまうことがあるというものです.
もしindex >= max_index
であれば投機的実行された結果は破棄されるので問題ない,と信じられてきましたが,例えば以下のようなコードを考えます.
if(index < max_index){ value = array[index]; _ = array2[(value & 1) * 0x100]; }
array2
のデータはキャッシュされていないとします.投機実行でif文の中が実行されたとき,
array[index]
の下位1bitの値に応じて,array2[0]
あるいはarray2[0x100]
のいずれかにアクセスされ,結果としてアクセスされた領域がキャッシュに乗ります.
従ってこの後array2[0]
とarray2[0x100]
のアクセス速度を比較することでarray[index]
の下位1bitの値が分かります.
これを応用してあるプロセスのメモリ空間のデータを読み取るのがvariant 1ですが,そもそもこの攻撃を実施するには,
- 攻撃対象のプロセスの中に上例のようなコードパターンが存在する
index
は攻撃者がコントロールできる- 適当な
array2
に攻撃者がアクセスできる - データがキャッシュに乗っているかどうか判断できるだけの精度を持つタイマが利用できる
というようなことが条件になってきます. そんなプログラム果たしてあるのかという話ですが.可能性としては外部のコードを実行するインタプリタやjitエンジンが一番あるのかなと思います. 実際GPZではeBPFを利用したカーネル内データの読み出し,論文の方ではV8のデータの読み出しのPoCを示しています.
eBPFを利用した攻撃例
GPZの攻撃例コードはこちらで公開されています.
variant 1の攻撃ではeBPF mapにアクセスする際の境界チェックのコードを利用します.
特に攻撃例で利用されているのはbpf_array
の読み出し(array_map_lookup_elem()
)及びbpf_tail_call()
です.
いずれもstruct bpf_array
のデータにアクセスする際に,array->map.max_entries
の確認をおこなっています.
array_map_lookup_elem()
(variant 1対策前のLinux 4.14のコードです)
https://github.com/torvalds/linux/blob/v4.14/kernel/bpf/arraymap.c#L112
static void *array_map_lookup_elem(struct bpf_map *map, void *key) { struct bpf_array *array = container_of(map, struct bpf_array, map); u32 index = *(u32 *)key; if (unlikely(index >= array->map.max_entries)) return NULL; return array->value + array->elem_size * index; }
bpf_tail_call()
https://github.com/torvalds/linux/blob/v4.13/kernel/bpf/core.c#L1009 (Linux 4.13のコード)
JMP_TAIL_CALL: { struct bpf_map *map = (struct bpf_map *) (unsigned long) BPF_R2; struct bpf_array *array = container_of(map, struct bpf_array, map); struct bpf_prog *prog; u64 index = BPF_R3; if (unlikely(index >= array->map.max_entries)) goto out; if (unlikely(tail_call_cnt > MAX_TAIL_CALL_CNT)) goto out; tail_call_cnt++; prog = READ_ONCE(array->ptrs[index]); if (!prog) goto out;
攻撃の流れは以下のようになります.
bpf_map_read()
を利用してカーネル内のデータを(投機実行で)読み出しbpf_tail_call()
を利用して読み出したデータの値に基づいて(投機実行で)ユーザスペースの領域にアクセス- ユーザスペース領域のキャッシュヒットを調べてデータを読みだす
攻撃のために2回投機実行を利用しています.
なぜ,ユーザスペースの領域にアクセスする際にbpf_map_read()
を利用していないかというと,これはおそらくbpf_map_read()
のindexが32bitである一方で,
bpf_tail_call()
のindexが64bitだったから(現在は修正済み)だと思います.
実際のeBPFコードは以下のようになっています.
struct bpf_insn insns[] = { // save context for tail call BPF_MOV64_REG(BPF_REG_6, BPF_REG_ARG1), // r7 = bitmask BPF_LD_MAP_FD(BPF_REG_ARG1, ret.data_map), BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -4), BPF_ST_MEM(BPF_W, BPF_REG_ARG2, 0, 2), BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), BPF_GOTO_EXIT_IF_R0_NULL, BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0), // r9 = bitshift selector BPF_LD_MAP_FD(BPF_REG_ARG1, ret.data_map), BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -4), BPF_ST_MEM(BPF_W, BPF_REG_ARG2, 0, 3), BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), BPF_GOTO_EXIT_IF_R0_NULL, BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 0), // r8 = prog_array_base_offset = *map_lookup_elem(data_map, &1) BPF_LD_MAP_FD(BPF_REG_ARG1, ret.data_map), BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -4), BPF_ST_MEM(BPF_W, BPF_REG_ARG2, 0, 1), BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), BPF_GOTO_EXIT_IF_R0_NULL, BPF_LDX_MEM(BPF_DW, BPF_REG_8, BPF_REG_0, 0), // r0 = secret_data_offset = *map_lookup_elem(data_map, &0) BPF_LD_MAP_FD(BPF_REG_ARG1, ret.data_map), BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -4), BPF_ST_MEM(BPF_W, BPF_REG_ARG2, 0, 0), BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), BPF_GOTO_EXIT_IF_R0_NULL, BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_0, 0), // r2 = &secret_data_offset BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP), BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -4), BPF_STX_MEM(BPF_W, BPF_REG_ARG2, BPF_REG_0, 0), BPF_LD_MAP_FD(BPF_REG_ARG1, ret.victim_map), BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem), /* speculative execution starts in here */ BPF_GOTO_EXIT_IF_R0_NULL, /* predicted: non-NULL, actual: NULL */ BPF_LDX_MEM(BPF_DW, BPF_REG_ARG3, BPF_REG_0, 0), /* * mask and shift secret value so that it maps to one of two cachelines. */ BPF_ALU64_REG(BPF_AND, BPF_REG_ARG3, BPF_REG_7), BPF_ALU64_REG(BPF_RSH, BPF_REG_ARG3, BPF_REG_9), BPF_ALU64_IMM(BPF_LSH, BPF_REG_ARG3, 7), BPF_ALU64_REG(BPF_ADD, BPF_REG_ARG3, BPF_REG_8), BPF_LD_MAP_FD(BPF_REG_ARG2, ret.prog_map), BPF_MOV64_REG(BPF_REG_ARG1, BPF_REG_6), BPF_EMIT_CALL(BPF_FUNC_tail_call), BPF_MOV64_IMM(BPF_REG_0, 0), BPF_EXIT_INSN() };
配列のオフセットを指定するためのindexは事前に作成したdata_map
という別のeBPF mapを利用して指定しています.
victim_map
という名のarrayを使ってカーネル内データ読み出し,prog_map
という名のarrayを使ってtail callを経由したユーザランド領域へのアクセスをおこなっています.
ちなみに,わざわざeBPFプログラムを利用しなくてもユーザ空間から直接投機実行でカーネル空間のデータを読み出せばいいのではないかと思った方.それがmeltdownです*1.
ユーザ空間へのオフセットの導出
さて,ここまでは比較的単純ですが,実際に攻撃する際にはprog_map
のindexとして,ユーザの特定領域にアクセスするための適当なindexを指定する必要があります.
言い換えると,prog_map
からユーザ領域へのオフセットを求める必要があります.
GPZの例ではこのオフセットの導出にもvariant 1を利用しています.
アイディアはシンプルで,prog_map
を使ってtail callを適当なindexで実行し,その後ユーザ領域がキャッシュされていればそこからオフセットが計算できます.
といっても,prog_map
はkmallocで確保*2
されますが,kmallocの空間の探索 (kernel logical address = ffff880000000000 - ffffc7ffffffffff = 64TB) を愚直に探索するのは非効率です.
そこでどうするのか...というのはGPZのブログに丁寧に書いてあるんですが,ざっくりまとめると
- 231の空間をユーザスペースに確保
- 実際には 24(=16個) のページからなる領域を215個作成
- 各領域は同じ物理メモリをマッピング
mmap(MAP_NORESERVE)
で231の空間を確保したのち,24ページサイズのファイルを作成,その領域にmmap()
で各領域にそのファイルを割り当てる
- tail callを利用してオフセットを探索.オフセットは231単位で更新する.
- キャッシュヒットを探すには最初の24ページの領域を調べればok
- ヒットが見つかると...
- 48-63: canonical address (ビットの先頭は1と決まっている)
- 32-47: brute forceのhiger bit
- 16-31: 不明
- 12-15: 領域のどのページにヒットしたかから分かる
- 0-11: 0 (kmallocで4KB単位で取得したから((mapを作成する際のサイズを2049にしている)))
- 中間の15bitがワカラナイ
- 中間ビットの求め方
- 2つのページを用意.探索したいアドレスの半分の仮想アドレスを一つのページ,もう一つを別のページに割り当て
- これを使ってバイナリサーチ
というようなことをしています,
キャッシュのフラッシュ方法
攻撃を成功させるために重要なことは,境界外のindexに対して投機実行されるようにすることです.
このために,攻撃前に境界内のindexを用いて何回か事前に関数を実行しておきます,
また,もう一つ重要なこととしてif()分の中で利用される変数(今回の場合はarray->map.max_entries
)がキャッシュされていないことが挙げられます.
x86ではclflushを利用すればキャッシュフラッシュができますが,当然ながら攻撃者がclflushを使ってarray->map.max_entries
のキャッシュをフラッシュすることは不可能です.
そこで,GPZの攻撃例ではfalse sharingを利用したキャッシュフラッシュをおこなっています.
もともとstruct bpf_map
は以下のようになっていました(現在では修正が入っています).
https://github.com/torvalds/linux/blob/v4.14/include/linux/bpf.h#L44
struct bpf_map { atomic_t refcnt; enum bpf_map_type map_type; u32 key_size; u32 value_size; u32 max_entries; u32 map_flags; u32 pages; u32 id; int numa_node; struct user_struct *user; const struct bpf_map_ops *ops; struct work_struct work; atomic_t usercnt; struct bpf_map *inner_map_meta; };
この構造ではmax_entries
とeBPF mapの参照カウントであるrefcnt
が同じキャッシュライン上に存在することになります(キャッシュラインは64byte).
refcnt
はmapを含むeBPFプログラムをロードする際(より正確にはverifierの内部)で更新されます.
そこで,攻撃プログラムを実行するCPUコアとは別のコアでprog_map
やvictim_map
を利用するeBPFプログラムをロードすると,その結果refcnt
が更新され,攻撃プログラム側のmax_entries
のキャッシュはfalse sharingによって破棄されることになります.
実際には以下のようなプログラムをロードしてfalse sharingします ((sched_setaffinity()
でスレッドを実行するコアを制御できます)).cacheline_bounce_fds
にprog_map
やvictim_map
のfdが入っています.
struct bpf_insn insns[] = { BPF_LD_MAP_FD(BPF_REG_0, cacheline_bounce_fds[0]), BPF_LD_MAP_FD(BPF_REG_0, cacheline_bounce_fds[1]), BPF_LD_MAP_FD(BPF_REG_0, 0xffffff) };
このプログラムはverifiationが通らずロードに失敗すると思いますが,キャッシュフラッシュするにはこれで十分です.
対策方法
variant 1に対する(ソフトウェア的な)対策の一つは投機実行がおきないようなプログラムを(フェンス命令などを使って)作成することです. ただし,CPUの投機実行の挙動を正確に把握することは困難です. もう一つのアプローチとしては投機実行されても問題がおきないようにする方法があります.
eBPFでは,eBPF mapにアクセスする際にindexをマスクするような修正が入りました(bpf: prevent out-of-bounds speculation).
例えば,array_map_lookup_elem()
は以下のようにindex
ではなくindex & array->index_msk
を使うように変更されています.
これにより誤って投機実行されても外部領域にアクセスすることを防いでいます.
https://github.com/torvalds/linux/blob/v4.15/kernel/bpf/arraymap.c#L140
static void *array_map_lookup_elem(struct bpf_map *map, void *key) { struct bpf_array *array = container_of(map, struct bpf_array, map); u32 index = *(u32 *)key; if (unlikely(index >= array->map.max_entries)) return NULL; return array->value + array->elem_size * (index & array->index_mask); }
また,eBPFにはrefcnt
とmax_entries
がfalse sharingされないようにする変更も入りました(bpf: avoid false sharing of map refcount with max_entries).
これにより,struct bpf_map
は以下のように変更されています.____cacheline_aligned
を利用してmax_entries
とrefcnt
が別々のキャッシュラインに乗るようにしています.
https://github.com/torvalds/linux/blob/v4.15/include/linux/bpf.h#L45
struct bpf_map { /* 1st cacheline with read-mostly members of which some * are also accessed in fast-path (e.g. ops, max_entries). */ const struct bpf_map_ops *ops ____cacheline_aligned; struct bpf_map *inner_map_meta; #ifdef CONFIG_SECURITY void *security; #endif enum bpf_map_type map_type; u32 key_size; u32 value_size; u32 max_entries; u32 map_flags; u32 pages; u32 id; int numa_node; bool unpriv_array; /* 7 bytes hole */ /* 2nd cacheline with misc members to avoid false sharing * particularly with refcounting. */ struct user_struct *user ____cacheline_aligned; atomic_t refcnt; atomic_t usercnt; struct work_struct work; char name[BPF_OBJ_NAME_LEN]; }
さらに,tail callのindexを32bitになるする修正が(Spectre問題とは関係なく昨年10月に)入っています(bpf: fix bpf_tail_call() x64 JIT). また,jitを有効化した方が攻撃に成功しやすくなるようなので,jitを無効化するというのも一つの緩和策ではあります*4.
一般的にはvariant 1の対策は個々のプログラム側でおこなわないといけない点が難しいところです.
linuxでは対策を容易にするためにarray_index_nospec
というマクロが提案されています.
これは以下のように使うことを想定しています.
if (index < size) {
index = array_index_nospec(index, size);
val = array[index];
}
このマクロはindexをマスクした値を返しますが,indexがsizeより大きい場合はマスクとして0が利用されるので,結果としてindexは0になります.
SMAPとの関係
最近のintelのCPU (broadwellあたりから?) にはSMAP(Supervisor Mode Access Prevention)と呼ばれる,カーネル空間からユーザ空間のメモリ領域へのアクセスを制限する機能が入っています. GPZでの実験ではSMAPは無効にしたマシンで実験しているようです. SMAPを有効にした際に今回のeBPFの攻撃が成功するのかはよく分かりません. meltdownで権限チェックが回避されてしまう場合があるのを考えると,intelのCPUではSMAPも回避される場合があってもおかしくはないと思います.
Variant2
Spectreのvariant 2(CVE-2017-5715)は,以下のような分岐予測の特性を利用したものです*5.
- CPUの特権レベルによらず同じ分岐予測処理が実行される
- 分岐予測判断にはソースアドレスの下位の一部しか利用されない
- 分岐ターゲットアドレスは一部しか保存されていない *6
これにより,例えばゲストOSのユーザ空間のコードがハイパーバイザのコードの分岐予測に影響を与えることが可能になります. 特にGPZのPoCでは主にindirect jumpを利用して, KVMゲストからKVMホストのメモリ領域の読み出しをおこなっています.
メモリ読み出しの基本はvariant 1と同じです.例えば,
jmp *%rax
というような分岐命令があったとき,うまいこと分岐予測を失敗させて,以下のようなコードを投機実行させます.
value = array[index]; x = array2[value+offset];
そうするとvariant 1と同様に(うまくindexとoffsetを指定すれば)データが読み出せることになります.
variant 2で攻撃するには
- データを読みだすのに使えるコード辺(ガジェット)が存在
- 適切なoffsetやindexを求めてガジェットに渡す
- 分岐予測を失敗させガジェットの関数を投機実行させる
といったことが必要になります.
まず,ガジェットに関して.GPZの例ではここでeBPFを利用します.
どういうことかというと,indexからデータを読み出してキャッシュに載せるのはeBPFプログラムでおこなうようにして,ガジェットは単にそのeBPFプログラムを実行させるのに利用します.
具体的には,以下のようなガジェット利用して,__bpf_prog_run(const void *ctx, const struct bpf_insn *insn)
を投機実行させます.
mov rsi, r9 call QWORD PTR [r8+0xb0]
__bpf_prog_run()
はその名の通りeBPFを実行するインタプリタの関数です.
System V AMD64のABIでは整数・ポインタ引数はRDI, RSI, RDX, RCX, R8, R9に順に格納されます.
従って,R8に__bpf_prog_run
のアドレス (-0xb0),R9にeBPFのプログラムを格納したメモリ領域のアドレスを格納できれば上のガジェットを投機実行した結果,eBPFプログラムが投機実行されることになります.
PoCのコードでは以下のようなeBPFプログラムを利用しています.
読み出した値のビットが1かどうかによってhost_timing_leak_addr
かhost_timing_leak_addr+0x800
をキャッシュに載せています.
struct bpf_insn evil_bytecode_instrs[] = { // rax = target_byte_addr { .code = BPF_LD | BPF_IMM | BPF_DW, .dst_reg = 0, .imm = target_byte_addr }, { .imm = target_byte_addr>>32 }, // rdi = timing_leak_array { .code = BPF_LD | BPF_IMM | BPF_DW, .dst_reg = 1, .imm = host_timing_leak_addr }, { .imm = host_timing_leak_addr>>32 }, // rax = *(u8*)rax { .code = BPF_LDX | BPF_MEM | BPF_B, .dst_reg = 0, .src_reg = 0, .off = 0 }, // rax = rax ^ (0x00 or 0xff) { .code = BPF_ALU64 | BPF_XOR | BPF_K, .dst_reg = 0, .imm = (invert ? 0xff : 0x00) }, // rax = rax << ... { .code = BPF_ALU64 | BPF_LSH | BPF_K, .dst_reg = 0, .imm = 10 - bit_idx }, // rax = rax & 0x400 { .code = BPF_ALU64 | BPF_AND | BPF_K, .dst_reg = 0, .imm = 0x400 }, // rax = rdi + rax { .code = BPF_ALU64 | BPF_ADD | BPF_X, .dst_reg = 0, .src_reg = 1 }, // rax = *((u8*)rax + 0x800) { .code = BPF_LDX | BPF_MEM | BPF_B, .dst_reg = 0, .src_reg = 0, .off = 0x800 }, // clear rdi (rdi = rdi & 0) { .code = BPF_ALU64 | BPF_AND | BPF_K, .dst_reg = 1, .imm = 0 }, // end { .code = BPF_JMP | BPF_EXIT } };
__bpf_prog_run
のアドレスやindexをどうやってガジェットに渡すかというと,GPZの例ではKVMでVMExitした際にR8やR9といったレジスタはゲストのものがそのまま引き継がれて暫く処理が実行されるという点を利用しています.
特にPoCではVMEXIT直後に呼ばれるkvm_x86_ops->handle_external_intr()
を攻撃に使用します.
このindirect jumpの分岐予測を失敗させてガジェットを投機実行させています((ちなみに,PoCでは攻撃をしやすくするためにVMEXIT直後(このへん)でkvm_x86_ops->handle_external_intr
のキャッシュを明示的にclflushでフラッシュさせています.writeupの文章によると効率良くcache evictionさせる方法がいくつか研究であるようです.)).
ハイパーバイザで分岐予測を失敗させるには,ハイパーバイザで分岐を実行するアドレスと,ガジェットのアドレスを求め,ゲストのVM内で同じアドレスを使って分岐を適当に実行させるだけです*7. 実際には分岐予測にはアドレスの一部(せいぜい下位32bit?)しか使用しないので,下位のアドレスだけを使えばユーザ空間で分岐予測のコントロールができます.
さて,それでは問題は一体どうやってガジェットのアドレスや,ホストからゲストの領域へアクセスするためのオフセットを求めるかということになります. まずKVMホストのメモリ領域にはゲストのメモリ領域がマッピングされているはずなので,適当なオフセットを利用してホストからゲストのアドレスにアクセスすることは可能です. またガジェットのアドレスや分岐命令のアドレスなどは,vmlinuxやkvm.koなどがロードされているアドレスが分かればあとは,利用しているバージョンに基づいてオフセットを計算すれば求められます.
PoCのコードではホストの物理アドレスやオフセットなどの情報はゲストに与えて実行していますが, GPZのブログの方にvariant 2を利用してこれらの必要なアドレスをゲストから求める方法が解説されています. 分岐予測履歴をダンプすることでホスト側の分岐アドレス(の下位)を求め,そこから全体のアドレスを求めるようです*8.
対応策
eBPF的にはインタプリタのコードがガジェットとして悪用されてしまうのが問題でした.
そこで,常にeBPF JITを有効にし,カーネル内にインタプリタのコードを残さないようにするBPF_JIT_ALWAYS_ON
というオプションが導入されました(従って,これはvariant 1とは逆の対応になります).
また,KVMの方では,VMEXITした後ゲストのレジスタの一部がそのままで処理が継続されるのが問題ということで,ゲストのレジスタを保存後にクリアする処理が追加されています. (コミットを見ると他にもいくつか修正が入っていることが確認できます)
一般的な対策として,Intelが低い特権レベルの分岐の影響を受けないようにするIBRS (Indirect Branch Restircted Speculation)や直前の分岐の影響を受けないようにするIBPB(Indirect Branch Prediction Barrier)といった, indirect jumpにおける投機実行を制限する機能をマイクロコードアップデートを通じて提供しています. ただしこの1月に提供されたマイクロコードにはバグがあったようで,つい先日windowsでマイクロコードアップデートを無効化するパッチが提供されていますし,この方法の安定利用にはもうしばらく時間がかかりそうです.
ソフトウェア的な解決方法としては, indirect jumpの代わりに一旦アドレスをスタックに積んでからreturnする retpolineという手法が提案されています. returnはindirect jumpとは違う分岐予測が適当されるようで, これにより仮にindirect jumpの分岐予測テーブルが汚染されても,その影響を受けないようにできるようです.
Linuxではブートオプションを通じてどの緩和策を利用するか選択できるようになっています.
variant 2もプログラムごとに対応しなければならない点が難しいところです. もうしばらくはspectre対策でいろいろとアップデートがありそうです.
*1:本来であればpage tableのパーミッションがないためアクセスに失敗するはずですが,meltdownではintel CPU特有の権限チェックの前に投機実行してしまうという性質を利用してデータをキャッシュに載せます
*2: map_create() => find_alloc_map() => alloc_map() (fd_array_map_alloc() => array_map_alloc() => bpf_map_area_alloc()
*3:L3がPIPTなのは実験的にも常識的に考えてもそうなんだと思いますが,公式にはキャッシュ構成の情報は公開されていないような気がします. パタヘネに何か書いてあったかもしれない.
*4:おそらくほぼ全てのディストリビューションでBPF JITはデフォルトで無効化されています.
*5:もちろん,分岐予測の実装はCPUアーキテクチャごとに異なり,例えばARMは分岐予測の際に特権レベル等を考慮するのでこの脆弱性の影響を受けないようです
*6:GPZの分析によると,下位32bitもしくはjump元からのオフセットの値
*7:VM内のガジェットのアドレスは特に何もする必要がないので適当にただretする関数でも作成しておきます
*8:一体どうやって分岐予測履歴を求めたのかというと,頑張ってリバースエンジニアリングしたようです.恐るべし..