Compartilhar via


Isolamento de GPU baseado em IOMMU

O isolamento de GPU baseado em IOMMU é uma técnica usada para aprimorar a segurança e a estabilidade do sistema, gerenciando como as GPUs acessam a memória do sistema. Este artigo descreve o recurso de isolamento de GPU baseado em IOMMU do WDDM para dispositivos compatíveis com IOMMU e como os desenvolvedores podem implementá-lo em seus drivers gráficos.

Esse recurso está disponível no Windows 10, versão 1803 e posteriores (WDDM 2.4). Consulte Remapeamento de DMA do IOMMU para obter atualizações mais recentes do IOMMU.

Visão geral

O isolamento de GPU baseado em IOMMU permite que o Dxgkrnl restrinja o acesso à memória do sistema da GPU usando o hardware IOMMU. O SO pode fornecer endereços lógicos em vez de endereços físicos. Esses endereços lógicos podem ser usados para restringir o acesso do dispositivo à memória do sistema apenas à memória que ele deve ser capaz de acessar. Ele faz isso garantindo que o IOMMU converta os acessos à memória por PCIe em páginas físicas válidas e acessíveis.

Se o endereço lógico acessado pelo dispositivo não for válido, o dispositivo não poderá obter acesso à memória física. Essa restrição impede uma série de explorações que permitem que um invasor obtenha acesso à memória física por meio de um dispositivo de hardware comprometido. Sem ele, os invasores podem ler o conteúdo da memória do sistema que não é necessário para a operação do dispositivo.

Por padrão, esse recurso só está habilitado para computadores em que o Windows Defender Application Guard está habilitado para o Microsoft Edge (ou seja, virtualização de contêiner).

Para fins de desenvolvimento, a funcionalidade real de remapeamento do IOMMU é habilitada ou desabilitada por meio da seguinte chave do registro:

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.

Se esse recurso estiver habilitado, o IOMMU será habilitada logo após a inicialização do adaptador. Todas as alocações de driver feitas antes desse horário são mapeadas quando ele é habilitado.

Além disso, se a chave de preparo de velocidade 14688597 estiver definida como habilitada, o IOMMU será ativado quando uma máquina virtual segura for criada. Por enquanto, essa chave de preparo está desabilitada por padrão para permitir a auto-hospedagem sem o suporte adequado ao IOMMU.

Enquanto habilitada, a inicialização de uma máquina virtual segura falhará se o driver não fornecer suporte a IOMMU.

No momento, não há como desabilitar o IOMMU depois que é habilitado.

Acesso à memória

Dxgkrnl garante que toda a memória acessível pela GPU seja remapeada por meio do IOMMU para garantir que essa memória esteja acessível. A memória física que a GPU precisa acessar atualmente pode ser dividida em quatro categorias:

  • As alocações específicas do driver feitas por meio de funções no estilo MmAllocateContiguousMemory ou MmAllocatePagesForMdl (incluindo o SpecifyCache e variações estendidas) devem ser mapeadas para o IOMMU antes que a GPU as acesse. Em vez de chamar as APIs Mm, o Dxgkrnl fornece retornos de chamada para o driver do modo kernel para permitir a alocação e o remapeamento em uma etapa. Qualquer memória destinada a ser acessível por GPU deve passar por esses retornos de chamada, ou a GPU não poderá acessar essa memória.

  • Toda a memória acessada pela GPU durante as operações de paginação ou mapeada por meio do GpuMmu deve ser mapeada para o IOMMU. Esse processo é totalmente interno ao Gerenciador de Memória de Vídeo (VidMm), que é um subcomponente do Dxgkrnl. O VidMm lida com o mapeamento e o desmapeamento do espaço de endereço lógico sempre que se espera que a GPU acesse essa memória, incluindo:

  • Mapear o repositório de backup de uma alocação para:

    • Toda a duração durante uma transferência de ou para VRAM.
    • O tempo todo em que o repositório de backup é mapeado para a memória do sistema ou segmentos de abertura.
  • Mapeamento e desmapeamento de cercas monitoradas.

  • Durante as transições de energia, o driver pode precisar salvar partes da memória reservada por hardware. Para lidar com essa situação, Dxgkrnl fornece um mecanismo para o driver especificar quanta memória está alocada para armazenar esses dados. A quantidade exata de memória que o driver requer pode mudar dinamicamente. Dito isso, Dxgkrnl assume uma carga de confirmação no limite superior no momento em que o adaptador é inicializado para garantir que as páginas físicas possam ser obtidas quando necessário. Dxgkrnl é responsável por garantir que essa memória seja bloqueada e mapeada para o IOMMU para a transferência durante as transições de energia.

  • Para quaisquer recursos reservados de hardware, o VidMm garante que ele mapeie corretamente os recursos de IOMMU no momento em que o dispositivo é anexado ao IOMMU. Isso inclui a memória relatada por segmentos de memória relatados com PopulatedFromSystemMemory. Para memória reservada (por exemplo, firmware/BIOD reservado) que não é exposta por meio de segmentos VidMm, Dxgkrnl faz uma chamada DXGKDDI_QUERYADAPTERINFO para consultar todos os intervalos de memória reservados que o driver precisa mapear com antecedência. Consulte Memória reservada de hardware para obter detalhes.

