次の方法で共有


予測実行サイド チャネルの C++ 開発者向けガイダンス

この記事には、C++ ソフトウェアの予測実行サイド チャネル ハードウェアの脆弱性を特定し、軽減するための開発者向けのガイダンスが含まれています。 これらの脆弱性によって、信頼境界を越えて機密情報が開示される可能性があります。また、命令の予測された順序どおりの実行をサポートするプロセッサで実行されるソフトウェアに影響を与える可能性があります。 この脆弱性のクラスは、2018 年 1 月に最初に説明されており、Microsoft のセキュリティ アドバイザリでその他の背景とガイダンスについて参照できます。

この記事で説明するガイダンスは、次によって表される脆弱性のクラスに関連しています。

  1. CVE-2017-5753。Spectre バリアント 1 とも呼ばれます。 このハードウェア脆弱性クラスは、条件分岐の誤った予測の結果として発生する予測実行によって発生する可能性がある、サイド チャネルに関連しています。 Visual Studio 2017 (バージョン 15.5.5 以降) の Microsoft C++ コンパイラには /Qspectre スイッチのサポートが含まれており、これによって、CVE-2017-5753 に関連する潜在的な脆弱なコーディング パターンの限られたセットに対するコンパイル時の軽減策が提供されます。 /Qspectre スイッチは Visual Studio 2015 Update 3 から KB 4338871 でも使用できます。 /Qspectre フラグのドキュメントでは、その効果と使用方法について詳しく説明しています。

  2. CVE-2018-3639。予測ストア バイパス (SSB) とも呼ばれます。 このハードウェア脆弱性クラスは、メモリアクセスの誤った予測の結果として依存ストアの事前の負荷の予測実行によって発生する可能性がある、サイド チャネルに関連しています。

予測実行サイド チャネルの脆弱性に対するアクセス可能な概要については、これらの問題を検出した調査チームのいずれかによって Spectre と Meltdown が発生した場合についてのプレゼンテーションに記載されています。

予測実行サイド チャネルのハードウェアの脆弱性とは何ですか。

最新の CPU は、命令の投機的な実行と順序を指定しない実行を使用することにより、より高いパフォーマンスを提供します。 たとえば、多くの場合、これは分岐のターゲット (条件と間接) を予測することによって達成されます。これにより、CPU が推測された分岐ターゲットで命令の実行を開始し、実際の分岐ターゲットが解決されるまで、失速を回避できます。 CPU が後で誤った予測が発生したことを検出した場合、計算され推測されたコンピューターの状態はすべて破棄されます。 これにより、誤って推測された投機の構造的に見える影響はありません。

予測実行は構造的に見える状態には影響しませんが、CPU によって使用されるさまざまなキャッシュなど、非アーキテクチャの状態で残留トレースを残すことができます。 これは、予測実行の残りのトレースであり、これにより、サイド チャネルの脆弱性が上昇します。 これについて理解を深めるために、次のコードについて考えてみます。これは、CVE-2017-5753 (境界チェックのバイパス) の例を示しています。

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

この例では、ReadByte にバッファー、バッファー サイズ、およびインデックスが指定されています。 untrusted_index によって指定された index パラメーターは、管理以外のプロセスなど、より低い特権のコンテキストによって提供されます。 untrusted_indexbuffer_size よりも小さい場合 は、そのインデックス位置にある文字が buffer から読み取られ、shared_buffer によって参照されるメモリの共有領域へのインデックス作成に使用されます。

アーキテクチャの観点からは、このコード シーケンスは、untrusted_index が常に buffer_size よりも小さくなることが保証されているため、完全に安全です。 ただし、予測実行の存在では、untrusted_indexbuffer_size以上の場合でも、CPU が条件分岐の予測を誤り、if ステートメントの本体を実行する可能性があります。 その結果、CPU は、buffer の境界を超えて (シークレットである可能性がある) バイトを読み取ることができなくなり、そのバイト値を使用して、shared_buffer を通じて後続の負荷のアドレスを計算する可能性があります。

