Isolamento GPU basato su IOMMU

L'isolamento GPU basato su IOMMU è una tecnica usata per migliorare la sicurezza e la stabilità del sistema gestendo il modo in cui le GPU accedono alla memoria del sistema. Questo articolo descrive la funzionalità di isolamento GPU basata su IOMMU di WDDM per i dispositivi con supporto per IOMMU e come gli sviluppatori possono implementarla nei driver di grafica.

Questa funzionalità è disponibile a partire da Windows 10 versione 1803 (WDDM 2.4). Per gli aggiornamenti più recenti di IOMMU, vedere IOMMU DMA remapping .See IOMMU DMA remapping for more recent IOMMU updates.See IOMMU DMA remapping for more recent IOMMU updates

Panoramica

L'isolamento GPU basato su IOMMU consente a Dxgkrnl di limitare l'accesso alla memoria di sistema dalla GPU usando hardware IOMMU. Il sistema operativo può fornire indirizzi logici anziché indirizzi fisici. Questi indirizzi logici possono essere usati per limitare l'accesso del dispositivo alla memoria di sistema solo alla memoria a cui può accedere. Ciò avviene assicurando che L'IOMMU converta gli accessi alla memoria tramite PCIe in pagine fisiche valide e accessibili.

Se l'indirizzo logico a cui si accede dal dispositivo non è valido, il dispositivo non può accedere alla memoria fisica. Questa restrizione impedisce una serie di exploit che consentono a un utente malintenzionato di accedere alla memoria fisica tramite un dispositivo hardware compromesso. Senza di esso, gli utenti malintenzionati potrebbero leggere il contenuto della memoria di sistema non necessaria per il funzionamento del dispositivo.

Per impostazione predefinita, questa funzionalità è abilitata solo per i PC in cui Windows Defender Application Guard è abilitato per Microsoft Edge, ovvero la virtualizzazione dei contenitori.

A scopo di sviluppo, la funzionalità di modifica del mapping di IOMMU effettiva è abilitata o disabilitata tramite la chiave del Registro di sistema seguente:

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 questa funzionalità è abilitata, l'IOMMU viene abilitato poco dopo l'avvio dell'adattatore. Tutte le allocazioni di driver effettuate prima di questo momento vengono mappate quando viene abilitato.

Inoltre, se la chiave di gestione temporanea della velocità 14688597 è impostata come abilitata, l'IOMMU viene attivato quando viene creata una macchina virtuale sicura. Per il momento, questa chiave di staging è disabilitata per impostazione predefinita per consentire l'hosting automatico senza il supporto IOMMU appropriato.

Durante l'abilitazione, l'avvio di una macchina virtuale sicura ha esito negativo se il driver non fornisce supporto IOMMU.

Attualmente non è possibile disabilitare l'IOMMU dopo l'abilitazione.

Accesso alla memoria

