IOMMU-basierte GPU-Isolation

Auf dieser Seite wird die IOMMU-basierte GPU-Isolationsfunktion für IOMMU-fähige Geräte beschrieben, die in Windows 10 Version 1803 (WDDM 2.4) eingeführt wurde. Neuere IOMMU-Updates finden Sie unter IOMMU DMA-Neuzuordnung .

Übersicht

Mit der IOMMU-basierten GPU-Isolation kann Dxgkrnl den Zugriff auf den Systemspeicher über die GPU einschränken, indem IOMMU-Hardware verwendet wird. Das Betriebssystem kann logische Adressen anstelle von physischen Adressen bereitstellen. Diese logischen Adressen können verwendet werden, um den Zugriff des Geräts auf den Systemspeicher nur auf den Arbeitsspeicher zu beschränken, auf den es zugreifen kann. Dazu wird sichergestellt, dass die IOMMU Speicherzugriffe über PCIe in gültige und zugängliche physische Seiten übersetzt.

Wenn die logische Adresse, auf die das Gerät zugreift, ungültig ist, kann das Gerät keinen Zugriff auf den physischen Arbeitsspeicher erhalten. Diese Einschränkung verhindert eine Reihe von Exploits, die es einem Angreifer ermöglichen, über ein kompromittiertes Hardwaregerät Zugriff auf physischen Arbeitsspeicher zu erhalten und den Inhalt des Systemspeichers zu lesen, der für den Betrieb des Geräts nicht benötigt wird.

Ab Windows 10 Version 1803 ist dieses Feature standardmäßig nur für PCs aktiviert, auf denen Windows Defender Application Guard für Microsoft Edge (d. a. Containervirtualisierung) aktiviert ist.

Zu Entwicklungszwecken wird die eigentliche IOMMU-Neuzuordnungsfunktion über den folgenden Registrierungsschlüssel aktiviert oder deaktiviert:

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.

Wenn dieses Feature aktiviert ist, wird die IOMMU kurz nach dem Start des Adapters aktiviert. Alle Treiberzuordnungen, die vor diesem Zeitpunkt vorgenommen wurden, werden zugeordnet, wenn sie aktiviert werden.

Wenn der Geschwindigkeits-Stagingschlüssel 14688597 als aktiviert festgelegt ist, wird die IOMMU aktiviert, wenn ein sicherer virtueller Computer erstellt wird. Derzeit ist dieser Stagingschlüssel standardmäßig deaktiviert, um das Selbsthosting ohne ordnungsgemäße IOMMU-Unterstützung zuzulassen.

Während der Aktivierung tritt beim Starten eines sicheren virtuellen Computers ein Fehler auf, wenn der Treiber keine IOMMU-Unterstützung bereitstellt.

Es gibt derzeit keine Möglichkeit, die IOMMU nach der Aktivierung zu deaktivieren.

Arbeitsspeicherzugriff

