IOMMU ベースの GPU 分離

このページでは、Windows 10 バージョン 1803 (WDDM 2.4) で導入された IOMMU 対応デバイスの IOMMU ベースの GPU 分離機能について説明します。 IOMMU の最新の更新プログラムについては、 IOMMU DMA の再マップ を参照してください。

概要

IOMMU ベースの GPU 分離により、 Dxgkrnl は IOMMU ハードウェアを使用して GPU からのシステム メモリへのアクセスを制限できます。 OS は、物理アドレスの代わりに論理アドレスを提供できます。 これらの論理アドレスを使用して、デバイスのシステム メモリへのアクセスを、アクセスできるメモリのみに制限できます。 そのためには、IOMMU が PCIe 経由のメモリ アクセスを有効でアクセス可能な物理ページに変換します。

デバイスによってアクセスされる論理アドレスが有効でない場合、デバイスは物理メモリにアクセスできません。 この制限により、攻撃者が侵害されたハードウェア デバイスを介して物理メモリにアクセスし、デバイスの操作に必要のないシステム メモリの内容を読み取ることを可能にするさまざまな悪用が防止されます。

Windows 10 バージョン 1803 以降では、既定では、この機能は、Windows Defender Application Guard が Microsoft Edge (つまりコンテナー仮想化) に対して有効になっている PC でのみ有効になっています。

開発の目的で、実際の IOMMU 再マップ機能は、次のレジストリ キーを使用して有効または無効になります。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\GraphicsDrivers
DWORD: IOMMUFlags

0x01 Enabled
     * Enables creation of domain and interaction with HAL

0x02 EnableMappings
     * Maps all physical memory to the domain
     * EnabledMappings is only valid if Enabled is also set. Otherwise no action is performed

0x04 EnableAttach
     * Attaches the domain to the device(s)
     * EnableAttach is only valid if EnableMappings is also set. Otherwise no action is performed

0x08 BypassDriverCap
     * Allows IOMMU functionality regardless of support in driver caps. If the driver does not indicate support for the IOMMU and this bit is not set, the enabled bits are ignored.

0x10 AllowFailure
     * Ignore failures in IOMMU enablement and allow adapter creation to succeed anyway.
     * This value cannot override the behavior when created a secure VM, and only applies to forced IOMMU enablement at device startup time using this registry key.

この機能を有効にすると、アダプターの起動直後に IOMMU が有効になります。 この時刻より前に行われたすべてのドライバー割り当ては、有効になるとマップされます。

さらに、ベロシティ ステージング キー 14688597 が 有効 に設定されている場合、セキュリティで保護された仮想マシンの作成時に IOMMU がアクティブになります。 現時点では、このステージング キーは既定で無効になっており、適切な IOMMU サポートなしでセルフホスティングが許可されます。

有効になっている間、ドライバーが IOMMU サポートを提供していない場合、セキュリティで保護された仮想マシンの起動は失敗します。

現在、IOMMU を有効にした後で無効にする方法はありません。

メモリ アクセス