CPU はこの予測の誤りを最終的に検出しますが、CPU キャッシュに残留副作用が残っている可能性があります。これは、buffer から読み取られたバイト値に関する情報を公開します。 これらの副作用は、shared_buffer の各キャッシュ行がアクセスされる速度を調べることによって、システムで実行されている特権の低いコンテキストによって検出できます。 これを実現するために実行できる手順は次のとおりです。

  1. ReadByte を、untrusted_indexbuffer_size よりも小さい値で複数回呼び出します。 攻撃コンテキストによって、対象となるコンテキストが ReadByte (RPC 経由などで) を呼び出すことがあります。これにより、分岐予測は、untrusted_indexbuffer_size 未満であると見なさないようにトレーニングされます。

  2. shared_buffer 内のすべてのキャッシュ行をフラッシュします。 攻撃コンテキストは、shared_buffer によって参照されるメモリの共有領域にあるすべてのキャッシュ行をフラッシュする必要があります。 メモリ領域は共有されているため、これは簡単であり、_mm_clflush などの組み込みを使用して実現できます。

  3. buffer_size を超える untrusted_indexReadByte を呼び出します。 攻撃コンテキストによって、対象となるコンテキストが ReadByte を呼び出し、分岐が取得されないことが誤って予測されます。 これにより、プロセッサは buffer_size を超えるuntrusted_index を持つ if ブロックの本体を実行するため、buffer の範囲外の読み取りが発生します。 その結果、shared_buffer に、範囲外で読み取られた可能性のあるシークレット値を使用してインデックスが作成されるため、それぞれのキャッシュ行が CPU によって読み込まれます。

  4. shared_buffer の各キャッシュ行を読み取って、最も高速にアクセスできることを確認します。 攻撃コンテキストでは、shared_buffer の各キャッシュ行を読み取り、他よりも大幅に高速に読み込まれるキャッシュ行を検出できます。 これは、手順 3 で発生する可能性のあるキャッシュ行です。 この例では、バイト値とキャッシュ行の間に 1:1 のリレーションシップがあるため、攻撃者は、読み取った範囲外のバイトの実際の値を推測できます。

上記の手順では、CVE-2017-5753 のインスタンスを使用する場合と共に、FLUSH + RELOAD という手法を使用する例を示します。

影響を受けるソフトウェアのシナリオ

セキュリティ開発ライフサイクル (SDL) のようなプロセスを使用してセキュリティで保護されたソフトウェアを開発するには、通常、開発者がアプリケーションに存在する信頼境界を特定する必要があります。 信頼境界は、システム上の別のプロセスや、カーネルモードのデバイスドライバーの場合は管理者以外のユーザー モード プロセスなど、信頼性の低いコンテキストによって提供されるデータをアプリケーションが操作する場所に存在します。 予測実行サイド チャネルを含む脆弱性の新しいクラスは、デバイス上のコードとデータを分離する既存のソフトウェア セキュリティ モデルの信頼境界の多くに関連しています。

次の表は、開発者がこれらの脆弱性の発生を懸念する必要があるソフトウェア セキュリティ モデルの概要を示しています。

信頼境界 説明
仮想マシン境界 別の仮想マシンから信頼されていないデータを受信する別の仮想マシン内のワークロードを分離するアプリケーションは、危険にさらされる可能性があります。
カーネル境界 管理者以外のユーザー モード プロセスから信頼されていないデータを受信するカーネルモード デバイス ドライバーは、危険にさらされる可能性があります。
プロセス境界 リモート プロシージャ コール (RPC)、共有メモリ、またはその他のプロセス間通信 (IPC) メカニズムなどを使用して、ローカル システムで実行されている別のプロセスから信頼されていないデータを受け取るアプリケーションは、危険にさらされる可能性があります。
エンクレーブ境界 エンクレーブの外部から信頼されていないデータを受信するセキュア エンクレーブ (Intel SGX など) 内で実行されるアプリケーションは、危険にさらされる可能性があります。
言語の境界 Just-In-Time (JIT) を解釈する、またはコンパイルするアプリケーションは、より高いレベルの言語で記述された信頼できないコードをコンパイルして実行するリスクがある可能性があります。

上記の信頼境界のいずれかに公開されている攻撃対象のアプリケーションでは、攻撃対象領域のコードを確認して、予測実行サイド チャネルの脆弱性の可能性のあるインスタンスを特定し、軽減する必要があります。 リモート ネットワーク プロトコルなどのリモート攻撃サーフェイスにさらされる信頼境界は、予測実行サイド チャネルの脆弱性に対するリスクがあることを示すものではないことに注意してください。

潜在的に脆弱なコーディング パターン

