共用方式為


IOMMU 型 GPU 隔離

IOMMU 型 GPU 隔離是一種技術,可藉由管理 GPU 存取系統記憶體的方式,來增強系統安全性和穩定性。 本文說明適用於 IOMMU 裝置的 WDDM IOMMU 型 GPU 隔離功能,以及開發人員如何在圖形驅動程式中實作此功能。

此功能可從 Windows 10 1803 版 (WDDM 2.4) 開始提供。 如需最新的 IOMMU 更新,請參閱 IOMMU DMA 重新對應

概觀

IOMMU 型 GPU 隔離可讓 Dxgkrnl 利用 IOMMU 硬體限制從 GPU 存取系統記憶體。 OS 可以提供邏輯位址,而不是實體位址。 這些邏輯位址可用來將裝置對系統記憶體的存取限制為只能存取的記憶體。 其方式是確保 IOMMU 會將透過 PCIe 的記憶體取轉譯為有效且可存取的實體頁面。

如果裝置存取的邏輯位址無效,裝置就無法存取物理記憶體。 這項限制可防止一系列惡意探索,讓攻擊者透過遭入侵的硬體裝置存取物理記憶體。 如果沒有它,攻擊者就可以讀取裝置作業不需要的系統記憶體內容。

根據預設,此功能只會針對已啟用 Windows Defender 應用程式防護 的電腦啟用 Microsoft Edge(也就是容器虛擬化)。

為了開發目的,會透過下列登錄機碼啟用或停用實際的 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 需要存取的實體記憶體目前可細分為四個類別:

  • 透過 MmAllocateContiguousMemory- 或 MmAllocatePagesForMdl-style 函式(包括 SpecifyCache 和擴充變化)所進行的驅動程式特定配置,必須先對應至 IOMMU,才能存取它們。 Dxgkrnl 不會呼叫 Mm API,而是提供內核模式驅動程式的回呼,以允許在一個步驟中配置和重新對應。 任何要可供 GPU 存取的記憶體都必須經過這些回呼,否則 GPU 無法存取此記憶體。

  • 在分頁作業期間由 GPU 存取的所有記憶體,或透過 GpuMmu 對應的所有記憶體都必須對應至 IOMMU。 此程式完全位於 Video Memory Manager (VidMm), 這是 Dxgkrnl子元件。 每當 GPU 預期存取此記憶體時,VidMm 會處理對應和取消對應邏輯位址空間,包括:

  • 針對下列任一項對應配置支援存放區:

    • 傳輸至 VRAM 或從 VRAM 傳輸期間的完整持續時間。
    • 備份存放區對應到系統記憶體或光圈區段的整個時間。
  • 對應和取消對應受監視的柵欄。

  • 在電源轉換期間,驅動程式可能需要儲存部分硬體保留記憶體。 為了處理這種情況, Dxgkrnl 會提供一個機制,讓驅動程式指定儲存此數據的內存量。 驅動程式所需的確切記憶體數量可以動態變更。 也就是說,當適配卡初始化時, Dxgkrnl 會在上限上收取認可費用,以確保在需要時可以取得實體頁面。 Dxgkrnl 負責確保此記憶體已鎖定,並對應至 IOMMU 以進行電源轉換期間的傳輸。

  • 針對任何硬體保留資源,VidMm 可確保在裝置連結至 IOMMU 時正確對應 IOMMU 資源。 這包括使用 PopulatedFromSystemMemory 回報的記憶體區段所報告的記憶體。 對於未透過 VidMm 區段 公開的保留記憶體(例如韌體/BIOD 保留),Dxgkrnl 會進行 DXGKDDI_QUERYADAPTERINFO 呼叫,以查詢驅動程式預先對應的所有保留記憶體範圍。 如需詳細資訊,請參閱 硬體保留記憶體

網域指派

在硬體初始化期間, Dxgkrnl 會為系統上的每個邏輯配接器建立網域。 網域會管理邏輯位址空間,並追蹤對應的頁面數據表和其他必要數據。 單一邏輯配接器中的所有實體配接器都屬於相同的網域。 Dxgkrnl 會透過新的配置回呼例程,以及 VidMm 本身配置的任何記憶體,追蹤所有對應的物理記憶體。

網域會在第一次建立安全虛擬機時連接到裝置,或在使用上述登錄機碼時啟動裝置后不久。

獨佔存取

IOMMU 網域連結和卸離速度很快,但目前並非不可部分完成。 因為不是不可部分完成的,所以在交換至具有不同對應之 IOMMU 網域時,不保證透過 PCIe 發出的交易會正確轉譯。

若要處理這種情況,從 Windows 10 1803 版(WDDM 2.4)開始,KMD 必須實作下列 DDI 配對, 才能呼叫 Dxgkrnl

這些 DIS 會形成開始/結束配對,其中 Dxgkrnl 會要求硬體在總線上無訊息。 每當裝置切換到新的 IOMMU 網域時,驅動程式必須確保其硬體為無訊息。 也就是說,驅動程式必須確保它不會在這兩個呼叫之間從裝置讀取或寫入系統記憶體。