Dxgkrnl stellt sicher, dass der gesamte Speicher, auf den die GPU zugreifen kann, über die IOMMU neu zugeordnet wird, um sicherzustellen, dass auf diesen Arbeitsspeicher zugegriffen werden kann. Der physische Arbeitsspeicher, auf den die GPU zugreifen muss, kann derzeit in vier Kategorien unterteilt werden:

  • Treiberspezifische Zuordnungen, die über Funktionen im MmAllocateContiguousMemory- oder MmAllocatePagesForMdl-Stil (einschließlich SpecifyCache und erweiterte Variationen) vorgenommen werden, müssen der IOMMU zugeordnet werden, bevor GPU darauf zugreift. Anstatt die Mm-APIs aufzurufen, stellt Dxgkrnl Rückrufe für den Kernelmodustreiber bereit, um die Zuordnung und Neuzuordnung in einem Schritt zuzulassen. Jeder Speicher, der für GPU-Zugriff vorgesehen ist, muss diese Rückrufe durchlaufen, andernfalls kann die GPU nicht auf diesen Speicher zugreifen.

  • Der gesamte Speicher, auf den die GPU während Pagingvorgängen zugreift oder über gpuMmu zugeordnet wird, muss der IOMMU zugeordnet werden. Dieser Prozess ist vollständig intern im Video Memory Manager (VidMm), einem Unterkomponenten von Dxgkrnl. VidMm übernimmt die Zuordnung und das Aufheben des logischen Adressraums, wenn erwartet wird, dass die GPU auf diesen Arbeitsspeicher zugreifen wird, einschließlich:

  • Zuordnen des Sicherungsspeichers einer Zuordnung für die gesamte Dauer während einer Übertragung an oder von VRAM oder die gesamte Zeit, in der er Systemspeicher- oder Blendensegmenten zugeordnet ist.

  • Zuordnen und Aufheben überwachter Zäune.

  • Während des Energieübergangs muss der Treiber möglicherweise Teile des hardwareseitig reservierten Arbeitsspeichers speichern. Um diese Situation zu bewältigen, stellt Dxgkrnl einen Mechanismus für den Treiber bereit, um anzugeben, wie viel Arbeitsspeicher im Voraus zum Speichern dieser Daten vorhanden ist. Die genaue Menge des vom Treiber benötigten Arbeitsspeichers kann sich dynamisch ändern, aber Dxgkrnl übernimmt zum Zeitpunkt der Initialisierung des Adapters eine Commit-Gebühr für die Obergrenze, um sicherzustellen, dass physische Seiten bei Bedarf abgerufen werden können. Dxgkrnl ist dafür verantwortlich, dass dieser Speicher gesperrt und der IOMMU für die Übertragung während des Energieübergangs zugeordnet wird.

  • Für alle reservierten Hardwareressourcen stellt VidMm sicher, dass die IOMMU-Ressourcen ordnungsgemäß zugeordnet werden, bis das Gerät an die IOMMU angefügt wird. Dies schließt Arbeitsspeicher ein, der von Speichersegmenten gemeldet wird , die mit PopulatedFromSystemMemory gemeldet wurden. Für reservierten Arbeitsspeicher (z. B. firmware/BIOD reserved), der nicht über VidMm-Segmente verfügbar gemacht wird, führt Dxgkrnl einen DXGKDDI_QUERYADAPTERINFO Aufruf aus, um alle reservierten Speicherbereiche abzufragen, die der Treiber im Voraus zugeordnet werden muss. Weitere Informationen finden Sie unter Reservierter Hardwarespeicher .

Domänenzuweisung

Während der Initialisierung der Hardware erstellt Dxgkrnl eine Domäne für jeden logischen Adapter auf dem System. Die Domäne verwaltet den logischen Adressraum und verfolgt Seitentabellen und andere erforderliche Daten für die Zuordnungen nach. Alle physischen Adapter in einem einzelnen logischen Adapter gehören zur gleichen Domäne. Dxgkrnl verfolgt den gesamten zugeordneten physischen Arbeitsspeicher über die neuen Zuordnungsrückrufroutinen und alle von VidMm selbst zugewiesenen Speicher nach.

Die Domäne wird an das Gerät angefügt, wenn zum ersten Mal ein sicherer virtueller Computer erstellt wird, oder kurz nach dem Starten des Geräts, wenn der oben genannte Registrierungsschlüssel verwendet wird.

Exklusiver Zugriff

Das Anfügen und Trennen von IOMMU-Domänen ist extrem schnell, aber derzeit nicht atomar. Dies bedeutet, dass eine über PCIe ausgegebene Transaktion nicht garantiert korrekt übersetzt wird, während sie in eine IOMMU-Domäne mit unterschiedlichen Zuordnungen ausgetauscht wird.

Um diese Situation zu behandeln, muss ein KMD ab Windows 10 Version 1803 (WDDM 2.4) das folgende DDI-Paar implementieren, damit Dxgkrnl aufrufen kann:

