基于 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 样式函数 ((包括 SpecifyCache 和扩展变体) )进行的特定于驱动程序的分配必须在 GPU 访问之前映射到 IOMMU。 Dxgkrnl 不会调用 Mm API,而是向内核模式驱动程序提供回调,以便一步完成分配和重新映射。 任何旨在可访问 GPU 的内存都必须经过这些回调,否则 GPU 无法访问此内存。

  • GPU 在分页操作期间访问或通过 GpuMmu 映射的所有内存都必须映射到 IOMMU。 此过程完全位于视频内存管理器 (VidMm) ( Dxgkrnl 的子组件)的内部。 每当预期 GPU 访问此内存时,VidMm 都可以处理逻辑地址空间的映射和取消映射,包括:

  • 在向/从 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

这些 DDI 形成开始/结束配对,其中 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 的生存期超过相应的取消映射调用。 否则,取消映射调用具有未定义的行为,这可能会导致 MDL 中的页面在取消映射时被 Mm 重新调整用途的安全受到损害。

VidMm 会自动管理它创建的任何分配, (例如 DdiCreateAllocationCb、受监视的围栏等,) 系统内存中。 驱动程序无需执行任何操作即可使这些分配正常工作。

帧缓冲区预留

对于在电源转换期间必须将帧缓冲区的保留部分保存到系统内存的驱动程序, Dxgkrnl 在初始化适配器时对所需内存收取提交费用。 如果驱动程序报告 IOMMU 隔离支持Dxgkrnl 将在查询物理适配器上限后立即发出对 DXGKDDI_QUERYADAPTERINFO 的调用,并使用以下代码:

Dxgkrnl 对驱动程序指定的数量收取提交费用,以确保它始终可以在请求时获取物理页面。 此操作是通过为每个物理适配器创建唯一的节对象来完成的,该对象为最大大小指定非零值。

驱动程序报告的最大大小必须是PAGE_SIZE的倍数。

可以在驱动程序选择的时间执行与帧缓冲区的传输。 为了帮助传输, Dxgkrnl 向内核模式驱动程序提供上表中的最后四个回调。 这些回调可用于映射初始化适配器时创建的 section 对象的相应部分。

驱动程序在调用这四个回调函数时,必须始终为 LDA 链中的主设备/潜在顾客设备提供 hAdapter

驱动程序有两个选项来实现帧缓冲区预留:

  1. (首选方法) 驱动程序应使用上述 DXGKDDI_QUERYADAPTERINFO 调用为每个物理适配器分配空间,以指定每个适配器所需的存储量。 在电源转换时,驱动程序应一次保存或还原一个物理适配器的内存。 此内存拆分为多个分区对象,每个物理适配器一个。

  2. (可选)驱动程序可以将所有数据保存或还原到单个共享节对象中。 可以通过在 DXGKDDI_QUERYADAPTERINFO 调用中为物理适配器 0 指定单个最大大小,然后为所有其他物理适配器指定零值来完成此操作。 然后,驱动程序可以固定整个分区对象一次,以便在所有保存/还原操作中用于所有物理适配器。 此方法的主要缺点是需要同时锁定更大的内存量,因为它不支持仅将内存的子范围固定到 MDL 中。 因此,此操作更有可能在内存压力下失败。 驱动程序还将使用正确的页偏移量将 MDL 中的页映射到 GPU。

驱动程序应执行以下任务以完成与帧缓冲区的传输:

  • 在初始化期间,驱动程序应使用分配回调例程之一预分配一小块 GPU 可访问内存。 如果无法一次性映射/锁定整个分区对象,此内存用于帮助确保向前进度。

  • 在电源转换时,驱动程序应首先调用 Dxgkrnl 以固定帧缓冲区。 成功后, Dxgkrnl 为驱动程序提供映射到 IOMMU 的锁定页的 MDL。 然后,驱动程序可以采用对硬件最高效的任何方式直接执行到这些页面的传输。 然后,驱动程序应调用 Dxgkrnl 来解锁/取消映射内存。

  • 如果 Dxgkrnl 无法同时固定整个帧缓冲区,则驱动程序必须尝试使用初始化期间分配的预分配缓冲区向前推进。 在这种情况下,驱动程序以小区块形式执行传输。 在每个区块) 传输 (迭代期间,驱动程序必须要求 Dxgkrnl 提供可将结果复制到的分区对象的映射范围。 然后,驱动程序必须在下一次迭代之前取消映射 section 对象的部分。

以下伪代码是此算法的示例实现。


#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;
        }
    }
}

硬件预留内存

在将设备附加到 IOMMU 之前,VidMm 映射硬件预留内存。

VidMm 自动处理任何报告为具有 PopulatedFromSystemMemory 标志的段的内存。 VidMm 根据提供的物理地址映射此内存。

对于未由段公开的专用硬件预留区域,VidMm 会发出 DXGKDDI_QUERYADAPTERINFO 调用,以通过驱动程序查询范围。 提供的范围不得与 NTOS 内存管理器使用的任何内存区域重叠;VidMm 验证不存在此类交集。 此验证可确保驱动程序不会意外报告超出保留范围的物理内存区域,这会违反该功能的安全保证。

查询调用进行一次以查询所需范围的数量,然后进行第二次调用以填充保留范围的数组。

测试

如果驱动程序选择启用此功能,HLK 测试会扫描驱动程序的导入表,以确保不调用以下 Mm 函数:

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

连续内存和 MDL 的所有内存分配应改为使用列出的函数通过 Dxgkrnl 的回调接口。 驱动程序也不应锁定任何内存。 Dxgkrnl 管理驱动程序的锁定页。 重新映射内存后,提供给驱动程序的页的逻辑地址可能不再与物理地址匹配。