投機実行とは
1990 年代までは、クロック数の増加によりプロセッサーの性能は、線形的に向上していた。
しかし 1995 年ごろともなると、光速度の限界など物理的な制約により、単純にクロックアップすれば性能が向上するという状況ではなくなってきた。
そこで、1 つの命令の実行完了時間が頭打ちになったとしても、1 つの命令を多くのステップに細分化し、それぞれのステップを並列で動作させることで、
単位時間当たりの命令実行数を向上させるという手法がとられるようになった。(スーパーパイプライン)
この手法の問題点としては、演算順序の依存性により、計算の完了していないデータを待たなければならないということがある。
その場合、計算が確定するまで、次の計算ができないので、その後の処理が一時停滞してしまい、性能に影響する。(パイプラインストール)
そのため、計算が確定しないデータがあったとしても、ある予測のもとに値を仮定し、継続する処理を実行するという手法が考案された。
その場合、計算が確定した段階で、予測が当たっていたらそのまま処理を続行し、間違っていたら、実行状態を元に戻すことになる。
これは予測が間違っていて、やり直すことがあるというリスクを許容して処理を行うということから、投機実行と呼ばれている。
予測が間違うことがあるにしても、計算リソースを遊ばせておくよりも処理効率が向上するので、現行のプロセッサーでは当たり前に実装されている。
Spectre 脆弱性とは
一見して問題なさそうな以下のコードを考察してみよう。
array1 : [size_of_array1] uint8
array2 : [0x10000] uint8
x : uintptr
if x < size_of_array1 {
y = array2[array1[x] * 0x100];
}
ここで、悪意ある実行者が x として array1 のサイズ範囲外の値を指定して、領域外読み取りを試みたとする。
論理的には、if 文の条件によって、サイズチェックが行われるため、if 節は実行されることはなく、事なきを得る。
しかしながら、投機実行の仕組みによって、if 条件が確定する前に、if 条件が真であると予測されて、
1) t := array1[x];
2) t = t * 0x100;
3) t = array2[t];
までが、投機的に実行されてしまうことあり得る。
その後、予測が誤りであることが判明すると、投機実行された計算は取り消しとなり、1) で読みだされた値は、闇に葬られるはず、であった。
ここで、現在のプロセッサーには、メモリーアクセスの性能を向上させるために、キャッシュメモリーが実装されていることを思いだしてみよう。
悪意ある実行者が、あらかじめ array2 のメモリー領域を、キャッシュから追い出していたとする。
投機実行によって、3) の操作が実行されると、array2 の特定の領域が読み込み操作によって、キャッシュメモリーに取り込まれることになる。
悪意ある実行者は、array2 の該当アドレスを順次読み取り、その読み込み速度の変化を観察することで、闇に葬られたはずの値を推定することが可能となる。
これが、
(投機的実行における)条件付き分岐における境界チェックの回避 "bounds check bypass" (CVE-2017-5753)
のあらましである。