Atribuição de domínio

Durante a inicialização do hardware, o Dxgkrnl cria um domínio para cada adaptador lógico no sistema. O domínio gerencia o espaço de endereço lógico e rastreia tabelas de páginas e outros dados necessários para os mapeamentos. Todos os adaptadores físicos em um único adaptador lógico pertencem ao mesmo domínio. O Dxgkrnl rastreia toda a memória física mapeada por meio das novas rotinas de retorno de chamada de alocação e qualquer memória alocada pelo próprio VidMm.

O domínio será anexado ao dispositivo na primeira vez que uma máquina virtual segura for criada ou logo após o dispositivo ser iniciado se a chave de registro acima for usada.

Acesso exclusivo

Anexar e desanexar o domínio do IOMMU é uma tarefa bem rápida, mas ainda não é atômica. Isso prova que não há garantia de que uma transação emitida por PCIe será convertida corretamente durante a troca para um domínio da IOMMU com mapeamentos diferentes.

Para lidar com essa situação, a partir do Windows 10 versão 1803 (WDDM 2.4), o KMD precisa implementar o seguinte par de DDIs para que o Dxgkrnl chame:

Esses DDIs formam um emparelhamento de início/término, em que Dxgkrnl solicita que o hardware seja silencioso no barramento. O driver precisa ter um hardware silencioso quando o dispositivo mudar para um novo domínio da IOMMU. Ou seja, o driver deve conseguir ler e gravar na memória do sistema do dispositivo entre essas duas chamadas.

Entre essas duas chamadas, o Dxgkrnl garante o seguinte:

  • O Agendador está suspenso. Todas as cargas de trabalho ativas são liberadas e nenhuma nova carga de trabalho é enviada ou agendada no hardware.
  • Nenhuma outra chamada de DDI é feita.

Como parte dessas chamadas, o driver pode desativar e cancelar interrupções (incluindo interrupções Vsync) durante o acesso exclusivo, mesmo sem notificação explícita do sistema operacional.

Dxgkrnl garante que qualquer trabalho pendente agendado no hardware seja concluído e, em seguida, entre nessa região de acesso exclusivo. Durante esse tempo, Dxgkrnl atribui o domínio ao dispositivo. Dxgkrnl não faz nenhuma solicitação do driver ou hardware entre essas chamadas.

Alterações de DDI

As seguintes alterações de DDI foram feitas para dar suporte ao isolamento de GPU baseado em IOMMU:

Alocação de memória e mapeamento para IOMMU

Dxgkrnl fornece os primeiros seis retornos de chamada na tabela anterior para o driver do modo kernel para permitir que ele aloque memória e a remapeie para o espaço de endereço lógico do IOMMU. Essas funções de retorno de chamada imitam as rotinas fornecidas pela interface da API Mm. Elas fornecem ao driver MDLs ou ponteiros que descrevem a memória que também é mapeada para o IOMMU. Esses MDLs continuam a descrever páginas físicas, mas o espaço de endereço lógico do IOMMU é mapeado no mesmo endereço.

Dxgkrnl rastreia solicitações para esses retornos de chamada para ajudar a garantir que não haja vazamentos pelo driver. Os retornos de chamada de alocação fornecem outro identificador como parte da saída que deve ser fornecido de volta ao respectivo retorno de chamada livre.

Para memória que não pode ser alocada por meio de um dos retornos de chamada de alocação fornecidos, o retorno de chamada DXGKCB_MAPMDLTOIOMMU é fornecido para permitir que MDLs gerenciados por driver sejam rastreados e usados com o IOMMU. Um driver que usa esse retorno de chamada é responsável por garantir que o tempo de vida do MDL exceda a chamada de desmapeamento correspondente. Caso contrário, a chamada para desmapear terá um comportamento indefinido. Esse comportamento indefinido pode levar ao comprometimento da segurança das páginas do MDL que a Mm reaproveitou no momento em que são desmapeadas.

O VidMm gerencia automaticamente todas as alocações que cria (por exemplo, DdiCreateAllocationCb, cercas monitoradas etc.) na memória do sistema. O driver não precisa fazer nada para que essas alocações funcionem.

Reserva de buffer de quadros

Para drivers que devem salvar partes reservadas do buffer de quadros na memória do sistema durante as transições de energia, Dxgkrnl assume uma carga de confirmação na memória necessária quando o adaptador é inicializado. Se o driver relatar suporte ao isolamento de IOMMU, o Dxgkrnl emitirá uma chamada para DXGKDDI_QUERYADAPTERINFO com o seguinte imediatamente após consultar as tampas do adaptador físico:

  • O tipo é DXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • A entrada é do tipo UINT, que é o índice do adaptador físico.
  • A saída é do tipo DXGK_FRAMEBUFFERSAVEAREA e deve ser o tamanho máximo exigido pelo driver para salvar a área de reserva do buffer de quadros durante as transições de energia.

