Isolation GPU basée sur IOMMU

Cette page décrit la fonctionnalité d’isolation GPU basée sur IOMMU pour les appareils compatibles IOMMU, introduite dans Windows 10 version 1803 (WDDM 2.4). Pour plus d’informations sur les mises à jour de l’IOMMU, consultez Le remapping IOMMU DMA .

Vue d’ensemble

L’isolation GPU basée sur IOMMU permet à Dxgkrnl de restreindre l’accès à la mémoire système à partir du GPU en utilisant du matériel IOMMU. Le système d’exploitation peut fournir des adresses logiques au lieu d’adresses physiques. Ces adresses logiques peuvent être utilisées pour restreindre l’accès de l’appareil à la mémoire système uniquement à la mémoire à laquelle il doit pouvoir accéder. Pour ce faire, l’IOMMU traduit les accès à la mémoire sur PCIe en pages physiques valides et accessibles.

Si l’adresse logique accessible par l’appareil n’est pas valide, l’appareil ne peut pas accéder à la mémoire physique. Cette restriction empêche toute une série d’attaques qui permettent à un attaquant d’accéder à la mémoire physique via un appareil matériel compromis et de lire le contenu de la mémoire système qui n’est pas nécessaire au fonctionnement de l’appareil.

À compter de Windows 10 version 1803, cette fonctionnalité est activée par défaut uniquement pour les PC où Windows Defender Protection d'application est activé pour Microsoft Edge (autrement dit, la virtualisation de conteneurs).

À des fins de développement, la fonctionnalité de remapping IOMMU réelle est activée ou désactivée via la clé de Registre suivante :

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.

Si cette fonctionnalité est activée, l’IOMMU est activé peu après le démarrage de l’adaptateur. Toutes les allocations de pilotes effectuées avant cette heure sont mappées quand elle est activée.

En outre, si le 14688597 de la clé de préproduction de vélocité est défini comme activé, l’IOMMU est activé lors de la création d’une machine virtuelle sécurisée. Pour l’instant, cette clé intermédiaire est désactivée par défaut pour autoriser l’auto-hébergement sans prise en charge appropriée de l’IOMMU.

Alors qu’il est activé, le démarrage d’une machine virtuelle sécurisée échoue si le pilote ne fournit pas de support IOMMU.

Il n’existe actuellement aucun moyen de désactiver l’IOMMU une fois qu’il a été activé.

Accès à la mémoire

Dxgkrnl garantit que toute la mémoire accessible par le GPU est remappée via l’IOMMU pour garantir que cette mémoire est accessible. La mémoire physique à laquelle le GPU doit accéder peut actuellement être divisée en quatre catégories :

  • Les allocations spécifiques aux pilotes effectuées via les fonctions de style MmAllocateContiguousMemory ou MmAllocatePagesForMdl (y compris specifyCache et les variantes étendues) doivent être mappées à l’IOMMU avant d’y accéder par GPU. Au lieu d’appeler les API Mm , Dxgkrnl fournit des rappels au pilote en mode noyau pour autoriser l’allocation et le remapping en une seule étape. Toute mémoire destinée à être accessible par GPU doit passer par ces rappels, sinon le GPU n’est pas en mesure d’accéder à cette mémoire.

  • Toute la mémoire accessible par le GPU pendant les opérations de pagination, ou mappée via gpuMmu doit être mappée à l’IOMMU. Ce processus est entièrement interne à Video Memory Manager (VidMm), qui est un sous-composant de Dxgkrnl. VidMm gère le mappage et le démappage de l’espace d’adressage logique chaque fois que le GPU est censé accéder à cette mémoire, notamment :

  • Mappage du magasin de stockage d’une allocation pendant toute la durée d’un transfert vers ou depuis VRAM, ou pendant toute la durée pendant laquelle elle est mappée à la mémoire système ou aux segments d’ouverture.

  • Mappage et démappage des clôtures surveillées.

  • Pendant les transitions d’alimentation, le pilote peut avoir besoin d’économiser des parties de la mémoire réservée au matériel. Pour gérer cette situation, Dxgkrnl fournit un mécanisme permettant au pilote de spécifier la quantité de mémoire à l’avance pour stocker ces données. La quantité exacte de mémoire requise par le pilote peut changer dynamiquement, mais Dxgkrnl prend des frais de validation sur la limite supérieure au moment de l’initialisation de l’adaptateur pour s’assurer que les pages physiques peuvent être obtenues lorsque cela est nécessaire. Dxgkrnl est chargé de s’assurer que cette mémoire est verrouillée et mappée à l’IOMMU pour le transfert pendant les transitions d’alimentation.

  • Pour toutes les ressources matérielles réservées, VidMm s’assure qu’il mappe correctement les ressources IOMMU au moment où l’appareil est attaché à l’IOMMU. Cela inclut la mémoire signalée par les segments de mémoire signalés avec PopulatedFromSystemMemory. Pour la mémoire réservée (par exemple, le microprogramme/la réserve BIOD) qui n’est pas exposée via des segments VidMm, Dxgkrnl effectue un appel DXGKDDI_QUERYADAPTERINFO pour interroger toutes les plages de mémoire réservées dont le pilote a besoin mappée à l’avance. Pour plus d’informations, consultez Mémoire réservée matérielle .

