FPGA開発日記

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

Speculative Store Bypass に学ぶ現代プロセッサの高速メモリアクセスユニットの仕組み

Microsoft および Google から、新たなCPUの脆弱性であるVariant 4およびVariant 3aについてのアナウンスがあり、Intel, AMD, Arm が対応に迫られている。

これらの脆弱性は、現代のコンピュータアーキテクチャの飽くなき性能追及の結果発生したものが多く、詳細を調べるとコンピュータアーキテクチャの勉強(復習?)にもなるのである意味重宝している。

今回発表されたのは、Variant3a / Variant 4である。それぞれ、

  • Variant 3a: Rogue System Register Read (CVE-2018-3640)
  • Variant 4: Speculative Store Bypass (CVE-2018-3639)

と名付けられている。Variant 3aについては後日調べるとして、Variant 4について調査してみた。名前の示す通り、"Speculative Store Bypass"というのは、ロードとストアの順番を交換することにより速度向上を図ったものであるが、これにより副作用が発生するという問題だ。

一般論として、ロードとストアの順番を入れ替えるという技法を使うマイクロプロセッサは、デスクトップ向けか、モバイル向けでもかなり性能に重点を置いたものに限定されるだろうと思われる。 ArmについてはCortex-A57以降の世代、IntelはCore世代が中心のようだ。

  • Vulnerability of Speculative Processors to Cache Timing Side-Channel Mechanism

developer.arm.com

  • Q2 2018 Speculative Execution Side Channel Update

INTEL-SA-00115

ちなみに、筆者は例によってセキュリティの専門家ではないし、CPUアーキテクチャにしてもデスクトップクラスの本格的なものは設計経験がないので、いまいち本文から読み取れない部分があったりとか、間違っている部分があるかもしれない。 概念的な部分でしか読み取れないのはいつも悲しいところだ。。。

メモリアクセスのデータフロー

Variant-4 について調査するにあたり、様々な情報を調査してみたのだが、主に参考にしたのは主に以下の資料である。

  • Analysis and mitigation of speculative store bypass (CVE-2018-3639)

Analysis and mitigation of speculative store bypass (CVE-2018-3639) – Security Research & Defense

  • Kernel Side-Channel Attack using Speculative Store Bypass - CVE-2018-3639

Kernel Side-Channel Attack using Speculative Store Bypass - CVE-2018-3639 - Red Hat Customer Portal

(個人的には、Redhatの説明が分かりやすかった)

ちなみに、これらのメモリアクセスのデータフローについては、Mike Johnsonの名著"Superscalar Microprocessor Design" が非常に詳細に説明している。 私もこの本を読み直しながら情報を読み解いていった。

www.amazon.com

ロード・ストアの入り混じった命令シーケンスでの実行順序

ここからは、Mike Johnson本の例を参考にしながら読み進めていく。 例えば、以下のようなロードストアの命令シーケンスが並んでいたとしよう。

STORE v  (1)
ADD      (2)
LOAD  w  (3)
LOAD  x  (4)
LOAD  v  (5)
ADD      (6)
STORE w  (7)

投機実行を行うプロセッサでは、後ろの命令が前の命令を追い越すことが可能であり、メモリアクセスについても基本的にそれは同じであるのだが、ストア命令については、「ストア命令よりも前の命令がすべて完了しないと実際のメモリアクセスを行ってはいけない」という原則がある。 なぜならば、メモリストアを投機的に実行してしまうと、ストア命令よりも前で例外が発生した場合にストア命令を破棄することができないからだ(いったんメモリに書いたものを取り消すのは非常に難しい)。

それからもう一つ、ロード命令とストア命令は基本的に順序の入れ替えは行われない。 なぜならば、最初にストアしたデータを次のロード命令で再びレジスタに持ってくる、というケースがあるからだ。もしも、「ストア[A]→ロード[A]」という順番を「ロード[A]→ストア[A]」と入れ替えてしまうと、誤った動作となってしまう。

1. バイパスをしないロードストアのシーケンス

この前提で、再び上記の命令列を考えてみる。 まず、何も考えずに、上記の命令列をそのままの順番で実行してみる。 デコーダは1サイクルで4命令をデコードすることができ、

  • サイクル1 = (1) - (4) の命令のデコード
  • サイクル2 = (5) - (8) の命令をデコード

とする。以下のような順番でメモリアクセスが実行される。図が長くて分かりにくいが、要するにすべて順番に実行される。

f:id:msyksphinz:20180523020740p:plain:w400
f:id:msyksphinz:20180523020805p:plain:w400
f:id:msyksphinz:20180523020816p:plain:w400

2. バイパスをした場合のロードストアシーケンス

次に、ストア命令よりも先にロード命令を先に発行することで投機的実行を行う方式を考えてみる。ロード命令がストア命令を追い抜かすためには、当然ロード用のリザベーションステーションとストア用のリザベーションステーションが別々に必要になる。

そして、ストアバッファを用意して、ストア命令を追い越すロード命令の発行アドレスを常にチェックしている。もしもストアバッファ内のアクセスアドレスが、ロード命令のアクセスアドレスと被っているならば、先にストア命令を発行しなければ整合性を取ることができない。この例では全体のサイクル数では大きな差はみられないが、それでもロード命令を先に発行することでレジスタにデータを取得するレイテンシが短くなり、速度向上に起因することができる。

f:id:msyksphinz:20180523020836p:plain:w400
f:id:msyksphinz:20180523020847p:plain:w400
f:id:msyksphinz:20180523020858p:plain:w400

3. 一般的な、Load命令の投機実行による弊害

これは今回の問題に限ったことではなく、Load命令を投機実行することによる弊害というものは一般に知られている。例えば、デバイス制御用のメモリマップドレジスタのようなもので、一度Loadアクセスを行うことでデバイスを制御するような装置では、投機的にマップされた当該レジスタにアクセスされると勝手にデバイスが動いてまうという弊害がある。このような場合には、一度メモリアクセスのシーケンスをSynchronizeするか、Pipelineを一度Flushした後に当該Load命令を発行させるなどの工夫を行う必要がある。

Speculative Store Bypass は、ロードストアの順番を入れ替える高速化技術を使った技法

実際、Speculative Store Bypass ではこのロードストアの入れ替えにより機密データを読み取るテクニックであり、Spectre が分岐予測による投機実行での機密データ読み取りであるのに対し、こちらはロードストアの入れ替えのテクニックを活用している。これによりキャッシュに投機的ロードした結果が副作用として残ってしまい、これを検査することで機密データのサイドチャネル攻撃が可能になってしまう。

RedHatの技術解説にも、以下のような記述がある。

The Memory Disambiguator(MD) predicts which of the loads do not depend on an earlier store instruction. Such load(read) instructions are then speculatively executed to load data from L1 data cache, even when the address of the earlier store is not known, thus bypassing the Store instruction. This adds to the overall performance by avoiding load latency. At the end, if the prediction was wrong and conflict between load(read) and store(write) instructions is detected, all instructions since (and including) the speculative load are re-executed.