FPGA開発日記

FPGAというより、コンピュータアーキテクチャかもね! カテゴリ別記事インデックス https://sites.google.com/site/fpgadevelopindex/

Meltdown, Spectre で学ぶ高性能コンピュータアーキテクチャ

巷ではIntel, AMD, ARMを巻き込んだCPUのバグ "Meltdown", "Spectre" が話題です。 これらの問題、内容を読み進めていくと、コンピュータアーキテクチャにおける重要な要素を多く含んでいることが分かって来ました。

つまり、このCPUのセキュリティ問題を読み解いていくと現代のマイクロプロセッサが持つ、性能向上のためのあくなき機能追加の一端が見えてくるのではないかと思い、Google, Intelの文献を読み解いてみることにしました。

が、私はセキュリティの専門家ではありませんし、過去にデスクトップPC向けのような大規模なCPU設計に参加したこともありません。 あくまでコンピュータアーキテクチャに比較的近い場所にいる人間として、この問題の本質はどこにあるのか、可能な限り読み解いていき、現代のマイクロプロセッサが持つ高性能かつ高機能な内部実装について解き明かしていきたいと思います。

と偉そうなことを書きましたが、私自身本質的なところの理解はまだ及んでいません。 3つの問題があると書いてありますが、それが全部根本的な原因は同じに見えるし、概念的なところでしか理解が及んでいないのが悔しい。

参考文献

Intel Analysis of Speculative Execution Side Channels

https://newsroom.intel.com/wp-content/uploads/sites/11/2018/01/Intel-Analysis-of-Speculative-Execution-Side-Channels.pdf

Reading privileged memory with a side-channel

https://googleprojectzero.blogspot.jp/2018/01/reading-privileged-memory-with-side.html

用語集

Googleセキュリティチームのブログには、用語集としてCPUの非常に重要な技術についての解説が簡単に書いてある。 これをきちんと理解するためには、現代のCPUが実装しているアウトオブオーダ実行技術と仮想化・キャッシュメモリのレイテンシについてきちんと理解する必要がある。

  • 投機実行
  • リタイア

An instruction retires when its results, e.g. register writes and memory writes, are committed and made visible to the rest of the system. Instructions can be executed out of order, but must always retire in order. (原文より)

現代のアウトオブオーダプロセッサは過度な投機実行を行うため、その投機実行を行った命令が本当に「正しい」命令であるかどうかを確認する必要がある。

アウトオブオーダ実行により外部メモリアクセスや計算に時間のかかる命令のレイテンシを隠蔽するために、長い命令と無関係な命令は順序を逆転して先に実行される。 特に条件分岐命令などは、条件がTrueかFalseかが分かり、さらに分岐先アドレスが決定されるまでにパイプライン中を何段も通過する必要があり、分岐命令の後続の命令は条件次第で実行すべきであったりそうでなかったりする。

従って、アウトオブオーダプロセッサでは、命令の発行は命令の順序を入れ替えても良いが、リオーダバッファなどの方式を用いて最終的には命令の順番をプログラムどおりに戻す。 分岐命令よりも後続の命令が、分岐命令よりも先に計算を終了したとしても、その結果はプログラムとしては確定に張らない。リオーダバッファに溜め込まれ、前の命令(条件分岐命令)の結果が確定したことをもってその命令が有効かどうかを決定する。このステージで「命令がコミット」されると、その命令が実行されることが確定し、リオーダバッファからレジスタファイルへのデータ書き戻しなどの確定処理が行われる。そうでなければ、その命令はリオーダバッファから「破棄される」。

このとき、コミットされた命令は命令の実行が確定したとして、最後の書き戻し処理を行って「リタイア」される。

リオーダバッファについては私のブログでも言及している。

msyksphinz.hatenablog.com

ちなみに上記の解説記事にも出てくるが、ロード命令は投機実行を行っても良い。しかしストア命令はダメだ。 何故かというと、ロード命令はロード値をレジスタファイルに書き込む前にリオーダバッファなどに格納して「破棄」することができる可能性を残しているが、ストア命令は発行してしまうとメモリの状態を変えてしまうため、破棄することが出来なくなる。従って、ロード命令のみ投機実行が行われ、ストア命令は投機実行されないというのが基本だ。

  • 論理プロセッサコア

A logical processor core is what the operating system sees as a processor core. With hyperthreading enabled, the number of logical cores is a multiple of the number of physical cores. (原文より)

