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부터 이 기능은 기본적으로 Microsoft Edge(즉, 컨테이너 가상화)에 대해 Windows Defender Application Guard 사용하도록 설정된 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를 사용하도록 설정한 후에는 현재 IOMMU를 사용하지 않도록 설정할 수 있는 방법이 없습니다.

메모리 액세스

Dxgkrnl 은 GPU에서 액세스할 수 있는 모든 메모리가 IOMMU를 통해 다시 매핑되어 이 메모리에 액세스할 수 있도록 합니다. GPU가 액세스해야 하는 실제 메모리는 현재 네 가지 범주로 나눌 수 있습니다.

  • MmAllocateContiguousMemory 또는 MmAllocatePagesForMdl 스타일 함수(SpecifyCache 및 확장 변형 포함)를 통해 수행되는 드라이버별 할당은 GPU가 액세스하기 전에 IOMMU에 매핑되어야 합니다. DxgkrnlMm API를 호출하는 대신 커널 모드 드라이버에 콜백을 제공하여 한 단계에서 할당 및 다시 매핑을 허용합니다. GPU에 액세스할 수 있도록 의도된 모든 메모리는 이러한 콜백을 거쳐야 합니다. 그렇지 않으면 GPU가 이 메모리에 액세스할 수 없습니다.

  • 페이징 작업 중에 GPU에서 액세스하거나 GpuMmu를 통해 매핑된 모든 메모리는 IOMMU에 매핑되어야 합니다. 이 프로세스는 Dxgkrnl의 하위 구성 요소인 VidMm(Video Memory Manager)의 내부 프로세스입니다. VidMm은 GPU가 다음을 포함하여 이 메모리에 액세스할 것으로 예상되는 경우 언제든지 논리 주소 공간 매핑 및 매핑 해제를 처리합니다.

  • 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는 Dxgkrnl이 호출할 다음 DDI 쌍을 구현해야 합니다.

이러한 DDI는 시작/끝 페어링을 형성합니다. 여기서 Dxgkrnl 은 버스에서 하드웨어가 자동으로 수행되도록 요청합니다. 드라이버는 디바이스가 새 IOMMU 도메인으로 전환될 때마다 하드웨어가 자동으로 유지되도록 해야 합니다. 즉, 드라이버는 이러한 두 호출 사이에 디바이스에서 시스템 메모리를 읽거나 쓰지 않도록 해야 합니다.

이러한 두 호출 사이에 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 체인의 master/리드 디바이스에 대한 hAdapter를 제공해야 합니다.

드라이버에는 프레임 버퍼 예약을 구현하는 두 가지 옵션이 있습니다.

  1. (기본 설정 방법) 드라이버는 위의 DXGKDDI_QUERYADAPTERINFO 호출을 사용하여 실제 어댑터당 공간을 할당하여 어댑터당 필요한 스토리지 크기를 지정해야 합니다. 전원 전환 시 드라이버는 메모리를 한 번에 하나의 실제 어댑터를 저장하거나 복원해야 합니다. 이 메모리는 실제 어댑터당 하나씩 여러 섹션 개체로 분할됩니다.

  2. 필요에 따라 드라이버는 모든 데이터를 단일 공유 섹션 개체에 저장하거나 복원할 수 있습니다. 이 작업은 실제 어댑터 0에 대한 DXGKDDI_QUERYADAPTERINFO 호출에서 최대 크기 하나를 지정한 다음, 다른 모든 물리적 어댑터에 대해 0 값을 지정하여 수행할 수 있습니다. 그런 다음 드라이버는 모든 실제 어댑터에 대해 모든 저장/복원 작업에서 사용하기 위해 전체 섹션 개체를 한 번 고정할 수 있습니다. 이 메서드는 메모리의 하위 범위만 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 은 드라이버에 대해 잠긴 페이지를 관리합니다. 메모리가 다시 매핑되면 드라이버에 제공된 페이지의 논리적 주소가 더 이상 실제 주소와 일치하지 않을 수 있습니다.