Dxgkrnl は、このメモリに確実にアクセスできるように、GPU からアクセス可能なすべてのメモリが IOMMU を介して再マップされるようにします。 GPU がアクセスする必要がある物理メモリは、現在、次の 4 つのカテゴリに分類できます。

  • MmAllocateContiguousMemory- または MmAllocatePagesForMdl スタイルの関数 (SpecifyCache や拡張バリエーションを含む) を通じて行われるドライバー固有の割り当ては、GPU がアクセスする前に IOMMU にマップする必要があります。 Mm API を呼び出す 代わりに、 Dxgkrnl はカーネル モード ドライバーにコールバックを提供して、1 つの手順で割り当てと再マップを許可します。 GPU アクセス可能なメモリは、これらのコールバックを経由する必要があります。または、GPU はこのメモリにアクセスできません。

  • ページング操作中に GPU によってアクセスされるすべてのメモリ、または GpuMmu 経由でマップされるすべてのメモリを IOMMU にマップする必要があります。 このプロセスは、 Dxgkrnlのサブコンポーネントである Video Memory Manager (VidMm) の内部に完全に含まれます。 VidMm は、GPU がこのメモリにアクセスすることが予想される場合は、次のような場合に、マッピングを処理し、論理アドレス空間のマッピングを解除します。

  • VRAM との間の転送中、またはシステム メモリまたはアパーチャ セグメントにマップされている時間全体の割り当てのバッキング ストアをマッピングします。

  • 監視対象のフェンスのマッピングとマッピング解除。

  • 電源切り替え中に、ドライバーはハードウェアで予約されたメモリの一部を保存する必要がある場合があります。 この状況を処理するために、 Dxgkrnl は、このデータを格納するメモリの量を指定するドライバーのメカニズムを提供します。 ドライバーに必要なメモリの正確な量は動的に変更できますが、 Dxgkrnl は、必要に応じて物理ページを確実に取得できるように、アダプターが初期化されるときに上限に対してコミット 料金を受け取ります。 Dxgkrnl は、このメモリがロックされ、電源切り替え中に転送のために IOMMU にマップされていることを確認する役割を担います。

  • ハードウェア予約済みリソースの場合、VidMm は、デバイスが IOMMU に接続されている時点までに IOMMU リソースを正しくマップするようにします。 これには、 PopulatedFromSystemMemoryで報告されたメモリ セグメントによって報告されるメモリが含まれます。 VidMm セグメントを介して公開されていない予約済みメモリ (たとえば、ファームウェア/BIOD 予約済み) の場合、 DxgkrnlDXGKDDI_QUERYADAPTERINFO 呼び出しを行い、ドライバーが事前にマップする必要がある予約済みメモリ範囲をすべて照会します。 詳細については、 ハードウェアの予約済みメモリ に関するページを参照してください。

ドメインの割り当て

ハードウェアの初期化中に、 Dxgkrnl はシステム上の論理アダプターごとにドメインを作成します。 ドメイン は、論理アドレス空間を管理し、マッピングに必要なページ テーブルやその他のデータを追跡します。 1 つの論理アダプター内のすべての物理アダプターは、同じドメインに属します。 Dxgkrnl は、新しい割り当てコールバック ルーチンと、VidMm 自体によって割り当てられたすべてのメモリを介して、マップされたすべての物理メモリを追跡します。

ドメインは、セキュリティで保護された仮想マシンが初めて作成されるときにデバイスに接続されます。または、上記のレジストリ キーが使用されている場合は、デバイスが起動した直後に接続されます。

排他アクセス

IOMMU ドメインのアタッチおよびデタッチは非常に高速ですが、現在はアトミックではありません。 これは、PCIe 経由で発行されたトランザクションが、異なるマッピングを持つ IOMMU ドメインにスワップされる間に正しく変換されることが保証されていないことを意味します。

この状況を処理するには、Windows 10 バージョン 1803 (WDDM 2.4) 以降、KMD は Dxgkrnl が呼び出す次の DDI ペアを実装する必要があります。

これらの DDI は開始/終了ペアリングを形成し、 Dxgkrnl はハードウェアがバス経由でサイレントであることを要求します。 ドライバーは、デバイスが新しい IOMMU ドメインにスイッチされるたびに、そのハードウェアがサイレントであることを確認する必要があります。 つまり、ドライバーは、これら 2 つの呼び出しの間にデバイスからシステム メモリの読み取りまたは書き込みが行われないことを確認する必要があります。

これら 2 つの呼び出しの間に、 Dxgkrnl は次のことを保証します。

  • スケジューラが中断されていること。 すべてのアクティブなワークロードがフラッシュされ、新しいワークロードはハードウェアに送信されたり、ハードウェアでスケジュールされたりしないこと。
  • 他の DDI 呼び出しが行われないこと。

これらの呼び出しの一環として、ドライバーは、OS からの明示的な通知がなくても、排他アクセス期間に割り込みを無効にして抑制 (Vsync 割り込みを含む) を選択できます。

