IOMMU-basierte GPU-Isolation

Die IOMMU-basierte GPU-Isolation ist eine Technik, die verwendet wird, um die Systemsicherheit und -stabilität zu verbessern, indem verwaltet wird, wie GPUs auf den Systemspeicher zugreifen. Dieser Artikel beschreibt das IOMMU-basierte GPU-Isolationsfeature von WDDM für IOMMU-fähige Geräte, und wie Entwickler es in ihren Grafiktreibern implementieren können.

Diese Funktion steht ab Windows 10 Version 1803 zur Verfügung (WDDM 2.4). Weitere aktuelle IOMMU-Updates finden Sie unter IOMMU DMA-Remapping.

Übersicht

Die IOMMU-basierte GPU-Isolation ermöglicht Dxgkrnl, den Zugriff auf den Systemspeicher von der GPU aus einzuschränken, indem IOMMU-Hardware verwendet wird. Das Betriebssystem kann logische Adressen anstelle physischer 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 können soll. Dabei 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 Speicher erhalten. Diese Einschränkung verhindert eine Reihe von Exploits, die es einem Angreifer ermöglichen, über ein kompromittiertes Hardwaregerät Zugriff auf physischen Speicher zu erhalten. Ohne dies könnten Angreifer den Inhalt des Systemspeichers lesen, der für den Betrieb des Geräts nicht benötigt wird.

Standardmäßig ist dieses Feature nur für PCs aktiviert, auf denen Windows Defender Application Guard für Microsoft Edge (d. h. Containervirtualisierung) aktiviert ist.

Für Entwicklungszwecke wird die tatsächliche IOMMU-Remappingfunktion ü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 vor diesem Zeitpunkt vorgenommenen Treiberzuweisungen werden durchgeführt, wenn das Feature aktiviert wird.

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

Wenn er aktiviert ist, 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.

Speicherzugriff

Dxgkrnl stellt sicher, dass der gesamte von der GPU zugängliche Speicher über die IOMMU neu zugeordnet wird, um sicherzustellen, dass auf diesen Speicher 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 der SpecifyCache- und erweiterten Variationen) vorgenommen werden, müssen der IOMMU zugeordnet werden, bevor die GPU darauf zugreift. Anstatt die Mm-APIs aufzurufen, bietet Dxgkrnl Rückrufe zum Kernelmodustreiber, um die Zuweisung und das Remapping in einem Schritt zu ermöglichen. Jeder Speicher, auf den GPU zugreifen können soll, muss diese Rückrufe durchlaufen, oder die GPU kann nicht auf diesen Speicher zugreifen.

  • Der gesamte Speicher, auf den die GPU während des Paginierungsvorgangs zugreift oder der über die GpuMmu zugeordnet wird, muss der IOMMU zugeordnet werden. Dieser Prozess findet vollständig innerhalb des Videospeicher-Managers (VidMm) statt, der eine Unterkomponente von Dxgkrnl ist. VidMm übernimmt die Zuordnung und die Aufhebung der Zuordnung des logischen Adressraums, wenn erwartet wird, dass die GPU auf diesen Speicher zugreifen kann, einschließlich:

  • Zuordnen des Sicherungsspeichers einer Zuweisung für:

    • Die gesamte Dauer während einer Übertragung zu oder von VRAM.
    • Die gesamte Zeit, in der der Sicherungsspeicher Systemspeicher- oder Blendensegmenten zugeordnet ist.
  • Zuordnung und Aufhebung der Zuordnung überwachter Zäune.

  • Bei Leistungsübergängen muss der Treiber möglicherweise Teile des hardwaregeschützten Speichers sichern. Um damit umzugehen, stellt Dxgkrnl einen Mechanismus für den Treiber bereit, um anzugeben, wie viel Arbeitsspeicher im Vorfeld zum Speichern dieser Daten bereitsteht. Die genaue Speichermenge, die der Treiber benötigt, kann dynamisch geändert werden. Das heißt, Dxgkrnl übernimmt eine Commit-Last für die obere Grenze zum Zeitpunkt der Initialisierung des Adapters, um sicherzustellen, dass physische Seiten bei Bedarf abgerufen werden können. Dxgkrnl ist dafür verantwortlich, sicherzustellen, dass dieser Speicher gesperrt und die IOMMU für die Übertragung während der Leistungsübergänge zugeordnet ist.

  • Für alle hardwarereservierten Ressourcen stellt VidMm sicher, dass die IOMMU-Ressourcen ordnungsgemäß zugeordnet werden, wenn das Gerät an die IOMMU angefügt ist. Dies schließt den Speicher ein, der von Speichersegmenten gemeldet wurde, die mit PopulatedFromSystemMemory gemeldet wurden. Für reservierten Speicher (z. B. firmware-/BIOD-reserviert), der nicht über VidMm-Segmente verfügbar gemacht wird, führt Dxgkrnl einen DXGKDDI_QUERYADAPTERINFO-Aufruf durch, um alle reservierten Speicherbereiche abzufragen, die für den Treiber vorab zugeordnet werden müssen. Einzelheiten finden Sie unter Hardwarereservierter Arbeitsspeicher.