Attribution de domaine

Pendant l’initialisation du matériel, Dxgkrnl crée un domaine pour chaque adaptateur logique sur le système. Le domaine gère l’espace d’adressage logique et suit les tables de pages et d’autres données nécessaires pour les mappages. Toutes les cartes physiques d’un adaptateur logique unique appartiennent au même domaine. Dxgkrnl suit toute la mémoire physique mappée via les nouvelles routines de rappel d’allocation et toute mémoire allouée par VidMm lui-même.

Le domaine est attaché à l’appareil la première fois qu’une machine virtuelle sécurisée est créée, ou peu de temps après le démarrage de l’appareil si la clé de Registre ci-dessus est utilisée.

Accès exclusif

L’attachement et le détachement du domaine IOMMU sont extrêmement rapides, mais ne sont pas atomiques actuellement. Cela signifie qu’une transaction émise sur PCIe n’est pas garantie d’être correctement traduite lors de l’échange vers un domaine IOMMU avec des mappages différents.

Pour gérer cette situation, à compter de Windows 10 version 1803 (WDDM 2.4), un KMD doit implémenter la paire DDI suivante pour que Dxgkrnl appelle :

Ces DDIs forment un appairage début/fin, où Dxgkrnl demande que le matériel soit silencieux sur le bus. Le pilote doit s’assurer que son matériel est silencieux chaque fois que l’appareil est basculé vers un nouveau domaine IOMMU. Autrement dit, le pilote doit s’assurer qu’il ne lit ni n’écrit dans la mémoire système à partir de l’appareil entre ces deux appels.

Entre ces deux appels, Dxgkrnl offre les garanties suivantes :

  • Le planificateur est suspendu. Toutes les charges de travail actives sont vidées et aucune nouvelle charge de travail n’est envoyée ou planifiée sur le matériel.
  • Aucun autre appel DDI n’est effectué.

Dans le cadre de ces appels, le pilote peut choisir de désactiver et de supprimer les interruptions (y compris les interruptions Vsync) pendant la durée de l’accès exclusif, même sans notification explicite du système d’exploitation.

Dxgkrnl garantit que tout travail en attente planifié sur le matériel se termine, puis entre dans cette région d’accès exclusif. Pendant ce temps, Dxgkrnl affecte le domaine à l’appareil. Dxgkrnl n’effectue aucune demande du pilote ou du matériel entre ces appels.

Modifications DDI

Les modifications DDI suivantes ont été apportées pour prendre en charge l’isolation GPU basée sur IOMMU :

Allocation de mémoire et mappage à IOMMU