いわゆるマルチスレッディング、ハイパースレッディングの技術である。 物理的なコア数よりも多くのコアがオペレーティングシステムからは見えるようになっており、パイプライン中に複数のプロセスが実行される。

1プロセスではパイプラインを埋めることが厳しいようなプログラムでも、独立した複数のプロセスを同時にパイプライン中に時分割で流すことにより、パイプラインを効率的に活用することが出来る。

  • キャッシュデータ・アンキャッシュデータ

In this blogpost, "uncached" data is data that is only present in main memory, not in any of the cache levels of the CPU. Loading uncached data will typically take over 100 cycles of CPU time. (原文より)

組み込み業界だけの話かと思ったらそうでもない。

一般的にリアルタイムデータや制御データなどのタイムクリティカルなデータは、キャッシュに入れてしまうと実際にコアの外のバスに流れるのがいつになるのか分からないため、アンキャッシュなデータとしてレジスタに持ってきたり、レジスタからストアする。

このようなアンキャッシュなデータはキャッシュを汚すことがないというのが1つの利点であり、またすぐにコア外のバスまで出て行くのでタイミングクリティカルな制御信号などを流すのに使われる。

f:id:msyksphinz:20180105230453p:plain
  • ミスプレディクションウィンドウ

The time window during which the CPU speculatively executes the wrong code and has not yet detected that mis-speculation has occurred.

上記の投機実行では、分岐予測が完了するまでとりあえず次の命令をフェッチして発行し続けるが、実際にはこれらの命令は条件分岐予測が外れることにより破棄されるかもしれない。

ミスプレディクションウィンドウは、この条件分岐命令において分岐予測に失敗することで、どれくらいの後続の命令が発行されるかを示しており、このウィンドウが大きいほど多くの命令が破棄される。

通常のレジスタ比較の命令ではレジスタリードしてから比較するだけのため、このウィンドウは比較的小さいと思われるが、例えばインダイレクトジャンプ命令(レジスタ間接分岐)の場合で、ターゲットのレジスタを外部の遠いメモリからロードしている場合はアドレス確定までかなり時間がかかる。 分岐の成立・不成立だけでなく、正しく分岐アドレスを予測できるのかが重要な鍵となる。

条件分岐予測って一見単純なようだが、

  • 条件を正しく予測できるか
  • 分岐先アドレスを正しく予測できるか

の2つが入り混じっており、結構面倒くさい分野であったりする。

バリアント1: Bounds check bypass

まず最初はこれ。理屈としては結構分かりやすい。

現代のアウトオブオーダプロセッサには、投機実行およびハードウェアプリフェッチという機能がある。 Intelのマニュアルにあるとおり、

Implicit caching occurs when a memory element is made potentially cacheable, although the element may never have been accessed in the normal von Neumann sequence. Implicit caching occurs on the P6 and more recent processor families due to aggressive prefetching, branch prediction, and TLB miss handling.

つまり、キャッシュ可能な領域であれば、ハードウェアが自動的にプリフェッチを走らせることもあれば、投機的にメモリフェッチを行うことがある。 これを、命令順どおりに実行されるわけではないという意味で「von Neumannシーケンス」ではないとしている。 分岐予測のペナルティを軽減するための機能でもあるし、TLBミスが発生する前にハードウェアプリフェッチを行っておくという機能もあるのかもしれない。

つまり、現代の高性能プロセッサはプログラムの意図しないところで勝手にプリフェッチを実行し、なるべくミス時のペナルティを軽減しようとしている。

これが間接分岐予測だとさらに問題は面倒になる。 レイテンシが長いため、もしL1→L2→L3と全てミスして外部までデータを取りに行き、順番にキャッシュを汚していく可能性があり、意図しないデータをキャッシュに持ってきている可能性がある。

以下の例が分かりやすい。

struct array {
 unsigned long length;
 unsigned char data[];
};
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
/* >0x400 (OUT OF BOUNDS!) */
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
 unsigned char value = arr1->data[untrusted_offset_from_caller];
 unsigned long index2 = ((value&1)*0x100)+0x200;
 if (index2 < arr2->length) {
   unsigned char value2 = arr2->data[index2];
 }
}

条件分岐(if文)にarr1->lengthの要素が使われているが、この要素は実際に値を取ってくるまで分からない。

しかしプロセッサはif文が成立すると予測した場合、勝手にarr1->data[untrusted_offset_from_caller]をハードウェアがフェッチを行い、そのデータをキャッシュまで持ってきている可能性があるということだ。

バリアント2: Branch target injection