Domänenzuweisung

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

Die Domäne wird beim ersten Erstellen eines sicheren virtuellen Computers oder kurz nach dem Starten des Geräts an das Gerät angefügt, wenn der obige Registrierungsschlüssel verwendet wird.

Exklusiver Zugriff

IOMMU Domain Attach und Detach ist schnell, aber dennoch derzeit nicht atomar. Dies Bedingung bedeutet, dass eine über PCIe ausgegebene Transaktion nicht garantiert korrekt übersetzt wird, während sie mit unterschiedlichen Zuordnungen in eine IOMMU-Domäne umgetauscht wird.

Um diese Situation zu behandeln, muss ein KMD ab Windows 10, Version 1803 (WDDM 2.4), das folgende DDI-Paar für Dxgkrnl implementieren, um aufzurufen:

Diese DDIs bilden eine Anfangs-/Endkopplung, bei der Dxgkrnl anfordert, dass die Hardware über den Bus automatisch ausgeführt wird. Der Treiber muss sicherstellen, dass die Hardware automatisch ausgeführt wird, wenn das Gerät zu einer neuen IOMMU-Domäne wechselt. Das heißt, der Treiber muss sicherstellen, dass der Systemspeicher zwischen diesen beiden Aufrufen nicht vom Gerät aus gelesen oder in den Systemspeicher geschrieben wird.

Zwischen diesen beiden Aufrufen stellt Dxgkrnl die folgenden Garantien dar:

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

Im Rahmen dieser Aufrufe kann sich der Treiber entscheiden, Unterbrechungen (einschließlich Vsync-Interrupts) während des exklusiven Zugriffs zu deaktivieren und zu unterdrücken, auch ohne explizite Benachrichtigung vom Betriebssystem.

Dxgkrnl stellt sicher, dass alle ausstehenden Arbeiten, die auf der Hardware geplant sind, abgeschlossen sind, und wechselt dann zu dieser exklusiven Zugriffsregion. Während dieser Zeit weist Dxgkrnl die Domäne dem Gerät zu. Dxgkrnl sendet keine Anforderungen des Treibers oder der Hardware zwischen diesen Aufrufen.

DDI-Änderungen

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

Speicherzuweisung und Zuordnung zu IOMMU

Dxgkrnl stellt die ersten sechs Rückrufe in der vorherigen Tabelle dem Kernelmodustreiber bereit, damit er Speicher zuweisen und diesen dem logischen Adressraum der IOMMU neu zuordnen kann. Diese Rückruffunktionen imitieren die Routinen, die von der Mm-API-Schnittstelle bereitgestellt werden. Sie stellen dem Treiber MDLs oder Zeiger zur Verfügung, die Speicher beschreiben, der ebenfalls 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 der Treiber keine Lecks enthält. Die Zuordnungsrückrufe stellen ein weiteres Handle als Teil der Ausgabe bereit, die wieder zum jeweiligen freien Rückruf bereitgestellt werden muss.

Für Speicher, der nicht über einen der bereitgestellten Zuordnungsrückrufe zugewiesen werden kann, wird der DXGKCB_MAPMDLTOIOMMU-Rückruf bereitgestellt, um treiberverwaltete MDLs zu verfolgen und mit der IOMMU zu verwenden. Ein Treiber, der diesen Rückruf verwendet, ist dafür verantwortlich, sicherzustellen, dass die Lebensdauer der MDL den entsprechenden Unmap-Aufruf überschreitet. Andernfalls zeigt der Unmap-Aufruf ein nicht definiertes Verhalten. Dieses undefinierte Verhalten kann zu Sicherheitsproblemen bei den MDL-Seiten führen, die Mm zum Zeitpunkt der Aufhebung ihrer Zuordnung einem neuen Zweck zuführt.

VidMm verwaltet automatisch alle von ihm erzeugten Zuordnungen (z. B. DdiCreateAllocationCb, überwachte Zäune usw.) im Systemspeicher. Der Treiber muss nichts tun, um diese Zuordnungen vorzunehmen.

Framepuffer-Reservierung

Bei Treibern, die reservierte Teile des Framepuffers während Leistungsübergängen im Systemspeicher speichern müssen, übernimmt Dxgkrnl beim Initialisieren des Adapters eine Commit-Last für den erforderlichen Arbeitsspeicher. Wenn der Treiber IOMMU-Isolationsunterstützung meldet, gibt, Dxgkrnl sofort nach der Abfrage der physischen Adapter-Caps einen Aufruf an DXGKDDI_QUERYADAPTERINFO mit Folgendem aus:

  • Typ ist DXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • Die Eingabe hat den Typ UINT, d. h. der Index des physischen Adapters.
  • Die Ausgabe hat den Typ DXGK_FRAMEBUFFERSAVEAREA und sollte die maximale Größe haben, die vom Treiber benötigt wird, um den Framepuffer-Reservebereich bei Leistungsübergängen zu speichern.