Dxgkrnl fournit les six premiers rappels du tableau ci-dessus au pilote en mode noyau pour lui permettre d’allouer de la mémoire et de la remapper à l’espace d’adressage logique de l’IOMMU. Ces fonctions de rappel imitent les routines fournies par l’interface API Mm . Ils fournissent au pilote des MDL, ou des pointeurs qui décrivent la mémoire qui est également mappée à l’IOMMU. Ces dll MDL continuent de décrire des pages physiques, mais l’espace d’adressage logique de l’IOMMU est mappé à la même adresse.

Dxgkrnl suit les demandes adressées à ces rappels pour s’assurer qu’il n’y a pas de fuites par le pilote. Les rappels d’allocation fournissent un handle supplémentaire dans le cadre de la sortie qui doit être fourni au rappel gratuit respectif.

Pour la mémoire qui ne peut pas être allouée via l’un des rappels d’allocation fournis, le rappel DXGKCB_MAPMDLTOIOMMU est fourni pour permettre le suivi et l’utilisation des dll MDL gérées par le pilote avec l’IOMMU. Un pilote qui utilise ce rappel est chargé de s’assurer que la durée de vie du MDL dépasse l’appel unmap correspondant. Sinon, l’appel de non-carte a un comportement non défini qui peut entraîner une sécurité compromise des pages du MDL qui sont réaffectées par Mm au moment où elles sont démassées.

VidMm gère automatiquement toutes les allocations qu’il crée (par exemple, DdiCreateAllocationCb, clôtures surveillées, etc.) dans la mémoire système. Le pilote n’a pas besoin de faire quoi que ce soit pour que ces allocations fonctionnent.

Réservation de mémoire tampon de trame

Pour les pilotes qui doivent enregistrer des parties réservées de la mémoire tampon de trame dans la mémoire système pendant les transitions d’alimentation, Dxgkrnl prend une charge de validation sur la mémoire requise lors de l’initialisation de l’adaptateur. Si le pilote signale la prise en charge de l’isolation IOMMU, Dxgkrnl émettra un appel à DXGKDDI_QUERYADAPTERINFO avec les éléments suivants immédiatement après avoir interrogé les majuscules de l’adaptateur physique :

  • Le type est DXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • L’entrée est de type UINT, qui est l’index de l’adaptateur physique.
  • La sortie est du type DXGK_FRAMEBUFFERSAVEAREA et doit correspondre à la taille maximale requise par le pilote pour enregistrer la zone de réserve de mémoire tampon d’images pendant les transitions d’alimentation.

Dxgkrnl prend des frais de validation sur le montant spécifié par le pilote pour s’assurer qu’il peut toujours obtenir des pages physiques sur demande. Cette action est effectuée en créant un objet de section unique pour chaque adaptateur physique qui spécifie une valeur différente de zéro pour la taille maximale.

La taille maximale signalée par le pilote doit être un multiple de PAGE_SIZE.

L’exécution du transfert vers et depuis la mémoire tampon d’image peut être effectuée à l’heure du choix du pilote. Pour faciliter le transfert, Dxgkrnl fournit les quatre derniers rappels dans le tableau ci-dessus au pilote en mode noyau. Ces rappels peuvent être utilisés pour mapper les parties appropriées de l’objet de section créé lors de l’initialisation de l’adaptateur.

Le pilote doit toujours fournir le hAdapter pour le périphérique master/prospect dans une chaîne LDA lorsqu’il appelle ces quatre fonctions de rappel.