これも分岐予測についての基本的な考え方を理解するための良い教材だ。 ってか自分も良く分かっていないことが多い。

KVM(Kernel VM)という機構については少しだけ聞いたことがあるのだが、しっかり調べてみると以下のウェブサイトに行き着いた。 http://www.atmarkit.co.jp/ait/articles/0903/12/news120.html なるほど、要するにハードウェアの仮想化支援機能を使った仮想化であり、ベースにはLinuxなどのOSが存在する。 VMwareVirtualBoxと違い、ホストOSの上に仮想化層が必要ない、これによりオーバヘッドを削減することができる。

Haswellの分岐予測機構の構造

ここから、Haswellの分岐予測機構について怒涛の解説が始まる。

分岐予測といっても、その方式は1つではなく、命令の種類によって多くの分岐予測機構が存在し、その特性によって使い分ける。

通常の分岐予測機構

PC相対、レジスタ値やフラグなどの値に応じて分岐が成立するかを決める。この場合は分岐先アドレスは固定(PC相対)であることが多い。

MIPSRISC-V命令では、BEQ, BNEなどの命令がこれに該当し、レジスタ値を比較してPC相対でジャンプする。

間接ジャンプ(間接呼び出し)

関数呼び出しや関数ポインタをイメージすればよい。関数の存在場所はメモリ中にアドレスリストとして格納されており、CPUはそのアドレス情報をロード命令を使ってレジスタに取得し、レジスタ相対でジャンプする。

従って、この場合はレジスタ相対ジャンプであり、レジスタの値が決定するまでジャンプ先アドレスが決まらない。また、MIPSRISC-V等でいうjal命令のように、条件分岐というよりも分岐することは決まっており、その分岐先アドレスを予測することが重要になる。

関数戻りアドレスを予測する

これは間接ジャンプと考え方は良く似ているが、アドレスが予測しやすいタイプである。 関数呼び出しは、よっぽどのことがない限り関数が終了すると呼び出し元に戻ってくる。 従って、関数呼び出しの命令(例えばjal命令)が実行されれば、関数から戻ってくるときは必ずjal命令の次のアドレスからフェッチが始まるはずだということが分かる。

従って、この関数呼び出しの場所をスタックに保存しておき、関数から戻るとき(ret命令などが実行された場合)は、次の関数フェッチ先をスタックから取り出して命令フェッチに使用する。

一般的な分岐予測機構の仕組み

分岐予測を実現するためにはいくつかの機構が必要だが、ここでは、分岐ターゲットバッファ(Branch Target Buffer:BTB)、分岐履歴テーブル(Branch History Table:BHT)が紹介されている。

分岐ターゲットバッファはその名のとおり、ソースアドレス(現在のPCアドレス)から、次にどの命令をフェッチするか(分岐先はどこか)を記憶しておくバッファである。 Haswellの分岐ターゲットバッファはソースアドレスとしてPCの下位32ビットを使うとしている。例えばPCアドレス 0x4141_0004_1000 で分岐が実行され 0x4141_0004_5123 にジャンプしたとすると、そのジャンプ履歴がBTBに記録される。

PCアドレスのうち32ビットよりも上位のアドレスはBTBの検索には利用しない。とりあえず下位の32ビットを使ってソースアドレスの参照を行い、さらに後続のステージで分岐タグバッファを参照して32ビットよりも上のビットをチェックし、上位ビットもマッチすれば分岐予測がヒットしたとしてBTBのエントリ値が次のフェッチアドレスとして使用される。

しかし、下位32ビットを使うといっても、32ビットのアドレスエントリを持つBTBバッファを作ってしまうと、232のエントリを持つBTBテーブルを作る必要が生じてしまい、ここはエントリアドレスをさらに省略してBTBのエントリ数を減らす。

ここでアドレス圧縮(というかXORによるビット数減らし)が行われる。

bit A bit B
0x40.0000 0x2000
0x80.0000 0x4000
0x100.0000 0x8000
0x200.0000 0x1.0000
0x400.0000 0x2.0000
0x800.0000 0x4.0000
0x2000.0000 0x10.0000
0x4000.0000 0x20.0000

またこの表が非常に分かりにくい。 要するにこの表を使って、アドレスのマスクを行い、bitAとbitBの1になっている部分のアドレスビットを互いにXORし、その結果をBTBのエントリアドレスの一部として利用する。

