Share via


Isolamento de GPU baseado em IOMMU

Esta página descreve o recurso de isolamento de GPU baseado em IOMMU para dispositivos compatíveis com IOMMU, introduzido no Windows 10 versão 1803 (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 dxgkrnl restrinja o acesso à memória do sistema da GPU usando o hardware IOMMU. O sistema operacional 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 traduza os acessos de memória via PCIe para 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 e leia o conteúdo da memória do sistema que não é necessário para a operação do dispositivo.

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

Para fins de desenvolvimento, a funcionalidade de remapeamento de IOMMU real está 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á habilitado logo após o início do adaptador. Todas as alocações de driver feitas antes desse momento são mapeadas quando elas são habilitadas.

Além disso, se a chave de preparo de velocidade 14688597 estiver definida como habilitada, a IOMMU será ativada 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 suporte adequado para IOMMU.

Enquanto habilitado, 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 ele tiver sido habilitado.

Acesso à memória

Dxgkrnl garante que toda a memória acessível pela GPU seja remapeada por meio da 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 de estilo MmAllocateContiguousMemory ou MmAllocatePagesForMdl (incluindo as variações SpecifyCache e estendidas) devem ser mapeadas para a IOMMU antes de a GPU acessá-las. 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 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 é capaz de acessar essa memória.

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

  • Mapeando o repositório de backup de uma alocação durante toda a duração durante uma transferência de ou para a VRAM ou o tempo inteiro em que ela é mapeada para segmentos de memória ou abertura do sistema.

  • Mapeamento e desmapeamento de cercas monitoradas.

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

  • Para todos os recursos reservados de hardware, o VidMm garante que ele mapeia corretamente os recursos IOMMU no momento em que o dispositivo é anexado à IOMMU. Isso inclui 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 mapeado antecipadamente. Confira Memória reservada de hardware para obter detalhes.

Atribuição de domínio

Durante a inicialização do hardware, 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 de 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 do Registro acima for usada.

Acesso exclusivo

A anexação e o desanexamento do domínio IOMMU são extremamente rápidos, mas, no entanto, não são atômicos no momento. Isso significa que uma transação emitida por PCIe não tem garantia de ser traduzida corretamente durante a troca para um domínio IOMMU com mapeamentos diferentes.

Para lidar com essa situação, começando no Windows 10 versão 1803 (WDDM 2.4), um KMD deve implementar o seguinte par DDI para Dxgkrnl chamar:

Esses DDIs formam um emparelhamento de início/fim, em que dxgkrnl solicita que o hardware fique silencioso sobre o barramento. O driver deve garantir que seu hardware fique silencioso sempre que o dispositivo for alternado para um novo domínio IOMMU. Ou seja, o driver deve garantir que ele não leia ou escreva na memória do sistema do dispositivo entre essas duas chamadas.

Entre essas duas chamadas, dxgkrnl faz as seguintes garantias:

  • 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 DDI é feita.

Como parte dessas chamadas, o driver pode optar por desabilitar e suprimir interrupções (incluindo interrupções do 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, insira essa 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 e mapeamento de memória para IOMMU

Dxgkrnl fornece os seis primeiros retornos de chamada na tabela acima para o driver do modo kernel para permitir que ele aloque memória e remapee-a para o espaço de endereço lógico da IOMMU. Essas funções de retorno de chamada imitam as rotinas fornecidas pela interface da API Mm . Eles 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.

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 um identificador adicional como parte da saída que deve ser fornecida de volta para o respectivo retorno de chamada gratuito.

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 pelo driver sejam rastreados e usados com a IOMMU. Um driver que usa esse retorno de chamada é responsável por garantir que o tempo de vida do MDL exceda a chamada não mapa correspondente. Caso contrário, a chamada não mapeada terá um comportamento indefinido que pode levar à segurança comprometida das páginas do MDL que são reaproveitadas por Mm no momento em que não forem mapeadas.

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 fazer essas alocações funcionarem.

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, 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 cobrança de confirmação sobre o valor especificado 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, o Dxgkrnl fornece os últimos quatro retornos de chamada na tabela acima 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 master/lead em uma cadeia LDA quando ele chama 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 acima 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 de um adaptador físico por vez. Essa memória é dividida entre 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 que requer o bloqueio de uma quantidade maior de memória ao mesmo tempo, pois não dá suporte à fixação apenas de um subconjunto da memória em um MDL. Como resultado, é mais provável que essa operação falhe sob pressão de memória. Espera-se também que o driver mapeie 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 de GPU usando uma das rotinas de retorno de chamada de alocação. Essa memória é usada para ajudar a garantir o progresso futuro se todo o objeto de seção não puder ser mapeado/bloqueado ao mesmo tempo.

  • No momento da transição de energia, o driver deve primeiro chamar Dxgkrnl para fixar o buffer de quadro. Com êxito, dxgkrnl fornece ao driver um MDL para páginas bloqueadas mapeadas para o IOMMU. Em seguida, o driver pode 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/desmarcar a memória.

  • Se dxgkrnl não puder fixar todo o buffer de quadros ao mesmo tempo, o driver deverá tentar avançar usando o buffer preallocado alocado durante a inicialização. Nesse caso, o driver executa a transferência em pequenas partes. Durante cada iteração da transferência (para cada parte), o driver deve pedir a Dxgkrnl para fornecer um intervalo mapeado do objeto de seção no qual eles podem copiar os resultados. Em seguida, o driver deve remover o mapa da 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 anexado à IOMMU.

O VidMm manipula automaticamente 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 valida que essas interseções não ocorrem. Essa validação garante que o driver não possa relatar acidentalmente uma região de memória física 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 aceitar esse recurso, um teste de 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 for remapeada, o endereço lógico das páginas fornecidas ao driver poderá não corresponder mais aos endereços físicos.