Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
O isolamento de GPU baseado em IOMMU é uma técnica usada para melhorar 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.
Este recurso está disponível a partir do Windows 10 versão 1803 (WDDM 2.4). Consulte Remapeamento de DMA da IOMMU para obter atualizações mais recentes da IOMMU.
Visão geral
O isolamento de GPU baseado em IOMMU permite que o Dxgkrnl restrinja o acesso à memória do sistema da GPU fazendo uso de 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 a IOMMU traduza 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. Sem ele, os invasores poderiam ler o conteúdo da memória do sistema que não é necessário para o funcionamento do dispositivo.
Por padrão, esse recurso só é habilitado para PCs 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 IOMMU é ativada ou desativada através da seguinte chave do registo:
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, a IOMMU será ativada logo após o início do adaptador. Todas as alocações de driver feitas antes desse tempo são mapeadas quando ele é habilitado.
Além disso, se a chave de configuração de velocidade 14688597 estiver definida como ativada, a IOMMU será ativada quando uma máquina virtual segura for criada. Por enquanto, essa chave de estágio está desativada por padrão para permitir o autoalojamento sem suporte adequado ao IOMMU.
Quando ativado, falhará o arranque de uma máquina virtual segura se o driver não fornecer suporte IOMMU.
Atualmente, não há como desativar a IOMMU depois que ela estiver ativada.
Acesso à memória
O Dxgkrnl garante que toda a memória acessível pela GPU seja remapeada através da IOMMU para garantir que essa memória seja acessível. Atualmente, a memória física que a GPU precisa acessar pode ser dividida em quatro categorias:
As alocações específicas do driver feitas por meio das funções no estilo MmAllocateContiguousMemory ou MmAllocatePagesForMdl (incluindo o SpecifyCache e as variações estendidas) devem ser mapeadas para a IOMMU antes que a GPU as acesse. Ao invés de chamar as APIs Mm, o Dxgkrnl fornece callbacks para o driver de modo kernel de modo a permitir a alocação e o remapeamento num único passo. 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 através do GpuMmu deve ser mapeada para o IOMMU. Este processo é totalmente interno ao Video Memory Manager (VidMm), que é um subcomponente do Dxgkrnl. O VidMm lida com o mapeamento e o desmapeamento do espaço de endereçamento lógico sempre que se espera que a GPU acesse essa memória, incluindo:
Mapeando o armazenamento de suporte de uma alocação para:
- Toda a duração durante uma transferência de ou para a VRAM.
- Durante todo o tempo em que o armazenamento auxiliar está 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á adiantada para armazenar esses dados. A quantidade exata de memória que o driver requer pode mudar dinamicamente. Tendo dito isso, o Dxgkrnl assume uma carga de compromisso no limite máximo quando 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 a IOMMU para a transferência durante as transições de energia.
Para quaisquer recursos reservados de hardware, o VidMm garante que mapeia corretamente os recursos IOMMU no momento em que o dispositivo é conectado ao IOMMU. Isto inclui a memória relatada por segmentos de memória que utilizam PopulatedFromSystemMemory. Para memória reservada (por exemplo, firmware/BIOD reservado) que não é exposta através de segmentos VidMm, o 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, 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ágina e outros dados necessários para os mapeamentos. Todos os adaptadores físicos em um único adaptador lógico pertencem ao mesmo domínio. Dxgkrnl rastreia toda a memória física mapeada através das novas rotinas de alocação de retorno de chamada 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 do Registro acima for usada.
Acesso exclusivo
A operação de anexação e desanexação de domínio IOMMU é rápida, mas atualmente não é atômica. Como não é atômica, não é garantido que uma transação emitida sobre PCIe seja traduzida corretamente durante a troca para um domínio IOMMU com mapeamentos diferentes.
Para lidar com esta situação, a partir do Windows 10 versão 1803 (WDDM 2.4), um KMD deve implementar o seguinte par DDI para que Dxgkrnl possa chamar:
- DxgkDdiBeginExclusiveAccess é chamado para notificar o KMD de que uma alternância de domínio IOMMU está prestes a acontecer.
- DxgkDdiEndExclusiveAccess é chamado depois de a comutação de domínio IOMMU estar completa.
Esses DDIs formam um emparelhamento início/fim, onde o Dxgkrnl solicita que o hardware não transmita sinais sobre o barramento. O driver deve garantir que seu hardware seja silencioso sempre que o dispositivo for alternado para um novo domínio IOMMU. Ou seja, o driver deve garantir que ele não lê ou grava na memória do sistema do dispositivo entre essas duas chamadas.
Entre estas 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 motorista pode optar por desativar e suprimir interrupções (incluindo interrupções Vsync) durante o acesso exclusivo, mesmo sem notificação explícita do sistema operacional.
O Dxgkrnl garante que qualquer trabalho pendente agendado no hardware seja concluído e, em seguida, entra 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 DDI foram feitas para suportar o isolamento de GPU baseado em IOMMU:
O limite IoMmuSecureModeSupported foi adicionado ao DXGK_VIDMMCAPS
Foi acrescentado o valor enum DXGKQAITYPE_FRAMEBUFFERSAVESIZE e a estrutura DXGK_FRAMEBUFFERSAVEAREA
Foram acrescentados o valor enum DXGKQAITYPE_HARDWARERESERVEDRANGES e a estrutura DXGK_HARDWARERESERVEDRANGE
O DXGK_MEMORY_CACHING_TYPE foi adicionado
Um driver de modo kernel deve implementar os seguintes DDIs:
Os seguintes retornos de chamada Dxgkrnl e estruturas de parâmetros de retornos de chamada foram adicionados. As funções de retorno de chamada devem ser chamadas no IRQL <= APC_LEVEL. A partir do WDDM 3.2, os drivers que chamam qualquer uma dessas funções são validados em relação a esse requisito e verificam se o IRQL é DISPATCH_LEVEL ou superior.
Alocação de memória e mapeamento para IOMMU
Dxgkrnl fornece os primeiros seis callbacks na tabela anterior ao driver em 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 . Eles fornecem ao driver MDLs, ou ponteiros que descrevem a memória que também é mapeada através do IOMMU. Essas 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 as solicitações para esses callbacks para ajudar a garantir que não haja vazamentos por parte do controlador. Os callbacks de alocação fornecem outro identificador como parte da saída que deve ser fornecida de volta para o respetivo callback de liberação.
Para memória que não pode ser alocada por meio de um dos callbacks de alocação fornecidos, é disponibilizado o callback DXGKCB_MAPMDLTOIOMMU de modo a permitir que as MDLs geridas pelo driver sejam rastreadas e usadas 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 unmap correspondente. Caso contrário, a chamada do unmap terá um comportamento indefinido. Esse comportamento indefinido pode levar a uma segurança comprometida das páginas do MDL que Mm reaproveitaram 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 motorista 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, o Dxgkrnl realiza um compromisso sobre a memória necessária quando o adaptador é inicializado. Se o driver relatar suporte de isolamento IOMMU, Dxgkrnl emitirá uma chamada para DXGKDDI_QUERYADAPTERINFO com o seguinte imediatamente após consultar as capacidades do adaptador físico:
- O tipo é DXGKQAITYPE_FRAMEBUFFERSAVESIZE
- A entrada é do tipo UINT, que é o índice do adaptador físico.
- A saída é de tipo DXGK_FRAMEBUFFERSAVEAREA e deve ser o tamanho máximo exigido pelo driver para salvar a área de reserva do buffer de frames durante as transições de estados de energia.
Dxgkrnl cobra uma taxa de compromisso sobre o valor especificado pelo motorista 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 comunicado pelo condutor deve ser um múltiplo de PAGE_SIZE.
A transferência de e para o frame buffer pode ser realizada num momento à escolha do driver. Para ajudar na transferência, Dxgkrnl fornece os últimos quatro retornos de chamada na tabela anterior para o driver em modo de kernel. Esses callbacks podem ser usados para mapear as secções adequadas do objeto que foi criado quando o adaptador foi inicializado.
O driver deve sempre fornecer o hAdapter para o dispositivo principal em uma cadeia de 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:
(Método preferido) 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 de cada vez. Essa memória é dividida em vários objetos de seção, um por adaptador físico.
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 grande tamanho máximo único na chamada DXGKDDI_QUERYADAPTERINFO para o adaptador físico 0 e depois um valor zero para todos os outros adaptadores físicos. O driver pode então 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. Este método tem a principal desvantagem de exigir o bloqueio de uma quantidade maior de memória de uma só vez, uma vez que não suporta fixar apenas um subintervalo da memória em um MDL. Como resultado, essa operação é mais provável de falhar sob pressão de memória. Espera-se também que o driver mapeie as páginas na MDL para a GPU usando os offsets de página corretos.
O driver deve executar as seguintes tarefas para concluir uma transferência para ou a partir do framebuffer:
Durante a inicialização, o driver deve pré-alocar um pequeno pedaço de memória acessível pela GPU usando uma das rotinas de retorno de chamada de alocação. Essa memória é usada 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 sucesso, o Dxgkrnl fornece ao driver um MDL para páginas trancadas que são mapeadas para o IOMMU. O driver pode então realizar uma transferência diretamente para essas páginas em qualquer meio que seja mais eficiente para o hardware. O driver deve então chamar Dxgkrnl para desbloquear / desmapear a memória.
Se o Dxgkrnl não conseguir fixar todo o buffer de quadros de uma só vez, o driver deverá tentar fazer progressos usando o buffer pré-alocado durante a inicialização. Neste caso, o motorista executa a transferência em pequenos pedaços. Durante cada iteração da transferência (para cada bloco), o driver deve pedir ao Dxgkrnl para fornecer um intervalo mapeado do objeto de seção para o qual eles podem copiar os resultados. O driver deve então 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 por hardware
O VidMm mapeia a memória reservada de hardware antes que o dispositivo seja conectado ao IOMMU.
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 que o driver consulte os intervalos. Os intervalos fornecidos não devem sobrepor-se a quaisquer regiões de memória utilizadas pelo gestor de memória NTOS; O VidMm valida que tais interseções não ocorrem. Essa validação garante que o motorista não possa relatar acidentalmente uma região de memória física que esteja 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.
Testes
Se o driver optar por esse recurso, um teste HLK verificará a tabela de importação do driver para garantir que nenhuma das seguintes funções Mm seja chamada:
- MmAlocarMemóriaContígua
- 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. Dxgkrnl gerencia páginas bloqueadas para o driver. Uma vez que a memória foi remapeada, o endereço lógico das páginas fornecidas ao driver pode não corresponder mais aos endereços físicos.