f:id:msyksphinz:20180105234119p:plain
f:id:msyksphinz:20180105234316p:plain
  • 例その1. アドレス 0x0100_0000 と 0x0180_0000 は、23ビット目と14ビット目のXORが、0 xor 0 = 0, 1 xor 0 = 1 なので互いに異なり識別可能。
  • 例その2. アドレス 0x0100_0000 と 0x0180_8000 は、24ビット目と15ビット目のXORが、0 xor 0 = 0, 0 xor 1=1だが、23ビット目と14ビット目のXORが、0 xor 0 = 0, 1 xor 0 = 1 , なので互いに異なり識別可能。
  • 例その3. アドレス 0x0100_0000 と 0x0140_2000 は、26ビット目と17ビット目のXORが、0 xor 0 = 0, 1 xor 1=0なので識別できない。
  • 例その4. アドレス 0x0100_0000 と 0x0180_4000 は、27ビット目と18ビット目のXORが、0 xor 0 = 0, 1 xor 1=0なので識別できない。

となってしまい、最後の2つのアドレスはBTBエントリにとって区別がつかなくなる。

間接分岐ジャンプにも利用される分岐履歴テーブル

上記の例では、非常に単純なBTBのアドレスエントリ生成方式を見たが、実際には分岐履歴テーブル(Branch History Table:BHT)、もしくは分岐履歴バッファ(Branch History Buffer:BHB)との組み合わせで利用される。

Haswellには、この分岐履歴テーブルが29本あるとしている。 分岐履歴テーブルを利用して過去の29回分の分岐履歴(分岐した場合のみ)を格納している。

29本あるといっても何だか分からないので、もう少し噛み砕いて分岐履歴テーブルを解説してみよう。

局所分岐予測と広域分岐予測の考え方の違い

分岐予測には大きく分けて局所分岐予測と広域分岐予測の2種類の考え方がある。

局所分岐予測というのは、大学の学部の授業でも頻繁に登場する考え方だ。

「このアドレスの分岐命令は過去何度も分岐が成立しているから、次も成立するだろう」という考え方である。つまり、現在のPCアドレスとそのアドレスの分岐命令の結果しか見ないので、局所分岐予測と言われる。

ところが、プログラムのシーケンス的にはそうでない場合も存在する。

「このアドレスの分岐命令を実行する前に、1つ前はAの場所で分岐が成立、2つ前はBの場所で分岐外不成立だった。このパターンを見ると次の分岐は成立と予測できる」なんてケースがある。 これはC言語などで、if文を並べて書いているとこのような状況が普通に起きる。 つまり、今現在のPCアドレスの分岐命令の結果だけでなく、その1つ前に実行された別のPCアドレスの分岐予測の結果、さらに、過去の別PCアドレスの分岐予測の結果まで考慮して分岐予測を実行する。 このためこれを大域分岐予測と呼ぶ。

実際には、局所分岐予測の場合には各分岐命令毎に分岐履歴バッファを持ち、広域分岐予測の場合は、全ての分岐命令で共通の分岐履歴バッファを使うため面積は削減できる。

その代わり、広域分岐予測の場合は分岐履歴バッファを長めに保持する必要があり、履歴バッファが長くなるとそのパタンをアドレスとして用いるパタン履歴テーブルのエントリ数も長くなってしまうという弱点がある。 そして広域分岐予測は、十分テーブルを大きくしても局所分岐予測よりも成績が少し悪い。

このBHBのアップデートを示した擬似コードが、以下だ。分岐を実行したソースアドレスとそのとび先アドレスを次々とXORしていることが分かる。これにより、分岐のシーケンスが記憶され、次の分岐予測に使用される。

void bhb_update(uint58_t *bhb_state, unsigned long src, unsigned long dst) {
 *bhb_state <<= 2;
 *bhb_state ^= (dst & 0x3f);
 *bhb_state ^= (src & 0xc0) >> 6;
 *bhb_state ^= (src & 0xc00) >> (10 - 2);
 *bhb_state ^= (src & 0xc000) >> (14 - 4);
 *bhb_state ^= (src & 0x30) << (6 - 4);
 *bhb_state ^= (src & 0x300) << (8 - 8);
 *bhb_state ^= (src & 0x3000) >> (12 - 10);
 *bhb_state ^= (src & 0x30000) >> (16 - 12);
 *bhb_state ^= (src & 0xc0000) >> (18 - 14);
}
f:id:msyksphinz:20180105235801p:plain

分岐予測器の内部をリバースエンジニアリングする

Googleのチームでは以下のようなテストを行っている。 2つのプログラムを、1つの物理コア(ただし2つのプロセス:論理コア)で実行する。