Diese DDIs bilden eine Anfangs-End-Kopplung, bei der Dxgkrnl anfordert, dass die Hardware im Hintergrund für den Bus ist. Der Treiber muss sicherstellen, dass seine Hardware stumm ist, wenn das Gerät zu einer neuen IOMMU-Domäne gewechselt wird. Das heißt, der Treiber muss sicherstellen, dass er zwischen diesen beiden Aufrufen nicht vom Gerät liest oder in den Systemspeicher schreibt.

Zwischen diesen beiden Aufrufen übernimmt Dxgkrnl die folgenden Garantien:

  • Der Scheduler wird angehalten. Alle aktiven Workloads werden geleert, und es werden keine neuen Workloads an die Hardware gesendet oder geplant.
  • Es werden keine weiteren DDI-Aufrufe getätigt.

Im Rahmen dieser Aufrufe kann der Treiber Interrupts (einschließlich Vsync-Interrupts) für die Dauer des exklusiven Zugriffs deaktivieren und unterdrücken, auch ohne explizite Benachrichtigung vom Betriebssystem.

Dxgkrnl stellt sicher, dass alle anstehenden Arbeiten, die auf der Hardware geplant sind, abgeschlossen werden, und tritt dann in diese exklusive Zugriffsregion ein. Während dieser Zeit weist Dxgkrnl dem Gerät die Domäne zu. Dxgkrnl stellt zwischen diesen Aufrufen keine Anforderungen an den Treiber oder die Hardware.

DDI-Änderungen

Die folgenden DDI-Änderungen wurden vorgenommen, um die IOMMU-basierte GPU-Isolation zu unterstützen:

Speicherzuordnung und -zuordnung zu IOMMU

Dxgkrnl stellt die ersten sechs Rückrufe in der obigen Tabelle für den Kernelmodustreiber bereit, damit er Arbeitsspeicher zuordnen und dem logischen Adressraum der IOMMU neu zuordnen kann. Diese Rückruffunktionen imitieren die von der Mm-API-Schnittstelle bereitgestellten Routinen. Sie stellen dem Treiber MDLs oder Zeiger zur Verfügung, die Arbeitsspeicher beschreiben, der auch der IOMMU zugeordnet ist. Diese MDLs beschreiben weiterhin physische Seiten, aber der logische Adressraum der IOMMU wird an derselben Adresse zugeordnet.

Dxgkrnl verfolgt Anforderungen an diese Rückrufe nach, um sicherzustellen, dass es keine Lecks durch den Treiber gibt. Die Zuordnungsrückrufe stellen als Teil der Ausgabe ein zusätzliches Handle bereit, das an den jeweiligen kostenlosen Rückruf zurückgegeben werden muss.

Für Arbeitsspeicher, der nicht über einen der bereitgestellten Zuordnungsrückrufe zugewiesen werden kann, wird der DXGKCB_MAPMDLTOIOMMU Rückruf bereitgestellt, damit vom Treiber verwaltete MDLs nachverfolgt und mit der IOMMU verwendet werden können. Ein Treiber, der diesen Rückruf verwendet, ist dafür verantwortlich, sicherzustellen, dass die Lebensdauer der MDL den entsprechenden Aufruf zum Aufheben der Zuordnung überschreitet. Andernfalls weist der Unmap-Aufruf ein undefiniertes Verhalten auf, das zu einer kompromittierten Sicherheit der Seiten aus der MDL führen kann, die von Mm erneut verwendet werden, wenn sie nicht zugeordnet werden.

VidMm verwaltet automatisch alle Von ihm erstellten Zuordnungen (z. B. DdiCreateAllocationCb, überwachte Zäune usw.) im Systemspeicher. Der Treiber muss nichts tun, damit diese Zuordnungen funktionieren.

Framepufferreservierung