Dxgkrnl garantisce che tutta la memoria accessibile dalla GPU venga mappata tramite IOMMU per garantire che questa memoria sia accessibile. La memoria fisica a cui deve accedere la GPU può attualmente essere suddivisa in quattro categorie:

  • Le allocazioni specifiche del driver eseguite tramite MmAllocateContiguousMemory- o MmAllocatePagesForMdl-style (incluse le varianti SpecifyCache e estese) devono essere mappate all'IOMMU prima di accedervi alla GPU. Invece di chiamare le API Mm , Dxgkrnl fornisce callback al driver in modalità kernel per consentire l'allocazione e il mapping in un unico passaggio. Qualsiasi memoria destinata a essere accessibile tramite GPU deve passare attraverso questi callback o la GPU non è in grado di accedere a questa memoria.

  • Tutta la memoria a cui si accede dalla GPU durante le operazioni di paging o mappata tramite GpuMmu deve essere mappata all'IOMMU. Questo processo è interamente interno al Video Memory Manager (VidMm), che è un sottocomponente di Dxgkrnl. VidMm gestisce il mapping e il mapping dello spazio indirizzi logico ogni volta che la GPU deve accedere a questa memoria, tra cui:

  • Mapping dell'archivio di backup di un'allocazione per:

    • Durata intera durante un trasferimento da o verso VRAM.
    • Tempo intero in cui viene eseguito il mapping dell'archivio di backup ai segmenti di memoria o apertura di sistema.
  • Mapping e annullamento del mapping delle recinzioni monitorate.

  • Durante le transizioni di alimentazione, il driver potrebbe dover salvare parti della memoria riservata dall'hardware. Per gestire questa situazione, Dxgkrnl fornisce un meccanismo per il driver per specificare la quantità di memoria in anticipo per archiviare questi dati. La quantità esatta di memoria richiesta dal driver può cambiare dinamicamente. Detto questo, Dxgkrnl prende un addebito di commit sul limite superiore al momento dell'inizializzazione dell'adattatore per garantire che le pagine fisiche possano essere ottenute quando necessario. Dxgkrnl è responsabile di garantire che questa memoria sia bloccata e mappata all'IOMMU per il trasferimento durante le transizioni di alimentazione.

  • Per tutte le risorse riservate hardware, VidMm garantisce che esenesse correttamente le risorse IOMMU al momento in cui il dispositivo è collegato all'IOMMU. Ciò include la memoria segnalata dai segmenti di memoria segnalati con PopulatedFromSystemMemory. Per la memoria riservata (ad esempio, firmware/BIOD riservato) non esposta tramite segmenti VidMm, Dxgkrnl effettua una chiamata DXGKDDI_QUERYADAPTERINFO per eseguire una query su tutti gli intervalli di memoria riservati necessari al driver in anticipo. Per informazioni dettagliate, vedere Memoria riservata hardware.

Assegnazione di dominio

Durante l'inizializzazione dell'hardware, Dxgkrnl crea un dominio per ogni scheda logica nel sistema. Il dominio gestisce lo spazio degli indirizzi logici e tiene traccia delle tabelle di pagine e di altri dati necessari per i mapping. Tutti gli adattatori fisici in una singola scheda logica appartengono allo stesso dominio. Dxgkrnl tiene traccia di tutta la memoria fisica mappata attraverso le nuove routine di callback di allocazione e qualsiasi memoria allocata da VidMm stesso.

Il dominio verrà collegato al dispositivo la prima volta che viene creata una macchina virtuale sicura o poco dopo l'avvio del dispositivo se viene usata la chiave del Registro di sistema precedente.

Accesso esclusivo

Il collegamento e lo scollegamento del dominio IOMMU sono veloci, ma attualmente non è atomico. Poiché non è atomica, non è garantita la conversione corretta di una transazione eseguita su PCIe durante lo scambio in un dominio IOMMU con mapping diversi.

Per gestire questa situazione, a partire da Windows 10 versione 1803 (WDDM 2.4), un KMD deve implementare la coppia DDI seguente per Dxgkrnl per chiamare:

Queste DDI formano un'associazione di inizio/fine, in cui Dxgkrnl richiede che l'hardware sia invisibile all'utente sul bus. Il driver deve assicurarsi che l'hardware sia invisibile all'utente ogni volta che il dispositivo viene passato a un nuovo dominio IOMMU. Ovvero, il driver deve assicurarsi che non legga o scriva nella memoria di sistema dal dispositivo tra queste due chiamate.

Tra queste due chiamate, Dxgkrnl garantisce quanto segue:

  • L'utilità di pianificazione è sospesa. Tutti i carichi di lavoro attivi vengono scaricati e non vengono inviati nuovi carichi di lavoro o pianificati nell'hardware.
  • Non vengono effettuate altre chiamate DDI.

Come parte di queste chiamate, il driver può scegliere di disabilitare e eliminare gli interrupt (inclusi gli interrupt Vsync) durante l'accesso esclusivo, anche senza notifiche esplicite dal sistema operativo.

Dxgkrnl garantisce che qualsiasi lavoro in sospeso pianificato sull'hardware venga completato e quindi entra in questa area di accesso esclusivo. Durante questo periodo, Dxgkrnl assegna il dominio al dispositivo. Dxgkrnl non effettua alcuna richiesta del driver o dell'hardware tra queste chiamate.

Modifiche DDI

