Изоляция GPU на основе IOMMU

На этой странице описывается функция изоляции GPU на основе IOMMU для устройств с поддержкой IOMMU, представленная в Windows 10 версии 1803 (WDDM 2.4). Дополнительные сведения о последних обновлениях IOMMU см. в статье Переназначение IOMMU DMA .

Общие сведения

Изоляция GPU на основе IOMMU позволяет Dxgkrnl ограничить доступ к системной памяти с GPU, используя оборудование IOMMU. ОС может предоставлять логические адреса, а не физические. Эти логические адреса можно использовать для ограничения доступа устройства к системной памяти только той памятью, к которую оно должно получить доступ. Для этого IOMMU преобразует доступ к памяти через PCIe в допустимые и доступные физические страницы.

Если логический адрес, к которым обращается устройство, недопустим, устройство не может получить доступ к физической памяти. Это ограничение предотвращает ряд эксплойтов, которые позволяют злоумышленнику получить доступ к физической памяти через скомпрометированное аппаратное устройство и считывать содержимое системной памяти, которое не требуется для работы устройства.

Начиная с Windows 10 версии 1803 эта функция по умолчанию включена только для компьютеров, на которых Защитник Windows Application Guard включена для 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 (включая SpecifyCache и расширенные варианты), должны быть сопоставлены с IOMMU перед доступом к gpu. Вместо вызова API MmDxgkrnl предоставляет обратные вызовы драйверу режима ядра, чтобы разрешить выделение и переназначение за один шаг. Любая память, доступная к GPU, должна проходить через эти обратные вызовы, иначе GPU не может получить доступ к этой памяти.

  • Вся память, доступная GPU во время операций подкачки или сопоставленная с помощью GpuMmu, должна быть сопоставлена с IOMMU. Этот процесс полностью является внутренним для диспетчера видеопамять (VidMm), который является подкомпонентом Dxgkrnl. VidMm обрабатывает сопоставление и распакуется логического адресного пространства в любое время, когда GPU должен получить доступ к этой памяти, в том числе:

  • Сопоставление резервного хранилища выделения в течение всего периода передачи в виртуальную память или из нее или за все время ее сопоставления с сегментами системной памяти или диафрагмы.

  • Сопоставление и распакуирование отслеживаемых заборов.

  • Во время переключения питания драйверу может потребоваться сохранить части памяти, зарезервированной оборудованием. Чтобы справиться с этой ситуацией, Dxgkrnl предоставляет драйверу механизм, позволяющий указать объем памяти, необходимой для хранения этих данных. Точный объем памяти, необходимый драйверу, может динамически изменяться, но Dxgkrnl берет на себя заряд фиксации на верхней границе в момент инициализации адаптера, чтобы при необходимости можно было получить физические страницы. Dxgkrnl отвечает за блокировку этой памяти и ее сопоставление с IOMMU для передачи во время переключения питания.

  • Для любого зарезервированного оборудования VidMm обеспечивает правильное сопоставление ресурсов IOMMU к моменту подключения устройства к IOMMU. К ним относится память, сообщаемая сегментами памяти с помощью Команды ЗаполнениеFromSystemMemory. Для зарезервированной памяти (например, зарезервированной встроенного ПО или BIOD), которая не предоставляется через сегменты VidMm, Dxgkrnl выполняет DXGKDDI_QUERYADAPTERINFO вызов для запроса всех зарезервированных диапазонов памяти, которые необходимо сопоставить драйверу заранее. Дополнительные сведения см. в разделе Зарезервированная память оборудования .

Назначение домена

Во время инициализации оборудования Dxgkrnl создает домен для каждого логического адаптера в системе. Домен управляет логическим адресным пространством и отслеживает таблицы страниц и другие необходимые данные для сопоставлений. Все физические адаптеры в одном логическом адаптере принадлежат к одному домену. Dxgkrnl отслеживает всю сопоставленную физическую память с помощью новых процедур обратного вызова выделения и любую память, выделенную самим VidMm.

Домен будет присоединен к устройству при первом создании защищенной виртуальной машины или вскоре после запуска устройства, если используется указанный выше раздел реестра.

Монопольный доступ

Подключение и отсоединение домена IOMMU выполняется очень быстро, но, тем не менее, в настоящее время не является атомарным. Это означает, что транзакция, выданная через PCIe, не гарантируется правильное преобразование при переключении на домен IOMMU с различными сопоставлениями.