Dxgkrnl は、ハードウェアでスケジュールされている保留中の作業が完了したことを確認し、この排他アクセスリージョンに入ります。 この間、 Dxgkrnl はドメインをデバイスに割り当てます。 Dxgkrnl は、これらの呼び出しの間にドライバーまたはハードウェアの要求を行いません。

DDI の変更

IOMMU ベースの GPU 分離をサポートするために、次の DDI 変更が行われました。

メモリの割り当てと IOMMU へのマッピング

Dxgkrnl は、メモリを割り当てて IOMMU の論理アドレス空間に再マップできるように、上記の表の最初の 6 つのコールバックをカーネル モード ドライバーに提供します。 これらのコールバック関数は、 Mm API インターフェイスによって提供されるルーチンを模倣します。 ドライバーには、MDL、または IOMMU にもマップされるメモリを記述するポインターが提供されます。 これらの MDL は引き続き物理ページを記述しますが、IOMMU の論理アドレス空間は同じアドレスにマップされます。

Dxgkrnl は、ドライバーによるリークがないことを確認するために、これらのコールバックへの要求を追跡します。 割り当てコールバックは、出力の一部として追加のハンドルを提供し、それぞれの空きコールバックに戻す必要があります。

指定された割り当てコールバックのいずれかを使用して割り当てることができないメモリの場合、ドライバーで管理される MDL を追跡して IOMMU と共に使用できるように、 DXGKCB_MAPMDLTOIOMMU コールバックが提供されます。 このコールバックを使用するドライバーは、MDL の有効期間が対応するマップ解除呼び出しを超えないようにします。 それ以外の場合、マップ解除呼び出しには未定義の動作があり、マップ解除までに Mm によって再利用される MDL からのページのセキュリティが損なわれる可能性があります。

VidMm は、システム メモリ内で作成された割り当て (DdiCreateAllocationCb、監視対象のフェンスなど) を自動的に管理します。 ドライバーは、これらの割り当てを機能させるために何もする必要はありません。

フレーム バッファー予約

電源切り替え中にフレーム バッファーの予約部分をシステム メモリに保存する必要があるドライバーの場合、 Dxgkrnl はアダプターの初期化時に必要なメモリに対してコミット 料金を受け取ります。 ドライバーが IOMMU 分離のサポートを報告した場合Dxgkrnl は、物理アダプターの大文字を照会した直後に、次の DXGKDDI_QUERYADAPTERINFO の呼び出しを発行します。

  • タイプDXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • 入力は、物理アダプターインデックスである UINT 型です。
  • 出力は DXGK_FRAMEBUFFERSAVEAREA のタイプであり、電源切り替え中にフレーム バッファー予約領域を保存するためにドライバーが必要とする最大サイズである必要があります。

Dxgkrnl は、要求時に常に物理ページを取得できるように、ドライバーによって指定された量のコミット料金を受け取ります。 このアクションは、最大サイズに 0 以外の値を指定する物理アダプターごとに一意のセクション オブジェクトを作成することによって行われます。

ドライバーによって報告される最大サイズは、PAGE_SIZEの倍数である必要があります。

フレーム バッファーとの間の転送は、ドライバーが選択した時点で実行できます。 転送を支援するために、 Dxgkrnl は上記の表の最後の 4 つのコールバックをカーネル モード ドライバーに提供します。 これらのコールバックを使用して、アダプターの初期化時に作成されたセクション オブジェクトの適切な部分をマップできます。

ドライバーは、これらの 4 つのコールバック関数を呼び出すときに、LDA チェーン内のマスター/リード デバイスの hAdapter を常に提供する必要があります。