予測実行サイド チャネルの脆弱性は、複数のコーディング パターンの結果として発生する可能性があります。 このセクションでは、脆弱な可能性のあるコーディング パターンについて説明し、それぞれの例を示します。ただし、これらのテーマのバリエーションが存在する可能性があることを認識する必要があります。 このため、開発者はこれらのパターンを例として使用することをお勧めします。これは、潜在的に脆弱なコーディング パターンを網羅した一覧ではありません。 現在のソフトウェアに存在できるのと同じクラスのメモリの安全性の脆弱性は、バッファー オーバーラン、範囲外の配列のアクセス、初期化されていないメモリの使用、型の混乱などに限定されない、実行の予測および順序のないパスにも存在する可能性があります。 アーキテクチャ パスに従って、攻撃者がメモリの安全性の脆弱性を悪用するために使用できるものと同じプリミティブが、予測パスにも適用される場合があります。

一般に、条件付き分岐の誤った予測に関連する予測実行サイド チャネルは、条件式が、信頼度の低いコンテキストで制御または影響を受ける可能性のあるデータに対して動作する場合に発生する可能性があります。 たとえば、ifforwhileswitch または三項ステートメントで使用される条件式を含めることができます。 これらの各ステートメントについて、コンパイラは、CPU が実行時に分岐ターゲットを予測できる条件分岐を生成する場合があります。

各例では、開発者が軽減策としてバリアを導入できる場所に "SPECULATION BARRIER" という語句のコメントが挿入されます。 詳細については、「軽減策」を参照してください。

予測外の負荷

このようなコーディング パターンのカテゴリには、条件分岐の誤った予測があります。これは、予測外のメモリアクセスにつながります。

配列の読み込み中に負荷が出てきた配列

このコーディング パターンは、CVE-2017-5753 (境界チェックのバイパス) で最初に記述された脆弱なコーディング パターンです。 この記事の背景セクションでは、このパターンについて詳しく説明します。

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        // SPECULATION BARRIER
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

同様に、配列の範囲外の読み込みは、誤った予測が原因で終了条件を超えるループと組み合わせて発生する場合があります。 この例では、x < buffer_size 式に関連付けられている条件分岐は、xbuffer_size 以上の場合にループの for 本体を誤って予測し、投機的に実行することがあります。その結果、予測外の負荷が発生します。

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadBytes(unsigned char *buffer, unsigned int buffer_size) {
    for (unsigned int x = 0; x < buffer_size; x++) {
        // SPECULATION BARRIER
        unsigned char value = buffer[x];
        return shared_buffer[value * 4096];
    }
}

間接分岐に対する配列の範囲外の読み込み

このコーディング パターンでは、条件分岐の誤った予測が関数ポインターの配列に対して範囲外のアクセスにつながる可能性があります。この場合、範囲外で読み取られたターゲット アドレスへの間接分岐が発生します。 次のスニペットは、この例を示しています。

この例では、untrusted_message_id パラメーターを使用して DispatchMessage に信頼されていないメッセージ識別子を指定しています。 untrusted_message_idMAX_MESSAGE_ID 未満である場合は、関数ポインターの配列にインデックスを作成し、対応する分岐ターゲットに分岐するために使用されます。 このコードは安全なアーキテクチャですが、CPU が条件分岐を誤って予測した場合、値が MAX_MESSAGE_ID 以上の場合に untrusted_message_id によって DispatchTable にインデックスが作成され、その結果、範囲外のアクセスが発生する可能性があります。 これにより、投機的に実行されるコードに応じて情報が漏えいする可能性のある、配列の境界を超えて派生した分岐ターゲット アドレスからの予測実行が発生する可能性があります。

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    if (untrusted_message_id < MAX_MESSAGE_ID) {
        // SPECULATION BARRIER
        DispatchTable[untrusted_message_id](buffer, buffer_size);
    }
}

配列の範囲外の読み込みによって別の負荷が発生した場合と同様に、この状態は、誤った予測が原因で終了条件を超えるループと組み合わせて発生することもあります。

間接分岐を出力する配列の範囲外のストア

前の例では、予測外の負荷が間接分岐ターゲットにどのように影響するかを示していましたが、範囲外のストアでは、関数ポインターや戻り先アドレスなどの間接分岐ターゲットを変更することもできます。 これにより、攻撃者が指定したアドレスから予測される実行が生じる可能性があります。