Чтобы справиться с этой ситуацией, начиная с Windows 10 версии 1803 (WDDM 2.4), KMD должен реализовать следующую пару DDI для вызова Dxgkrnl:

  • DxgkDdiBeginExclusiveAccess вызывается, чтобы уведомить KMD о том, что ожидается переключение домена IOMMU.
  • DxgkDdiEndExclusiveAccess вызывается после завершения переключения домена IOMMU.

Эти DDIs образуют начальное и конечное связывание, где Dxgkrnl запрашивает , чтобы оборудование было беззвучно над шиной. Драйвер должен гарантировать, что его оборудование не работает при каждом переключении устройства на новый домен IOMMU. То есть драйвер должен убедиться, что он не считывает и не записывает данные в системную память с устройства между этими двумя вызовами.

Между этими двумя вызовами Dxgkrnl дает следующие гарантии:

  • Планировщик приостанавливается. Все активные рабочие нагрузки сбрасываются, а новые рабочие нагрузки не отправляются или не планируются на оборудовании.
  • Другие вызовы DDI не выполняются.

В рамках этих вызовов драйвер может отключить и отключить прерывания (включая прерывания Vsync) на время монопольного доступа, даже без явного уведомления от ОС.

Dxgkrnl гарантирует, что все ожидающие работы, запланированные на оборудовании, завершаются, а затем входит в этот регион монопольного доступа. В течение этого времени Dxgkrnl назначает домен устройству. Dxgkrnl не выполняет никаких запросов драйвера или оборудования между этими вызовами.

Изменения DDI

Для поддержки изоляции GPU на основе IOMMU были внесены следующие изменения DDI:

Выделение памяти и сопоставление с IOMMU

Dxgkrnl предоставляет первые шесть обратных вызовов из приведенной выше таблицы драйверу режима ядра, чтобы позволить ему выделить память и повторно сопоставить ее с логическим адресным пространством IOMMU. Эти функции обратного вызова имитируют подпрограммы, предоставляемые интерфейсом API Mm . Они предоставляют драйверу многомерные списки или указатели, описывающие память, которая также сопоставляется с IOMMU. Эти многомерные списки по-прежнему описывают физические страницы, но логическое адресное пространство IOMMU сопоставляется с тем же адресом.

Dxgkrnl отслеживает запросы к этим обратным вызовам, чтобы гарантировать отсутствие утечек драйвера. Обратные вызовы выделения предоставляют дополнительный дескриптор в составе выходных данных, который должен быть предоставлен в соответствующий бесплатный обратный вызов.

Для памяти, которая не может быть выделена с помощью одного из предоставленных обратных вызовов выделения, предоставляется обратный вызов DXGKCB_MAPMDLTOIOMMU , позволяющий отслеживать управляемые драйвером многомерные списки и использовать их с IOMMU. Драйвер, использующий этот обратный вызов, отвечает за то, чтобы время существования MDL превысило соответствующий вызов unmap. В противном случае вызов unmap имеет неопределенное поведение, которое может привести к компрометации безопасности страниц из MDL, которые будут перепрофилированы mm к моменту их отмены сопоставления.

VidMm автоматически управляет всеми созданными выделениями (например, DdiCreateAllocationCb, отслеживаемых ограждений и т. д.) в системной памяти. Драйверу не нужно ничего делать, чтобы эти выделения работали.

Резервирование буфера кадров

Для драйверов, которые должны сохранять зарезервированные части буфера кадров в системной памяти во время перехода питания, Dxgkrnl берет на себя заряд фиксации требуемой памяти при инициализации адаптера. Если драйвер сообщает о поддержке изоляции IOMMU, Dxgkrnl вызовет DXGKDDI_QUERYADAPTERINFO со следующим кодом сразу после запроса к ограничениям физического адаптера:

  • Тип— DXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • Входные данные являются типом UINT, который является физическим индексом адаптера.
  • Выходные данные типа DXGK_FRAMEBUFFERSAVEAREA и должны быть максимальным размером, требуемым драйвером для сохранения резервной области буфера кадров во время перехода питания.