ここで、ASLRというのはアドレスをランダム化する機構で、これを使ってしまうと2つのプログラムで使用するPCアドレスがずれてしまうためこの機能を無効化している。

ここで再現したいのは、2つのプログラムで分岐予測テーブルが共有されてしまいことにより他のプロセスの情報が見えてしまうことである。

そのために、関数ポインタのコールを行いテスト変数へのアクセスを実行する。これによりテスト変数はキャッシュに格納される。 その後その値をCLFLUSH(これはx86のキャッシュラインのフラッシュ命令である)を使ってフラッシュしてしまう。 次に、「現在の分岐予測器の状態」を作り出すために、必ず成立する分岐命令をN回実行する(ここで「必ず成立する」というのは、上述したとおりBHBは成立した分岐しか記録しないためだ)。

そしてそれぞれインスタンス1とインスタンス2が値をテストしているときに、再びテスト値をキャッシュに読み出す。このとき、インスタンス1とインスタンス2が同じ分岐ヒストリのパタンを使えば、インスタンス2も分岐予測のレイテンシを埋めるために投機的にテスト値を自分のレジスタに持ってくるだろう。

その後インスタンス1とインスタンス2でテスト値を取得したところ、インスタンス2はインスタンス1の値を見ることが出来たというわけか。

ここでNの数を増やしていくことを考える。上述したようにBHBはプログラムのソースアドレスとターゲットアドレスの情報をXORで記録しているはずだ。 N=25とすると、この間違いは発生しなかった。つまり、N=25までは分岐予測器は2つのインスタンスの違いが識別できている。 ところが、N=26とすると途端にこの現象が発生し始めた。 従って、Haswellの場合はこの少なくとも26個までの分岐履歴を保存しているということになる。

バリアント3: Rogue data cache load

バリアント3の解説について指摘があり、少なくとも過去の解説について重大な誤りがありました。申し訳ありません。文献をよく読み直し修正しましたが、この解説も疑問が残り誤っている可能性があります。鵜呑みにしないようよろしくお願いします。

こちらも投機的実行を使ったもので、ユーザモードのプログラムが、カーネルモードの領域を参照するプログラムの投機実行を行った途中結果までを利用してデータを読み取るという巧妙なものになっている。

まずは以下の解説を読むべし。

投機的実行のハードウェア構造

こちらは原文から参照させてもらっている、アウトオブオーダプロセッサの構成だ。

これを見ると、パイプラインを流れていく命令は、リオーダバッファを通過した後に命令の種類ごとに異なるユニットに発行されることが分かる。 ポートは0から8まで用意されており、整数命令はポート0,1、ロードストアは2,3といった具合だ。

そして重要な点は、違うポートに発行された命令は、プログラムの順番とは独立に実行が進んでいき、ポーとさえ違っていれば、レイテンシの短い命令はレイテンシの長い命令を追い越すことができる。 これが非常に重要な役割を持つ。

もう一つの特徴は、ある命令で例外が発生したとしても、実際に例外が発生するのは命令が実行ユニットを通過し、命令がリタイアするときにのみ例外が発生するということだ。

例えば極端な例を挙げてみる。

load  dst1, mem[A]      // dst2 <- mem[A] のメモリロード
add.f dst2, dst2, 10.0  // dst2 <- dst2 + 10.0 の浮動小数点演算

ここで、2番目のadd.fで浮動小数点例外が発生したものとしよう。 ところがレイテンシ的にloadのほうが時間がかかり、add.fの方が先に命令の実行が終了し、例外を検出できたものとする。

しかし1番目のloadの完了を待たずに、add.fの例外に飛んでしまうと、プログラムの意味的におかしなことになってしまう。 ソフトウェア的にみると、loadが完了していないのに勝手にadd.fの例外が発生したとして誤動作としてとらえられてしまい、これを防ぐために、必ず例外はリオーダバッファによって命令が完了するときに実行される。

リオーダバッファが命令を完了させるときは、必ず命令はプログラムの順番通りに戻っているため、add.fの例外発生(つまりadd.fが完了しリタイアする)ときは、それよりも前に発行されたloadも必ず終了しており、プログラム的に意味を損なわない、という訳だ。

f:id:msyksphinz:20180106190624p:plain

さて、以上の前提に基づいて、最初の原文に戻って問題を解析してみる。

アウトオブオーダによってカーネルモード命令を結果をうまく使いまわす仕組み

まず、以下のプログラムを考えてみる。

mov rax,[somekernelmodeaddress]