Le pilote dispose de deux options pour implémenter la réservation de mémoire tampon de trame :

  1. (Méthode préférée) Le pilote doit allouer de l’espace par carte physique à l’aide de l’appel DXGKDDI_QUERYADAPTERINFO ci-dessus pour spécifier la quantité de stockage nécessaire par carte. Au moment de la transition d’alimentation, le pilote doit enregistrer ou restaurer la mémoire un adaptateur physique à la fois. Cette mémoire est fractionnée entre plusieurs objets de section, un par adaptateur physique.

  2. Si vous le souhaitez, le pilote peut enregistrer ou restaurer toutes les données dans un seul objet de section partagé. Cette action peut être effectuée en spécifiant une seule grande taille maximale dans l’appel DXGKDDI_QUERYADAPTERINFO pour l’adaptateur physique 0, puis une valeur zéro pour toutes les autres cartes physiques. Le pilote peut ensuite épingler l’objet de section entier une fois pour l’utiliser dans toutes les opérations d’enregistrement/restauration, pour toutes les cartes physiques. Cette méthode présente l’inconvénient principal qu’elle nécessite le verrouillage d’une plus grande quantité de mémoire en même temps, car elle ne prend pas en charge l’épinglage uniquement d’une sous-plage de la mémoire dans une MDL. Par conséquent, cette opération est plus susceptible d’échouer sous la pression de la mémoire. Le pilote doit également mapper les pages du MDL au GPU à l’aide des décalages de page appropriés.

Le pilote doit effectuer les tâches suivantes pour effectuer un transfert vers ou depuis la mémoire tampon de trame :

  • Pendant l’initialisation, le pilote doit préallouer une petite partie de la mémoire accessible par GPU à l’aide de l’une des routines de rappel d’allocation. Cette mémoire est utilisée pour garantir la progression de la progression si l’objet de section entier ne peut pas être mappé/verrouillé en même temps.

  • Au moment de la transition d’alimentation, le pilote doit d’abord appeler Dxgkrnl pour épingler la mémoire tampon du cadre. En cas de réussite, Dxgkrnl fournit au pilote un MDL aux pages verrouillées qui sont mappées à l’IOMMU. Le pilote peut ensuite effectuer un transfert directement vers ces pages dans les moyens les plus efficaces pour le matériel. Le pilote doit ensuite appeler Dxgkrnl pour déverrouiller/annuler le mappage de la mémoire.

  • Si Dxgkrnl ne peut pas épingler la mémoire tampon d’image entière en même temps, le pilote doit tenter d’avancer en utilisant la mémoire tampon préallouée allouée lors de l’initialisation. Dans ce cas, le pilote effectue le transfert en petits blocs. Lors de chaque itération du transfert (pour chaque segment), le pilote doit demander à Dxgkrnl de fournir une plage mappée de l’objet de section dans lequel il peut copier les résultats. Le pilote doit ensuite annuler le mappage de la partie de l’objet de section avant l’itération suivante.

Le pseudocode suivant est un exemple d’implémentation de cet algorithme.


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

Mémoire réservée matérielle

VidMm mappe la mémoire réservée matérielle avant que l’appareil ne soit attaché à l’IOMMU.

VidMm gère automatiquement toute mémoire signalée en tant que segment avec l’indicateur PopulatedFromSystemMemory . VidMm mappe cette mémoire en fonction de l’adresse physique fournie.

Pour les régions réservées de matériel privé non exposées par des segments, VidMm effectue un appel DXGKDDI_QUERYADAPTERINFO pour interroger les plages par le pilote. Les plages fournies ne doivent pas chevaucher les régions de mémoire utilisées par le gestionnaire de mémoire NTOS ; VidMm vérifie qu’aucune intersection de ce type ne se produit. Cette validation garantit que le pilote ne peut pas signaler accidentellement une région de mémoire physique qui se trouve en dehors de la plage réservée, ce qui violerait les garanties de sécurité de la fonctionnalité.

L’appel de requête est effectué une fois pour interroger le nombre de plages nécessaires et est suivi d’un deuxième appel pour remplir le tableau des plages réservées.

Test

Si le pilote opte pour cette fonctionnalité, un test HLK analyse la table d’importation du pilote pour s’assurer qu’aucune des fonctions Mm suivantes n’est appelée :

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

Toutes les allocations de mémoire pour la mémoire contiguë et les DLL doivent passer par l’interface de rappel de Dxgkrnl à l’aide des fonctions répertoriées. Le pilote ne doit pas non plus verrouiller la mémoire. Dxgkrnl gère les pages verrouillées pour le pilote. Une fois la mémoire réapplique, l’adresse logique des pages fournies au pilote peut ne plus correspondre aux adresses physiques.