この例では、信頼されていないインデックスが untrusted_index パラメーターを介して渡されます。 untrusted_indexpointers 配列の要素数 (256 要素) 未満である場合、ptr の指定されたポインター値が pointers 配列に書き込まれます。 このコードは安全なアーキテクチャですが、CPU が条件分岐を誤って予測した場合、スタックに割り当てられた pointers 配列の境界を越えて ptr が投機的に書き込まれる可能性があります。 これにより、WriteSlot の返信先アドレスが投機的に破損する可能性があります。 攻撃者が ptr の値を制御できる場合、投機的なパスに沿って WriteSlot が返されたときに、任意のアドレスから予測される実行が発生する可能性があります。

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
}

同様に、func という名前の関数ポインターのローカル変数がスタックに割り当てられている場合は、条件分岐の誤った予測が発生したときに func を参照するアドレスを投機的に変更する可能性があります。 これにより、関数ポインターが呼び出されたときに、任意のアドレスからの投機的な実行が発生する可能性があります。

unsigned char WriteSlot(unsigned int untrusted_index, void *ptr) {
    void *pointers[256];
    void (*func)() = &callback;
    if (untrusted_index < 256) {
        // SPECULATION BARRIER
        pointers[untrusted_index] = ptr;
    }
    func();
}

これらの両方の例には、スタック割り当て間接分岐ポインターの投機的な変更が含まれていることに注意してください。 また、グローバル変数、ヒープに割り当てられたメモリ、および一部の CPU の読み取り専用メモリに対しても、投機的な変更が発生する可能性があります。 スタックに割り当てられたメモリの場合、Microsoft C++ コンパイラは、スタックに割り当てられた間接分岐ターゲットを推測的に変更するための手順を既に実行しています。たとえば、/GS コンパイラのセキュリティ機能の一部として、バッファーがセキュリティ Cookie に隣接して配置されるようにローカル変数が並べ替えられます。

投機的型の混乱

このカテゴリは、投機的型の混乱を招く可能性があるコーディング パターンを扱います。 これは、投機的実行時に非アーキテクチャ パスに沿って、不適切な型を使用してメモリにアクセスした場合に発生します。 条件分岐の誤った予測と予測ストア バイパスはどちらも、投機的型の混乱を招く可能性があります。

投機的ストア バイパスの場合、コンパイラが複数の型の変数にスタック位置を再利用するシナリオでこのような状況が発生する可能性があります。 これは、型 A の変数のアーキテクチャ ストアがバイパスされ、変数が割り当てられる前に、型 A の読み込みを投機的に実行することを許可する可能性があるためです。 以前に格納された変数の型が異なる場合は、投機的型の混乱の条件が発生する可能性があります。

条件分岐の誤った予測の場合、次のコードスニペットを使用して、投機的型の混乱を招く可能性のあるさまざまな条件を記述します。

enum TypeName {
    Type1,
    Type2
};

class CBaseType {
public:
    CBaseType(TypeName type) : type(type) {}
    TypeName type;
};

class CType1 : public CBaseType {
public:
    CType1() : CBaseType(Type1) {}
    char field1[256];
    unsigned char field2;
};

class CType2 : public CBaseType {
public:
    CType2() : CBaseType(Type2) {}
    void (*dispatch_routine)();
    unsigned char field2;
};

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ProcessType(CBaseType *obj)
{
    if (obj->type == Type1) {
        // SPECULATION BARRIER
        CType1 *obj1 = static_cast<CType1 *>(obj);

        unsigned char value = obj1->field2;

        return shared_buffer[value * 4096];
    }
    else if (obj->type == Type2) {
        // SPECULATION BARRIER
        CType2 *obj2 = static_cast<CType2 *>(obj);

        obj2->dispatch_routine();

        return obj2->field2;
    }
}

投機的型の混乱。範囲外の読み込みにつながる

このコーディング パターンでは、投機的型の混乱によって、読み込まれた値が後続の読み込みアドレスにフィードする、範囲外または型を混同したフィールド アクセスが発生する場合があります。 これは、配列の範囲外のコーディング パターンに似ていますが、上記のように別のコーディング シーケンスを使用して示されます。 この例では、攻撃コンテキストによって、CType1 型のオブジェクト (type フィールドは Type1 に等しい) を使用して、対象のコンテキストが ProcessType を複数回実行する可能性があります。 これにより、最初の if ステートメントの条件分岐をトレーニングして、取得されていないことを予測する効果が得られます。 攻撃コンテキストは、型 CType2 のオブジェクトを使用して、対象のコンテキストに ProcessType を実行させる可能性があります。 これにより、最初の if ステートメントの条件分岐が誤って予測されて if ステートメントの本体が実行され、型 CType2 のオブジェクトが CType1 にキャストされると、投機的型の混乱が生じる可能性があります。 CType2CType1 未満であるため、CType1::field2 へのメモリアクセスは、秘密である可能性があるデータの範囲外の投機的な負荷が発生します。 この値は、前に説明した配列範囲外の例と同様に、shared_buffer から観測可能な副作用を作成する可能性がある読み込みで使用されます。