Sono state apportate le modifiche DDI seguenti per supportare l'isolamento GPU basato su IOMMU:

Allocazione di memoria e mapping a IOMMU

Dxgkrnl fornisce i primi sei callback nella tabella precedente al driver in modalità kernel per consentire l'allocazione della memoria e il nuovo mapping allo spazio indirizzi logico di IOMMU. Queste funzioni di callback simulano le routine fornite dall'interfaccia DELL'API Mm . Forniscono al driver mdls o puntatori che descrivono la memoria mappata anche all'IOMMU. Questi mdls continuano a descrivere le pagine fisiche, ma lo spazio indirizzi logico di IOMMU viene mappato nello stesso indirizzo.

Dxgkrnl tiene traccia delle richieste a questi callback per garantire che non ci siano perdite dal driver. I callback di allocazione forniscono un altro handle come parte dell'output che deve essere fornito al rispettivo callback gratuito.

Per la memoria che non può essere allocata tramite uno dei callback di allocazione forniti, il callback DXGKCB_MAPMDLTOIOMMU viene fornito per consentire il rilevamento e l'uso di MDLS gestiti dal driver con IOMMU. Un driver che usa questo callback è responsabile di garantire che la durata del file MDL superi la chiamata unmap corrispondente. In caso contrario, la chiamata unmap ha un comportamento non definito. Questo comportamento non definito può causare una compromissione della sicurezza delle pagine MDL riutilizzate da Mm al momento dell'annullamento del mapping.

VidMm gestisce automaticamente tutte le allocazioni create (ad esempio, DdiCreateAllocationCb, recinzioni monitorate e così via) nella memoria di sistema. Il driver non deve eseguire alcuna operazione per far funzionare queste allocazioni.

Prenotazione del buffer di frame

Per i driver che devono salvare parti riservate del buffer dei fotogrammi nella memoria di sistema durante le transizioni di alimentazione, Dxgkrnl assume un addebito di commit sulla memoria necessaria quando viene inizializzato l'adattatore. Se il driver segnala il supporto per l'isolamento IOMMU, Dxgkrnl emetterà una chiamata a DXGKDDI_QUERYADAPTERINFO con i seguenti immediatamente dopo aver eseguito una query sui limiti dell'adattatore fisico:

  • Il tipo è DXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • L'input è di tipo UINT, ovvero l'indice dell'adattatore fisico.
  • L'output è del tipo DXGK_FRAMEBUFFERSAVEAREA e deve essere la dimensione massima richiesta dal driver per salvare l'area di riserva del buffer dei fotogrammi durante le transizioni di alimentazione.

Dxgkrnl accetta un addebito di commit sull'importo specificato dal driver per assicurarsi che possa sempre ottenere pagine fisiche su richiesta. Questa azione viene eseguita creando un oggetto sezione univoco per ogni adattatore fisico che specifica un valore diverso da zero per le dimensioni massime.

Le dimensioni massime segnalate dal driver devono essere multiple di PAGE_SIZE.

L'esecuzione del trasferimento da e verso il buffer dei fotogrammi può essere eseguita alla volta della scelta del driver. Per facilitare il trasferimento, Dxgkrnl fornisce gli ultimi quattro callback nella tabella precedente al driver in modalità kernel. Questi callback possono essere utilizzati per eseguire il mapping delle parti appropriate dell'oggetto sezione creato durante l'inizializzazione dell'adapter.

Il driver deve sempre fornire hAdapter per il dispositivo lead in una catena LDA quando chiama queste quattro funzioni di callback.