これをユーザモードで実行した場合、余裕で例外が発生する。ダメに決まってる!

しかし、例外が発生するのは、「リオーダバッファに入って命令が完了するとき」であるということを思い出してほしい。 つまり、実際にカーネルモードにアクセスを行い、データをリオーダバッファ中、もしくはキャッシュに持ってきている可能性がある。 これを原文中では、「マイクロアーキテクチャの状態を変更している可能性」としている。

次のプログラムだ。

mov rax, [Somekerneladdress]
mov rbx, [someusermodeaddress]

上記のアウトオブオーダ実行の図を見ると、ロードストアの命令ユニットは2つあるようだ。 しかも上記の2命令には依存関係がないので、この2つは同時に実行することができる。 そして1番目の命令は例外を発生しているのだが、2番目の命令はすでにロードを行っているか、少なくともキャッシュに[someusermodeaddress]のデータをロードしているかもしれない。 これは例外が発生した後に、当該キャッシュをアクセスしてみると確認することができる。

次のプログラムだ。これは上記と違って依存関係を持っている。

mov rax, [somekerneladdress]
and rax, 1
mov rbx,[rax+Someusermodeaddress]

ここで、2番目と3番目の命令が同時に発行されているものとする。

重要なのは2番目のmovの実行結果は、raxつまりsomekerneladdressの値に依存しているということだ。 つまり、アウトオブオーダで2番目のmovの途中結果がキャッシュに保存されていると、その情報を頼りにsomekerneladdressの値を特定することが可能になる!

ここで原文の著者が随分と苦労しているのが、じゃあ1番目の命令が例外を発生させる前に、どうにかして2番目と3番目の命令を実行させなければならない。

これを行うために、mov命令で使用するLoad/Storeとは別のユニットを使って実行するand命令を挿入してある。 Load/Storeユニットを使う命令とは別の命令挿入することで、CPUはこれらの命令を投機的に実行することができるようになる。 (筆者の解釈: ここの説明は正直よく分からない。別のユニットを使っても、投機的な実行を確認することにはならないのでは?)

正しく理解できているか微妙だが、おそらくこういうことだ。

最初のmov命令がカーネル領域のデータをraxにロードするが、実際にはこの命令は例外を発生させる。 この例外が発生する前に、投機的実行によりand命令と次のmov命令を実行することができれば、その痕跡をキャッシュに残すことができるだろう、という考え方だろう。

f:id:msyksphinz:20180106190644p:plain

ただし、この考え方も合っているかは疑問が残る。 そもそも、最初のmovがリタイアするまでに十分な時間が確保したい場合、最初のmovの前にさらに時間がかかる処理を挿入すれば、リタイアまでの時間を大幅に稼ぐことができるのではないか? その間に2番目と3番目の命令を投機実行させることで、ユーザモードのデータ領域をL1に十分な余裕をもって残すことができそうなのだが...

// この部分に非常にレイテンシの長い命令を挿入する。
mov rax, [somekerneladdress]       // ↑の命令の影響でこの命令はなかなか例外を出すことができない
and rax, 1                         // その間に、この命令と
mov rbx,[rax+Someusermodeaddress]  //           この命令がkerneladdressのデータに基づいてユーザ領域のデータを取り出す。

まあ、分からん。

マイクロアーキテクチャから見たこれらの脆弱性の修正方法

また、Intelの公式声明でもこれらの解決法(Mitigation:緩和法?)について言及されている。

Bound check Bypass の解決方法

基本的にはソフトウェアによる解決法が望まれている様子。 実際にアクセスを行ってはいけないシーケンスについては、FENSE命令を使って明示的にメモリアクセスの同期を行い、ハードウェアプリフェッチを防ぐという方式。

Branch Target Injection の解決方法

こちらはあまりパッとしないのだが、プロセッサとシステムソフトウェアの間に新しいインタフェースを仕込むという風になっている。 マイクロコードによるアップデートとしては、

  • Indirect Branch Restricted Speculation (IBRS): 間接分岐の投機実行を制限する。
  • Single Thread Indirect Branch Predictors(STIBP): 間接分岐の投機的移行は、1つのスレッドのみが実行できるように制約する。
  • Indirect Branch Predictor Barrier(IBPB): 前のプログラムの挙動が、間接分岐の分岐予測に影響しないようにする。

Rogue Data Cache Load の解決方法

これはユーザモードとスーパバイザモードのページ構造を分け、分離することで解決できるとしている。

まあどっちにしても、ソフトウェアによる修正が必要ということだな。