間接分岐につながる投機的型の混乱

このコーディングパターンでは、投機的型の混乱によって、投機的実行中に安全でない間接分岐が発生する可能性があります。 この例では、攻撃コンテキストによって、CType2 型のオブジェクト (type フィールドは Type2 に等しい) を使用して、対象のコンテキストが ProcessType を複数回実行する可能性があります。 これにより、最初の if ステートメントの取得のための条件分岐をトレーニングし、else if ステートメントを取得しないようにできます。 攻撃コンテキストは、型 CType1 のオブジェクトを使用して、対象のコンテキストに ProcessType を実行させる可能性があります。 これにより、最初の if ステートメントの条件分岐で予測が発生し、else if ステートメントの予測が取得されなかった場合に、else if の本体を実行し、型 CType1 のオブジェクトを CType2 にキャストすると、投機的型の混乱が生じる可能性があります。 CType2::dispatch_routine フィールドが char 配列 CType1::field1 と重複しているため、意図しない分岐ターゲットに対する投機的間接分岐が発生する可能性があります。 攻撃コンテキストが CType1::field1 配列内のバイト値を制御できる場合は、分岐ターゲット アドレスを制御できる可能性があります。

投機的な初期化されていない使用

このようなコーディング パターンのカテゴリには、予測実行が初期化されていないメモリにアクセスし、それを使用して後続の読み込みまたは間接分岐をフィードするシナリオが含まれます。 これらのコーディング パターンを悪用可能にするには、攻撃者が、使用されているコンテキストによって初期化されることなく、使用されるメモリの内容を制御または意味を与える必要があります。

投機的な初期化されていない使用により、範囲外の読み込みが発生する

投機的な初期化されていない使用によって、攻撃者によって制御される値を使用して、範囲外の負荷が生じる可能性があります。 次の例では、index の値がすべてのアーキテクチャ パスの trusted_index に割り当てられており、trusted_indexbuffer_size 以下であると想定されています。 ただし、コンパイラによって生成されるコードによっては、予測ストア バイパスが発生する可能性があります。これにより、buffer[index] からの読み取りが可能になり、index への割り当ての前に、依存式を実行できます。 これが発生する場合、index の初期化されていない値が buffer へのオフセットとして使用されます。これにより、攻撃者は機密情報を範囲外に読み取り、shared_buffer の依存する負荷を通じて、この情報をサイド チャネル経由で伝達できる可能性があります。

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

void InitializeIndex(unsigned int trusted_index, unsigned int *index) {
    *index = trusted_index;
}

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int trusted_index) {
    unsigned int index;

    InitializeIndex(trusted_index, &index); // not inlined

    // SPECULATION BARRIER
    unsigned char value = buffer[index];
    return shared_buffer[value * 4096];
}

間接分岐につながる、投機的な初期化されていない使用

投機的な初期化されていない使用は、分岐ターゲットが攻撃者によって制御される間接分岐につながる可能性があります。 次の例では、routine は、DefaultMessageRoutine1 または DefaultMessageRoutine のいずれかに mode の値に応じて割り当てられます。 アーキテクチャ パスでは、これにより、routine は常に間接分岐の前に初期化されます。 ただし、コンパイラによって生成されるコードによっては、routine に対する割り当ての前に、routine を介する間接分岐から投機的に実行することを許可する予測ストア バイパスが発生する可能性があります。 このような状況が発生した場合、攻撃者が routine の初期化されていない値に影響を与える、または制御する可能性があると仮定して、任意のアドレスから投機的に実行できる可能性があります。

#define MAX_MESSAGE_ID 16

typedef void (*MESSAGE_ROUTINE)(unsigned char *buffer, unsigned int buffer_size);

const MESSAGE_ROUTINE DispatchTable[MAX_MESSAGE_ID];
extern unsigned int mode;

void InitializeRoutine(MESSAGE_ROUTINE *routine) {
    if (mode == 1) {
        *routine = &DefaultMessageRoutine1;
    }
    else {
        *routine = &DefaultMessageRoutine;
    }
}