Bei Treibern, die reservierte Teile des Framepuffers während der Energieübergänge im Systemspeicher speichern müssen, übernimmt Dxgkrnl bei der Initialisierung des Adapters eine Commitgebühr für den erforderlichen Arbeitsspeicher. Wenn der Treiber die Unterstützung der IOMMU-Isolation meldet, ruft Dxgkrnl sofort nach dem Abfragen der physischen Adapterkappen einen Aufruf an DXGKDDI_QUERYADAPTERINFO mit folgendem Aus:

  • Typ ist DXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • Die Eingabe ist vom Typ UINT, d. h. der index des physischen Adapters.
  • Die Ausgabe ist vom Typ DXGK_FRAMEBUFFERSAVEAREA und sollte die maximale Größe aufweisen, die der Treiber zum Speichern des Framepufferreservebereichs bei Stromwechseln benötigt.

Dxgkrnl übernimmt eine Commit-Gebühr für den vom Treiber angegebenen Betrag, um sicherzustellen, dass es auf Anfrage immer physische Seiten erhalten kann. Diese Aktion wird ausgeführt, indem ein eindeutiges Abschnittsobjekt für jeden physischen Adapter erstellt wird, das einen nonzero-Wert für die maximale Größe angibt.

Die vom Treiber gemeldete maximale Größe muss ein Vielfaches von PAGE_SIZE sein.

Die Übertragung in und aus dem Framepuffer kann zu einem Zeitpunkt der Wahl des Treibers erfolgen. Um die Übertragung zu unterstützen, stellt Dxgkrnl die letzten vier Rückrufe in der obigen Tabelle an den Kernelmodustreiber bereit. Diese Rückrufe können verwendet werden, um die entsprechenden Teile des Abschnittsobjekts zuzuordnen, das bei der Initialisierung des Adapters erstellt wurde.

Der Treiber muss immer den hAdapter für das master/Lead-Gerät in einer LDA-Kette bereitstellen, wenn er diese vier Rückruffunktionen aufruft.

Der Treiber verfügt über zwei Optionen zum Implementieren der Framepufferreservierung:

  1. (Bevorzugte Methode) Der Treiber sollte Speicherplatz pro physischem Adapter mithilfe des oben DXGKDDI_QUERYADAPTERINFO Aufrufs zuweisen, um die Menge an Speicher anzugeben, die pro Adapter benötigt wird. Zum Zeitpunkt des Energieübergangs sollte der Treiber den Arbeitsspeicher jeweils einen physischen Adapter speichern oder wiederherstellen. Dieser Arbeitsspeicher wird auf mehrere Abschnittsobjekte aufgeteilt, eines pro physischem Adapter.

  2. Optional kann der Treiber alle Daten in einem einzelnen freigegebenen Abschnittsobjekt speichern oder wiederherstellen. Diese Aktion kann ausgeführt werden, indem sie eine einzelne große maximale Größe im DXGKDDI_QUERYADAPTERINFO Aufruf für physischen Adapter 0 und dann einen Nullwert für alle anderen physischen Adapter angeben. Der Treiber kann dann das gesamte Abschnittsobjekt einmal anheften, um es für alle Speicher-/Wiederherstellungsvorgänge für alle physischen Adapter zu verwenden. Diese Methode hat den Hauptnachteil, dass sie eine größere Menge an Arbeitsspeicher gleichzeitig sperren muss, da sie das Anheften nur eines Teilbereichs des Arbeitsspeichers in eine MDL nicht unterstützt. Daher tritt bei diesem Vorgang mit größerer Wahrscheinlichkeit ein Fehler unter Speicherdruck auf. Es wird auch erwartet, dass der Treiber die Seiten in der MDL der GPU mit den richtigen Seitenoffsets zuordnen wird.