Dxgkrnl übernimmt eine Commit-Last für die vom Treiber angegebene Menge, um sicherzustellen, dass auf Anfrage immer physische Seiten erhalten werden können. Diese Aktion erfolgt durch Erstellen eines eindeutigen Abschnittsobjekts für jeden physischen Adapter, der einen Wert ungleich Null für die maximale Größe angibt.

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

Die Durchführung der Übertragung zum und vom Framepuffer kann zu einem vom Treiber gewählten Zeitpunkt erfolgen. Um die Übertragung zu unterstützen, stellt Dxgkrnl die letzten vier Rückrufe in der vorherigen Tabelle für den Kernelmodustreiber bereit. Diese Rückrufe können verwendet werden, um die entsprechenden Teile des Abschnittsobjekts zuzuordnen, die beim Initialisieren des Adapters erstellt wurden.

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

Der Treiber hat zwei Optionen zum Implementieren der Framepuffer-Reservierung:

  1. (Bevorzugte Methode) Der Treiber sollte Speicherplatz pro physischem Adapter mithilfe des DXGKDDI_QUERYADAPTERINFO-Aufrufs zuweisen, um den pro Adapter benötigten Speicherplatz anzugeben. Zum Zeitpunkt des Leistungsübergangs sollte der Treiber den Speicher jeweils eines physischen Adapters speichern oder wiederherstellen. Dieser Speicher 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 durchgeführt werden, indem eine einzelne große maximale Größe im DXGKDDI_QUERYADAPTERINFO-Aufruf für physische Adapter 0 und dann ein Nullwert für alle anderen physischen Adapter angegeben wird. Der Treiber kann dann das gesamte Abschnittsobjekt einmal für alle Speicher-/Wiederherstellungsvorgänge für alle physischen Adapter anheften. Diese Methode hat hauptsächlich den Nachteil, dass sie gleichzeitig eine größere Speichermenge sperren muss, da das Anheften nur eines Unterbereichs des Speichers in eine MDL nicht unterstützt wird. Daher schlägt dieser Vorgang unter Speicherdruck eher fehl. Es würde auch erwartet, dass der Treibe die Seiten in der MDL der GPU mithilfe der richtigen Seitenoffsets zuordnet.

Der Treiber sollte die folgenden Vorgänge durchführen, um eine Übertragung zum oder vom Framepuffer auszuführen:

  • Während der Initialisierung sollte der Treiber mithilfe einer der Zuordnungsrückrufroutinen einen kleinen Teil des GPU-Speichers vorab zuweisen, auf den zugegriffen werden kann. Dieser Speicher wird verwendet, um den Vorwärtsfortschritt sicherzustellen, wenn das gesamte Abschnittsobjekt nicht gleichzeitig zugeordnet/gesperrt werden kann.

  • Zum Zeitpunkt des Leistungsübergangs sollte der Treiber zuerst Dxgkrnl aufrufen, um den Framepuffer anzuheften. Wenn dies erfolgreich ist, stellt Dxgkrnl dem Treiber eine MDL für gesperrte Seiten bereit, die der IOMMU zugeordnet sind. Der Treiber kann dann direkt auf diese Seiten übertragen, was für die Hardware am effizientesten ist. Der Treiber sollte dann Dxgkrnl aufrufen, um den Speicher zu entsperren bzw. seine Zuordnung aufzuheben.

  • Wenn Dxgkrnl nicht den gesamten Framepuffer gleichzeitig anheften kann, muss der Treiber versuchen, den Fortschritt mithilfe des während der Initialisierung zugewiesenen vorab zugewiesenen Puffers vorwärts zu bringen. In diesem Fall führt der Treiber die Übertragung in kleinen Blöcken durch. Während jeder Iteration der Übertragung (für jeden Block) muss der Treiber Dxgkrnl auffordern, 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 ein Beispiel für die Implementierung 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;
        }
    }
}

Hardwarereservierter Speicher

VidMm ordnet hardwarereservierten Speicher zu, bevor das Gerät an die IOMMU angeschlossen wird.

VidMm behandelt automatisch allen als Segment gemeldeten Speicher mit dem Flag PopulatedFromSystemMemory. VidMm ordnet diesen Speicher basierend auf der bereitgestellten physischen Adresse zu.

Für private hardwarereservierte Regionen, die nicht durch Segmente verfügbar gemacht werden, führt VidMm einen DXGKDDI_QUERYADAPTERINFO-Aufruf aus, um die Bereiche des Treibers abzufragen. Die bereitgestellten Bereiche dürfen keine vom NTOS-Speicher-Manager verwendeten Speicherbereiche überlappen; VidMm überprüft, ob solche Überschneidungen auftreten. Diese Überprüfung stellt sicher, dass der Treiber nicht versehentlich einen Bereich des physischen Speichers melden kann, der 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 reservierter Bereiche aufzufüllen.

Testen

Wenn sich der Treiber für dieses Feature anmeldet, ü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 Speicherzuweisungen für zusammenhängenden Speicher 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 vom Treiber bereitgestellten Seiten möglicherweise nicht mehr mit den physischen Adressen überein.