ドライバーには、フレーム バッファー予約を実装する 2 つのオプションがあります。

  1. (推奨される方法)ドライバーは、上記の DXGKDDI_QUERYADAPTERINFO 呼び出しを使用して物理アダプターごとに領域を割り当てて、アダプターごとに必要な記憶域の量を指定する必要があります。 電源切り替え時に、ドライバーは一度に 1 つの物理アダプターのメモリを保存または復元する必要があります。 このメモリは、物理アダプターごとに 1 つずつ、複数のセクション オブジェクトに分割されます。

  2. 必要に応じて、ドライバーは、1 つの共有セクション オブジェクトにすべてのデータを保存または復元できます。 このアクションは、物理アダプター 0 の DXGKDDI_QUERYADAPTERINFO 呼び出しで 1 つの大きな最大サイズを指定し、その他のすべての物理アダプターに 0 の値を指定することで実行できます。 その後、ドライバーは、すべての物理アダプターのすべての保存/復元操作で使用するために、セクション オブジェクト全体を 1 回固定できます。 このメソッドには、メモリのサブ範囲のみを MDL にピン留めすることはサポートされていないため、一度に大量のメモリをロックする必要がある主な欠点があります。 その結果、この操作はメモリ不足で失敗する可能性が高くなります。 ドライバーは、正しいページ オフセットを使用して、MDL 内のページを GPU にマップすることも想定されます。

ドライバーは、フレーム バッファー間の転送を完了するには、次のタスクを実行する必要があります。

  • 初期化中、ドライバーは、割り当てコールバック ルーチンのいずれかを使用して、GPU アクセス可能メモリの小さなチャンクを事前に割り当てる必要があります。 このメモリは、セクション オブジェクト全体を一度にマップまたはロックダウンできない場合に進行を進めるために使用されます。

  • 電源切り替え時に、ドライバーはまず Dxgkrnl を呼び出してフレーム バッファーをピン留めする必要があります。 成功すると、 Dxgkrnl は、IOMMU にマップされているロックされたページに MDL をドライバーに提供します。 ドライバーは、ハードウェアの最も効率的な手段でこれらのページに直接転送を実行できます。 その後、ドライバーは Dxgkrnl を呼び出してメモリのロックを解除またはマップ解除する必要があります。

  • フレーム バッファー全体を Dxgkrnl が一度にピン留めできない場合、ドライバーは初期化中に割り当てられた事前割り当て済みバッファーを使用して、前方進行を試みる必要があります。 この場合、ドライバーは小さなチャンクで転送を実行します。 転送の各イテレーション中に (チャンクごとに)、ドライバーは Dxgkrnl に要求して、結果をコピーできるセクション オブジェクトのマップされた範囲を指定する必要があります。 ドライバーは、次のイテレーションの前にセクション オブジェクトの部分のマップを解除する必要があります。

次の擬似コードは、このアルゴリズムの実装例です。


#define SMALL_SIZE (PAGE_SIZE)

PMDL PHYSICAL_ADAPTER::m_SmallMdl;
PMDL PHYSICAL_ADAPTER::m_PinnedMdl;

NTSTATUS PHYSICAL_ADAPTER::Init()
{
    DXGKARGCB_ALLOCATEPAGESFORMDL Args = {};
    Args.TotalBytes = SMALL_SIZE;
    
    // Allocate small buffer up front for forward progress transfers
    Status = DxgkCbAllocatePagesForMdl(SMALL_SIZE, &Args);
    m_SmallMdl = Args.pMdl;

    ...
}

NTSTATUS PHYSICAL_ADAPTER::OnPowerDown()
{    
    Status = DxgkCbPinFrameBufferForSave(&m_pPinnedMdl);
    if(!NT_SUCCESS(Status))
    {
        m_pPinnedMdl = NULL;
    }
    
    if(m_pPinnedMdl != NULL)
    {        
        // Normal GPU copy: frame buffer -> m_pPinnedMdl
        GpuCopyFromFrameBuffer(m_pPinnedMdl, Size);
        DxgkCbUnpinFrameBufferForSave(m_pPinnedMdl);
    }
    else
    {
        SIZE_T Offset = 0;
        while(Offset != TotalSize)
        {
            SIZE_T MappedOffset = Offset;
            PVOID pCpuPointer;
            Status = DxgkCbMapFrameBufferPointer(SMALL_SIZE, &MappedOffset, &pCpuPointer);
            if(!NT_SUCCESS(Status))
            {
                // Driver must handle failure here. Even a 4KB mapping may
                // not succeed. The driver should attempt to cancel the
                // transfer and reset the adapter.
            }
            
            GpuCopyFromFrameBuffer(m_pSmallMdl, SMALL_SIZE);
            
            RtlCopyMemory(pCpuPointer + MappedOffset, m_pSmallCpuPointer, SMALL_SIZE);
            
            DxgkCbUnmapFrameBufferPointer(pCpuPointer);
            Offset += SMALL_SIZE;
        }
    }
}