Il driver include due opzioni per implementare la prenotazione del buffer dei frame:

  1. (Metodo Preferred) Il driver deve allocare spazio per ogni scheda fisica usando la chiamata DXGKDDI_QUERYADAPTERINFO per specificare la quantità di spazio di archiviazione necessaria per ogni adattatore. Al momento della transizione di alimentazione, il driver deve salvare o ripristinare la memoria una scheda fisica alla volta. Questa memoria è suddivisa tra più oggetti sezione, uno per ogni adattatore fisico.

  2. Facoltativamente, il driver può salvare o ripristinare tutti i dati in un singolo oggetto sezione condiviso. Questa azione può essere eseguita specificando una singola dimensione massima elevata nella DXGKDDI_QUERYADAPTERINFO chiamata per l'adattatore fisico 0 e quindi un valore zero per tutte le altre schede fisiche. Il driver può quindi aggiungere l'intero oggetto sezione una volta per l'uso in tutte le operazioni di salvataggio/ripristino, per tutte le schede fisiche. Questo metodo presenta lo svantaggio principale che richiede il blocco di una quantità maggiore di memoria contemporaneamente, perché non supporta l'aggiunta di un solo intervallo secondario della memoria in un file MDL. Di conseguenza, è più probabile che questa operazione abbia esito negativo sotto pressione sulla memoria. Il driver dovrebbe anche eseguire il mapping delle pagine nel file MDL alla GPU usando gli offset di pagina corretti.

Il driver deve eseguire le attività seguenti per completare un trasferimento da o verso il buffer dei frame:

  • Durante l'inizializzazione, il driver deve preallocare un piccolo blocco di memoria accessibile dalla GPU usando una delle routine di callback di allocazione. Questa memoria viene usata per garantire lo stato di avanzamento se l'intero oggetto sezione non può essere mappato/bloccato contemporaneamente.

  • Al momento della transizione di alimentazione, il driver deve prima chiamare Dxgkrnl per bloccare il buffer dei fotogrammi. In caso di esito positivo, Dxgkrnl fornisce al driver un MDL per bloccare le pagine mappate all'IOMMU. Il driver può quindi eseguire un trasferimento direttamente a queste pagine in qualsiasi modo sia più efficiente per l'hardware. Il driver dovrebbe quindi chiamare Dxgkrnl per sbloccare/rimuovere il mapping della memoria.

  • Se Dxgkrnl non è in grado di bloccare l'intero buffer dei fotogrammi contemporaneamente, il driver deve tentare di avanzare usando il buffer preallocato allocato durante l'inizializzazione. In questo caso, il driver esegue il trasferimento in piccoli blocchi. Durante ogni iterazione del trasferimento (per ogni blocco), il driver deve chiedere a Dxgkrnl di fornire un intervallo mappato dell'oggetto sezione in cui possono copiare i risultati. Il driver deve quindi rimuovere il mapping della parte dell'oggetto sezione prima dell'iterazione successiva.

Lo pseudocodice seguente è un esempio di implementazione di questo 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;
        }
    }
}

Memoria riservata hardware

VidMm esegue il mapping della memoria riservata hardware prima che il dispositivo sia collegato all'IOMMU.

VidMm gestisce automaticamente qualsiasi memoria segnalata come segmento con il flag PopulatedFromSystemMemory. VidMm esegue il mapping di questa memoria in base all'indirizzo fisico specificato.

Per le aree riservate dell'hardware privato non esposte da segmenti, VidMm effettua una chiamata DXGKDDI_QUERYADAPTERINFO per eseguire query negli intervalli dal driver. Gli intervalli forniti non devono sovrapporsi ad alcuna area di memoria utilizzata dal gestore di memoria NTOS; VidMm convalida che non si verifichino intersezioni di questo tipo. Questa convalida garantisce che il driver non possa segnalare accidentalmente un'area di memoria fisica esterna all'intervallo riservato, che viola le garanzie di sicurezza della funzionalità.

La chiamata di query viene eseguita una volta per eseguire una query sul numero di intervalli necessari e viene seguita da una seconda chiamata per popolare la matrice di intervalli riservati.

Test

Se il driver acconsente a questa funzionalità, un test HLK analizza la tabella di importazione del driver per assicurarsi che nessuna delle funzioni Mm seguenti venga chiamata:

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

Tutte le allocazioni di memoria per la memoria contigua e gli mdls devono invece passare attraverso l'interfaccia di callback di Dxgkrnl usando le funzioni elencate. Il driver non deve anche bloccare alcuna memoria. Dxgkrnl gestisce le pagine bloccate per il driver. Dopo il mapping della memoria, l'indirizzo logico delle pagine fornite al driver potrebbe non corrispondere più agli indirizzi fisici.