在這兩個呼叫之間, Dxgkrnl 會提供下列保證:

  • 排程器已暫停。 所有作用中工作負載都會排清,而且硬體上不會傳送任何新的工作負載或排程。
  • 不會進行其他 DDI 呼叫。

在這些呼叫過程中,驅動程式可以選擇在獨佔存取期間停用和隱藏中斷(包括 Vsync 中斷),即使沒有來自 OS 的明確通知也一樣。

Dxgkrnl 可確保硬體上排程的任何擱置工作都已完成,然後輸入這個獨佔存取區域。 在此期間, Dxgkrnl 會將網域指派給裝置。 Dxgkrnl 不會在這些呼叫之間提出驅動程式或硬體的任何要求。

DDI 變更

已進行下列 DDI 變更,以支援以 IOMMU 為基礎的 GPU 隔離:

記憶體配置與 IOMMU 的對應

Dxgkrnl 提供上表中前六個回呼給內核模式驅動程式,以允許其配置記憶體,並將它重新對應至 IOMMU 的邏輯地址空間。 這些回呼函式會模擬 Mm API 介面所提供的例程。 它們會提供驅動程式的 MDL,或描述也對應至 IOMMU 之內存的指標。 這些 MDL 會繼續描述實體頁面,但 IOMMU 的邏輯位址空間會對應到相同的位址。

Dxgkrnl 會追蹤這些回呼的要求,以協助確保驅動程式不會洩漏。 配置回呼會提供另一個句柄做為輸出的一部分,必須提供給個別的免費回呼。

對於無法透過其中一個提供配置回呼配置的記憶體, 會提供DXGKCB_MAPMDLTOIOMMU 回呼,以允許追蹤驅動程式管理的 MDL 並搭配 IOMMU 使用。 使用此回呼的驅動程式負責確保 MDL 的存留期超過對應的 unmap 呼叫。 否則,unmap 呼叫具有未定義的行為。 此未定義的行為可能會導致 MDL 頁面的安全性遭到入侵,而 MDL 頁面在取消對應時會重新調整其用途。

VidMm 會自動管理它在系統記憶體中建立的任何配置(例如 DdiCreateAllocationCb、受監視的柵欄等)。 驅動程式不需要執行任何動作,就能讓這些配置正常運作。

框架緩衝區保留

對於在電源轉換期間必須將框架緩衝區保留部分儲存至系統記憶體的驅動程式, Dxgkrnl 會在初始化配接器時,對所需的記憶體收取認可費用。 如果驅動程式回報 IOMMU 隔離支援Dxgkrnl 會在查詢實體配接器上限之後立即發出呼叫 DXGKDDI_QUERYADAPTERINFO

Dxgkrnl 會收取驅動程式所指定金額的認可費用,以確保其一律可以在要求時取得實體頁面。 此動作是針對每個實體配接器建立唯一區段物件,以指定大小上限的非零值。

驅動程式所報告的大小上限必須是PAGE_SIZE的倍數。

在驅動程式選擇時,可以執行往返畫面緩衝區的傳輸。 為了協助傳輸, Dxgkrnl 會將上表的最後四個回呼提供給內核模式驅動程式。 這些回呼可用來對應初始化配接器時所建立之區段對象的適當部分。

驅動程式在呼叫這四個回呼函式時,必須一律為 LDA 鏈結中的潛在客戶裝置提供 hAdapter

驅動程式有兩個選項可實作框架緩衝區保留:

  1. (慣用方法)驅動程式應該使用 DXGKDDI_QUERYADAPTERINFO 呼叫來 配置每個實體適配卡的空間,以指定每個適配卡所需的記憶體數量。 在電源轉換時,驅動程式應該一次儲存或還原一個實體適配卡的記憶體。 此記憶體會分割成多個區段物件,每個實體配接器各一個。

  2. 選擇性地,驅動程式可以將所有數據儲存或還原到單一共享區段物件。 您可以在實體配接器 0 的 DXGKDDI_QUERYADAPTERINFO 呼叫中指定單一大型大小上限,然後指定所有其他實體適配卡的零值來完成此動作。 然後,驅動程式就可以針對所有實體配接器,將整個區段對象釘選一次,以用於所有儲存/還原作業。 這個方法的主要缺點是它需要一次鎖定較大的記憶體,因為它不支援將記憶體子範圍釘選到 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 會驗證不會發生這類交集。 此驗證可確保驅動程式無法意外回報超出保留範圍的實體記憶體區域,這會違反功能的安全性保證。

查詢呼叫會進行一次來查詢所需的範圍數目,後面接著第二次呼叫以填入保留範圍陣列。

測試

如果驅動程式選擇加入這項功能,HLK 測試會掃描驅動程式的匯入資料表,以確保不會呼叫下列 任何 Mm 函式:

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

連續記憶體和 MDL 的所有記憶體配置都應該改為使用列出的函式,透過 Dxgkrnl 的回呼介面。 驅動程式也不應該鎖定任何記憶體。 Dxgkrnl 會管理驅動程式的鎖定頁面。 重新對應記憶體之後,提供給驅動程式的頁面邏輯位址可能不再符合實體位址。