void DispatchMessage(unsigned int untrusted_message_id, unsigned char *buffer, unsigned int buffer_size) {
    MESSAGE_ROUTINE routine;

    InitializeRoutine(&routine); // not inlined

    // SPECULATION BARRIER
    routine(buffer, buffer_size);
}

対応策オプション

予測実行サイド チャネルの脆弱性は、ソース コードに変更を加えることで軽減できます。 これらの変更には、投機的バリアの追加や、予測実行から機密情報にアクセスできなくなるようにアプリケーションの設計を変更することによって、脆弱性の特定のインスタンスを軽減することが含まれる場合があります。

手動インストルメンテーションを使用した投機的バリア

開発者は、推定バリアを手動で挿入して、非アーキテクチャ パスに従って予測実行が行われないようにすることができます。 たとえば、開発者は、ブロックの先頭 (条件分岐の後) または問題のある最初の読み込みの前に、条件付きブロックの本体の危険なコーディング パターンの前に、投機的バリアを挿入できます。 これにより、条件分岐の誤った予測は、実行をシリアル化することで、非アーキテクチャ パスで危険なコードを実行できなくなります。 次の表に示すように、投機的バリア シーケンスはハードウェア アーキテクチャによって異なります。

Architecture CVE-2017-5753 に固有の投機的バリア CVE-2018-3639 に固有の投機的バリア
x86/x64 _mm_lfence() _mm_lfence()
ARM 現在使用できません __dsb(0)
ARM64 現在使用できません __dsb(0)

たとえば、次のように _mm_lfence 組み込みを使用すると、次のコード パターンを軽減できます。

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        _mm_lfence();
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

コンパイラ時間インストルメンテーションを使用した投機的バリア

Visual Studio 2017 (バージョン 15.5.5 以降) の Microsoft C++ コンパイラには /Qspectre スイッチがサポートされており、これにより、CVE-2017-5753 に関連する潜在的に脆弱なコード パターンの限られたセットに対して、投機的バリアが自動的に挿入されます。 /Qspectre フラグのドキュメントでは、その効果と使用方法について詳しく説明しています。 このフラグは、脆弱な可能性のあるコーディング パターンをすべて網羅しているわけではありません。そのため、このような開発者は、このクラスの脆弱性を包括的に軽減するために、これに依存するべきではありません。

配列インデックスのマスク

予測外の負荷が発生する可能性がある場合は、配列インデックスを明示的にバインドするロジックを追加することで、アーキテクチャ パスと非アーキテクチャ パスの両方で配列インデックスを厳密に制限できます。 たとえば、2 のべき乗に固定されたサイズに配列を割り当てることができる場合、単純なマスクを導入できます。 このことを次のサンプルで示します。この例では、2 のべき乗にアラインされる buffer_size と想定されています。 これにより、untrusted_index が常に buffer_size 未満であることが保証されます。これは、条件分岐の誤った予測が発生し、untrusted_indexbuffer_size 以上の値を使用して渡された場合でも維持されます。

ここで実行されるインデックスマスクは、コンパイラによって生成されるコードによっては、予測ストアのバイパスの対象になる可能性があることに注意してください。

// A pointer to a shared memory region of size 1MB (256 * 4096)
unsigned char *shared_buffer;

unsigned char ReadByte(unsigned char *buffer, unsigned int buffer_size, unsigned int untrusted_index) {
    if (untrusted_index < buffer_size) {
        untrusted_index &= (buffer_size - 1);
        unsigned char value = buffer[untrusted_index];
        return shared_buffer[value * 4096];
    }
}

メモリからの機密情報の削除

予測実行サイド チャネルの脆弱性を軽減するために使用できるもう 1 つの手法は、機密情報をメモリから削除することです。 ソフトウェア開発者は、予測実行中に機密情報にアクセスできないように、アプリケーションをリファクターする機会を探すことができます。 これは、アプリケーションの設計をリファクタリングして、機密情報を個別のプロセスに分離することによって実現できます。 たとえば、Web ブラウザー アプリケーションでは、各 Web オリジンに関連付けられたデータを個別のプロセスに分離して、1 つのプロセスが予測実行によってクロスオリジン データにアクセスできないようにすることができます。

関連項目

予測実行のサイドチャネルの脆弱性を軽減するためのガイダンス
予測実行サイド チャネル ハードウェアの脆弱性の軽減