IOMMU 型 GPU 隔離

此頁面說明 IOMMU 型裝置的 IOMMU 型 GPU 隔離功能,Windows 10 1803 版 (WDDM 2.4) 。 如需最新的 IOMMU 更新,請參閱 IOMMU DMA 重新對應

概觀

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

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

從 Windows 10 1803 版開始,此功能預設只會針對啟用 Microsoft Edge (Windows Defender 應用程式防護 的電腦啟用此功能,也就是容器虛擬化) 。

為了開發目的,會透過下列登錄機碼啟用或停用實際的 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 和擴充變化,) 必須在 GPU 存取之前對應到 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 中斷,即使操作系統沒有明確通知也一樣。

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 中頁面的安全性遭到入侵,這些頁面會在取消對應時由 Mm 重新用途。

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 會將 MDL 提供給對應至 IOMMU 的鎖定頁面。 然後,驅動程式就可以直接對這些頁面執行傳輸,不論對硬體而言最有效率的方式為何。 驅動程式接著應該呼叫 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 會管理驅動程式的鎖定頁面。 重新對應記憶體之後,提供給驅動程式的頁面邏輯位址可能不再符合實體位址。