NTSTATUS PHYSICAL_ADAPTER::OnPowerUp()
{
    Status = DxgkCbPinFrameBufferForSave(&m_pPinnedMdl);
    if(!NT_SUCCESS(Status))
    {
        m_pPinnedMdl = NULL;
    }
    
    if(pPinnedMemory != NULL)
    {
        // Normal GPU copy: m_pPinnedMdl -> frame buffer
        GpuCopyToFrameBuffer(m_pPinnedMdl, Size);
        DxgkCbUnpinFrameBufferForSave(m_pPinnedMdl);
    }
    else
    {
        SIZE_T Offset = 0;
        while(Offset != TotalSize)
        {
            SIZE_T MappedOffset = Offset;
            PVOID pCpuPointer;
            Status = DxgkCbMapFrameBufferPointer(SMALL_SIZE, &MappedOffset, &pCpuPointer);
            if(!NT_SUCCESS(Status))
            {
                // Driver must handle failure here. Even a 4KB mapping may
                // not succeed. The driver should attempt to cancel the
                // transfer and reset the adapter.
            }
                        
            RtlCopyMemory(m_pSmallCpuPointer, pCpuPointer + MappedOffset, SMALL_SIZE);
            
            GpuCopyToFrameBuffer(m_pSmallMdl, SMALL_SIZE);

            DxgkCbUnmapFrameBufferPointer(pCpuPointer);
            Offset += SMALL_SIZE;
        }
    }
}

ハードウェア予約メモリ

VidMm は、デバイスが IOMMU に接続される前に、ハードウェア予約メモリをマップします。

VidMm は、セグメントとして報告されたメモリを自動的に処理し、 PopulatedFromSystemMemory フラグを設定します。 VidMm は、指定された物理アドレスに基づいてこのメモリをマップします。

セグメントによって公開されていないプライベート ハードウェア予約リージョンの場合、VidMm はドライバーによって範囲を照会する DXGKDDI_QUERYADAPTERINFO 呼び出しを行います。 指定された範囲は、NTOS メモリ マネージャーによって使用されるメモリ領域と重複してはなりません。VidMm は、このような交差が発生しなかったことを検証します。 この検証により、ドライバーが予約範囲外の物理メモリの領域を誤って報告することができなくなります。これは、機能のセキュリティ保証に違反します。

クエリ呼び出しは、必要な範囲の数を照会するために 1 回行われ、その後に 2 回目の呼び出しが行われ、予約された範囲の配列が設定されます。

テスト

ドライバーがこの機能にオプトインした場合、HLK テストはドライバーのインポート テーブルをスキャンして、次の Mm 関数が呼び出されていないことを確認します。

  • MmAllocateContiguousMemory
  • MmAllocateContiguousMemorySpecifyCache
  • MmFreeContiguousMemory
  • MmAllocatePagesForMdl
  • MmAllocatePagesForMdlEx
  • MmFreePagesFromMdl
  • MmProbeAndLockPages

連続するメモリと MDL のすべてのメモリ割り当ては、代わりに、一覧に示されている関数を使用して Dxgkrnlのコールバック インターフェイスを経由する必要があります。 また、ドライバーはメモリをロックしないでください。 Dxgkrnl は、ドライバーのロックされたページを管理します。 メモリが再マップされると、ドライバーに提供されるページの論理アドレスが物理アドレスと一致しなくなる可能性があります。