Dxgkrnl assume uma carga de confirmação sobre a quantidade especificada pelo driver para garantir que ele sempre possa obter páginas físicas mediante solicitação. Essa ação é feita criando um objeto de seção exclusivo para cada adaptador físico que especifica um valor diferente de zero para o tamanho máximo.

O tamanho máximo relatado pelo driver deve ser um múltiplo de PAGE_SIZE.

A execução da transferência de e para o buffer de quadros pode ser feita em um momento de escolha do driver. Para ajudar na transferência, Dxgkrnl fornece os últimos quatro retornos de chamada na tabela anterior para o driver do modo kernel. Esses retornos de chamada podem ser usados para mapear as partes apropriadas do objeto de seção que foi criado quando o adaptador foi inicializado.

O driver sempre deve fornecer o hAdapter para o dispositivo principal em uma cadeia LDA ao chamar essas quatro funções de retorno de chamada.

O driver tem duas opções para implementar a reserva de buffer de quadros:

  1. (Método preferencial) O driver deve alocar espaço por adaptador físico usando a chamada DXGKDDI_QUERYADAPTERINFO para especificar a quantidade de armazenamento necessária por adaptador. No momento da transição de energia, o driver deve salvar ou restaurar a memória um adaptador físico por vez. Essa memória é dividida em vários objetos de seção, um por adaptador físico.

  2. Opcionalmente, o driver pode salvar ou restaurar todos os dados em um único objeto de seção compartilhada. Essa ação pode ser feita especificando um único tamanho máximo grande na chamada DXGKDDI_QUERYADAPTERINFO para o adaptador físico 0 e, em seguida, um valor zero para todos os outros adaptadores físicos. Em seguida, o driver pode fixar todo o objeto de seção uma vez para uso em todas as operações de salvamento/restauração, para todos os adaptadores físicos. Esse método tem a principal desvantagem de exigir o bloqueio de uma quantidade maior de memória de uma só vez, pois não dá suporte à fixação apenas de um subintervalo da memória em um MDL. Como resultado, é mais provável que essa operação falhe sob pressão de memória. O driver também deve mapear as páginas no MDL para a GPU usando os deslocamentos de página corretos.

O driver deve executar as seguintes tarefas para concluir uma transferência de ou para o buffer de quadros:

  • Durante a inicialização, o driver deve pré-alocar uma pequena parte da memória acessível da GPU usando uma das rotinas de retorno de chamada de alocação. Essa memória é utilizada para ajudar a garantir o progresso se todo o objeto de seção não puder ser mapeado/bloqueado de uma só vez.

  • No momento da transição de energia, o driver deve primeiro chamar Dxgkrnl para fixar o buffer de quadros. Em caso de êxito, Dxgkrnl fornece ao driver um MDL para páginas bloqueadas mapeadas para o IOMMU. O driver pode então executar uma transferência diretamente para essas páginas em qualquer meio que seja mais eficiente para o hardware. Em seguida, o driver deve chamar Dxgkrnl para desbloquear/desmapear a memória.

  • Se Dxgkrnl não puder fixar todo o buffer de quadros de uma só vez, o driver tentará avançar usando o buffer pré-alocado alocado durante a inicialização. Nesse caso, o driver executa a transferência em pequenos blocos. Durante cada iteração da transferência (para cada bloco), o driver deve solicitar que Dxgkrnl forneça um intervalo mapeado do objeto de seção para o qual eles podem copiar os resultados. Em seguida, o driver deve desmapear a parte do objeto de seção antes da próxima iteração.

O pseudocódigo a seguir é um exemplo de implementação desse algoritmo.


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

Memória reservada de hardware

O VidMm mapeia a memória reservada de hardware antes que o dispositivo seja conectado ao IOMMU.

O VidMm lida automaticamente com qualquer memória relatada como um segmento com o sinalizador PopulatedFromSystemMemory. O VidMm mapeia essa memória com base no endereço físico fornecido.

Para regiões reservadas de hardware privado não expostas por segmentos, o VidMm faz uma chamada DXGKDDI_QUERYADAPTERINFO para consultar os intervalos pelo driver. Os intervalos fornecidos não devem se sobrepor a nenhuma região de memória usada pelo gerenciador de memória NTOS. O VidMm garanta que tais interseções não ocorram. Essa validação garante que o driver não possa relatar acidentalmente uma região da memória física que está fora do intervalo reservado, o que violaria as garantias de segurança do recurso.

A chamada de consulta é feita uma vez para consultar o número de intervalos necessários e é seguida por uma segunda chamada para preencher a matriz de intervalos reservados.

Testando

Se o driver optar por esse recurso, um teste HLK examinará a tabela de importação do driver para garantir que nenhuma das seguintes funções Mm seja chamada:

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

Toda a alocação de memória para memória contígua e MDLs deve, em vez disso, passar pela interface de retorno de chamada do Dxgkrnl usando as funções listadas. O driver também não deve bloquear nenhuma memória. O Dxgkrnl gerencia páginas bloqueadas para o driver. Depois que a memória é remapeada, o endereço lógico das páginas fornecidas ao driver pode não corresponder mais aos endereços físicos.