Dxgkrnl берет на себя плату за фиксацию на сумму, указанную драйвером, чтобы гарантировать, что он всегда может получать физические страницы по запросу. Это действие выполняется путем создания уникального объекта раздела для каждого физического адаптера, указывающего ненулевое значение для максимального размера.

Максимальный размер, сообщаемый драйвером, должен быть кратным PAGE_SIZE.

Выполнение передачи в буфер кадров и из буфера может выполняться по выбору драйвера. Чтобы упростить передачу, Dxgkrnl предоставляет последние четыре обратных вызова в приведенной выше таблице драйверу режима ядра. Эти обратные вызовы можно использовать для сопоставления соответствующих частей объекта section, созданного при инициализации адаптера.

Драйвер всегда должен предоставлять hAdapter для master/ведущего устройства в цепочке LDA при вызове этих четырех функций обратного вызова.

Драйвер может реализовать резервирование буфера кадров двумя способами:

  1. (Предпочтительный метод) Драйвер должен выделить место для каждого физического адаптера с помощью приведенного выше DXGKDDI_QUERYADAPTERINFO вызова для указания объема хранилища, необходимого для каждого адаптера. Во время перехода на питание драйвер должен сохранять или восстанавливать память по одному физическому адаптеру за раз. Эта память разделена на несколько объектов разделов, по одному на физический адаптер.

  2. При необходимости драйвер может сохранить или восстановить все данные в одном объекте общего раздела. Это действие можно выполнить, указав один большой максимальный размер в DXGKDDI_QUERYADAPTERINFO вызов физического адаптера 0, а затем нулевое значение для всех остальных физических адаптеров. Затем драйвер может закрепить весь объект раздела один раз для использования во всех операциях сохранения и восстановления для всех физических адаптеров. Этот метод имеет основной недостаток, который требует одновременной блокировки большего объема памяти, так как он не поддерживает закрепление только поддиапапапа памяти в MDL. В результате эта операция, скорее всего, завершится сбоем при нехватке памяти. Драйвер также должен сопоставить страницы в MDL с GPU, используя правильные смещения страниц.

Чтобы завершить передачу в буфер кадров или из буфера кадров, драйвер должен выполнить следующие задачи:

  • Во время инициализации драйвер должен предварительно выделить небольшой блок доступной памяти GPU с помощью одной из процедур обратного вызова выделения. Эта память используется для обеспечения прогресса вперед, если не удается сопоставить или заблокировать весь объект раздела одновременно.

  • Во время перехода на питание драйвер должен сначала вызвать Dxgkrnl , чтобы закрепить буфер кадров. При успешном выполнении Dxgkrnl предоставляет драйверу MDL для заблокированных страниц, сопоставленных с IOMMU. Затем драйвер может выполнить передачу непосредственно на эти страницы любым способом, наиболее эффективным для оборудования. Затем драйвер должен вызвать 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;
        }
    }
}

Зарезервированная память оборудования

VidMm сопоставляет зарезервированную память оборудования до того, как устройство будет подключено к IOMMU.

VidMm автоматически обрабатывает любую память, сообщаемую как сегмент, с флагом PopulatedFromSystemMemory . VidMm сопоставляет эту память на основе предоставленного физического адреса.

Для частных зарезервированных областей оборудования, не предоставляемых сегментами, VidMm выполняет DXGKDDI_QUERYADAPTERINFO вызов для запроса диапазонов драйвером. Указанные диапазоны не должны перекрывать области памяти, используемые диспетчером памяти NTOS; VidMm проверяет отсутствие таких пересечений. Эта проверка гарантирует, что драйвер не может случайно сообщить о области физической памяти, которая находится за пределами зарезервированного диапазона, что нарушает гарантии безопасности функции.

Вызов запроса выполняется один раз, чтобы запросить количество необходимых диапазонов, а затем второй вызов для заполнения массива зарезервированных диапазонов.

Тестирование

Если драйвер согласится использовать эту функцию, тест HLK проверяет таблицу импорта драйвера, чтобы убедиться, что ни одна из следующих функций Mm не вызывается:

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

Все выделения памяти для непрерывной памяти и многомерных выражений должны проходить через интерфейс обратного вызова Dxgkrnl с использованием перечисленных функций. Драйвер также не должен блокировать память. Dxgkrnl управляет заблокированными страницами для драйвера. После переназначения памяти логический адрес страниц, предоставленный драйверу, может перестать совпадать с физическими адресами.