Der Treiber sollte die folgenden Aufgaben ausführen, um eine Übertragung zum oder aus dem Framepuffer abzuschließen:

  • Während der Initialisierung sollte der Treiber mithilfe einer der Zuordnungsrückrufroutinen einen kleinen Teil des gpu-zugänglichen Arbeitsspeichers vorab zuordnen. Dieser Arbeitsspeicher wird verwendet, um den Fortschritt sicherzustellen, wenn das gesamte Abschnittsobjekt nicht gleichzeitig zugeordnet/gesperrt werden kann.

  • Zum Zeitpunkt des Energieübergangs sollte der Treiber zuerst Dxgkrnl aufrufen, um den Framepuffer anzuheften. Bei Erfolg stellt Dxgkrnl dem Treiber eine MDL für gesperrte Seiten bereit, die der IOMMU zugeordnet sind. Der Treiber kann dann eine Übertragung direkt auf diese Seiten mit den für die Hardware effizientesten Mitteln durchführen. Der Treiber sollte dann Dxgkrnl aufrufen, um den Speicher zu entsperren bzw. die Zuordnung aufzuheben.

  • Wenn Dxgkrnl nicht den gesamten Framepuffer gleichzeitig anheften kann, muss der Treiber versuchen, den Fortschritt mithilfe des vorab zugewiesenen Puffers zu erzielen, der während der Initialisierung zugewiesen wurde. In diesem Fall führt der Treiber die Übertragung in kleinen Blöcken durch. Während jeder Iteration der Übertragung (für jeden Blöcke) muss der Treiber Dxgkrnl bitten, einen zugeordneten Bereich des Abschnittsobjekts bereitzustellen, in den die Ergebnisse kopiert werden können. Der Treiber muss dann die Zuordnung des Teils des Abschnittsobjekts vor der nächsten Iteration aufheben.

Der folgende Pseudocode ist eine Beispielimplementierung dieses Algorithmus.


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

Reservierter Hardwarespeicher

VidMm ordnet den reservierten Hardwarespeicher zu, bevor das Gerät an die IOMMU angeschlossen wird.

VidMm verarbeitet automatisch alle Speicher, die als Segment mit dem Flag "PopulatedFromSystemMemory " gemeldet werden. VidMm ordnet diesen Arbeitsspeicher basierend auf der angegebenen physischen Adresse zu.

Für private Hardware reservierte Regionen, die nicht durch Segmente verfügbar gemacht werden, führt VidMm einen DXGKDDI_QUERYADAPTERINFO Aufruf aus, um die Bereiche vom Treiber abzufragen. Die bereitgestellten Bereiche dürfen keine vom NTOS-Speicher-Manager verwendeten Speicherbereiche überlappen. VidMm überprüft, ob keine solchen Überschneidungen auftreten. Diese Überprüfung stellt sicher, dass der Treiber nicht versehentlich eine Region mit physischem Arbeitsspeicher melden kann, die sich außerhalb des reservierten Bereichs befindet, was gegen die Sicherheitsgarantien des Features verstoßen würde.

Der Abfrageaufruf wird einmal ausgeführt, um die Anzahl der erforderlichen Bereiche abzufragen, gefolgt von einem zweiten Aufruf, um das Array der reservierten Bereiche aufzufüllen.

Testen

Wenn der Treiber dieses Feature aktiviert, überprüft ein HLK-Test die Importtabelle des Treibers, um sicherzustellen, dass keine der folgenden Mm-Funktionen aufgerufen wird:

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

Alle Speicherzuordnungen für zusammenhängenden Arbeitsspeicher und MDLs sollten stattdessen die Rückrufschnittstelle von Dxgkrnl mithilfe der aufgeführten Funktionen durchlaufen. Der Treiber sollte auch keinen Speicher sperren. Dxgkrnl verwaltet gesperrte Seiten für den Treiber. Nachdem der Speicher neu zugeordnet wurde, stimmt die logische Adresse der dem Treiber bereitgestellten Seiten möglicherweise nicht mehr mit den physischen Adressen überein.