Partager via


Isolation du GPU basée sur l'IOMMU

L’isolation GPU basée sur l’IOMMU est une technique utilisée pour améliorer la sécurité et la stabilité du système en gérant la manière dont les GPU accèdent à la mémoire système. Cet article décrit la fonctionnalité d’isolation GPU basée sur l’IOMMU dans WDDM pour les périphériques compatibles avec l’IOMMU, et comment les développeurs peuvent l’implémenter dans leurs pilotes graphiques.

Cette fonctionnalité est disponible à partir de la version 1803 de Windows 10 (WDDM 2.4). Veuillez consulter la section Remappage DMA de l’IOMMU pour des mises à jour plus récentes sur l’IOMMU.

Vue d’ensemble

L’isolation GPU basée sur l’IOMMU permet à Dxgkrnl de restreindre l’accès à la mémoire système depuis le GPU en utilisant le 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 du périphérique à la mémoire système uniquement à la mémoire qu’il doit pouvoir accéder. Il le fait en s’assurant que l’IOMMU traduit les accès à la mémoire via PCIe vers des pages physiques valides et accessibles.

Si l’adresse logique accédée par le périphérique n’est pas valide, le périphérique ne peut pas accéder à la mémoire physique. Cette restriction empêche toute une gamme d’exploits qui permettent à un attaquant d’accéder à la mémoire physique via un périphérique matériel compromis. Sans cela, les attaquants pourraient lire le contenu de la mémoire système qui n’est pas nécessaire pour le fonctionnement du périphérique.

Par défaut, cette fonctionnalité est activée uniquement sur les PC où Windows Defender Application Guard est activé pour Microsoft Edge (c’est-à-dire la virtualisation de conteneurs).

À des fins de développement, la fonctionnalité réelle de remappage de l’IOMMU 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ée peu de temps après le démarrage de l’adaptateur. Toutes les allocations de pilotes effectuées avant ce moment sont mappées lorsque l’IOMMU est activée.

De plus, si la clé de mise en scène de vélocité 14688597 est définie comme activée, l’IOMMU est activée lorsqu’une machine virtuelle sécurisée est créée. Pour l’instant, cette clé de mise en scène est désactivée par défaut pour permettre l’auto-hébergement sans support approprié de l’IOMMU.

Lorsque l’IOMMU est activée, le démarrage d’une machine virtuelle sécurisée échoue si le pilote ne prend pas en charge l’IOMMU.

Il n’existe actuellement aucun moyen de désactiver l’IOMMU une fois activée.

Accès mémoire

Dxgkrnl s’assure 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 décomposée en quatre catégories :

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

  • Toute mémoire accédée par le GPU lors des opérations de pagination, ou mappée via le GpuMmu doit être mappée à l’IOMMU. Ce processus est entièrement interne au gestionnaire de mémoire vidéo (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 soutien d’une allocation pour :

    • Toute la durée d’un transfert vers ou depuis la VRAM.
    • Toute la durée pendant laquelle le magasin de soutien est mappé à la mémoire système ou aux segments d’ouverture.
  • Mappage et dé-mappage des clôtures surveillées.

  • Lors des transitions d’alimentation, le pilote peut avoir besoin de sauvegarder des portions 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. Cela dit, Dxgkrnl prend une charge de validation sur la limite supérieure au moment où l’adaptateur est initialisé pour s’assurer que des pages physiques peuvent être obtenues lorsque nécessaire. Dxgkrnl est responsable de s’assurer que cette mémoire est verrouillée et mappée à l’IOMMU pour le transfert lors des transitions d’alimentation.

  • Pour toutes les ressources réservées au matériel, VidMm s’assure que les ressources IOMMU sont correctement mappées au moment où le périphérique 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, mémoire réservée au firmware/BIOD) qui n’est pas exposée via les segments VidMm, Dxgkrnl fait un appel DXGKDDI_QUERYADAPTERINFO pour interroger toutes les plages de mémoire réservée que le pilote doit mapper à l’avance. Veuillez consulter la section Mémoire réservée au matériel pour plus de détails.

Affectation de domaine

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

Le domaine sera attaché au périphérique la première fois qu’une machine virtuelle sécurisée est créée, ou peu de temps après le démarrage du périphérique si la clé de registre ci-dessus est utilisée.

Accès exclusif

L’attachement et le détachement de domaine IOMMU est rapide, mais néanmoins pas actuellement atomique. Parce qu’il n’est pas atomique, une transaction émise via PCIe n’est pas garantie d’être traduite correctement lors du basculement vers un domaine IOMMU avec des mappages différents.

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

Ces DDI forment une paire 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 inactif chaque fois que le périphérique est basculé vers un nouveau domaine IOMMU. C’est-à-dire que le pilote doit s’assurer qu’il ne lit ni n’écrit dans la mémoire système depuis le périphérique entre ces deux appels.

Entre ces deux appels, Dxgkrnl garantit les éléments suivants :

  • 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 l’accès exclusif, même sans notification explicite du système d’exploitation.

Dxgkrnl s’assure que tout travail en attente planifié sur le matériel est terminé, puis entre dans cette région d’accès exclusif. Pendant ce temps, Dxgkrnl attribue le domaine au périphérique. Dxgkrnl ne fait aucune demande au pilote ou au matériel entre ces appels.

Modifications DDI

Les changements DDI suivants ont été apportés pour prendre en charge l’isolation GPU basée sur l’IOMMU :

Allocation de mémoire et mappage à l’IOMMU

Dxgkrnl fournit les six premiers rappels du tableau précédent 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. Elles fournissent au pilote des MDL, ou des pointeurs qui décrivent la mémoire également mappée à l’IOMMU. Ces 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 autre handle comme partie de la sortie qui doit être renvoyée au rappel de libération correspondant.

Pour toute 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 aux MDL gérés par le pilote d’être suivis et utilisés avec l’IOMMU. Un pilote qui utilise ce rappel est responsable de s’assurer que la durée de vie du MDL dépasse l’appel de dé-mappage correspondant. Sinon, l’appel de dé-mappage a un comportement indéfini. Ce comportement indéfini peut compromettre la sécurité des pages du MDL que Mm a réaffectées au moment où elles sont dé-mappé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 rien à faire pour que ces allocations fonctionnent.

Réservation de tampon d’image

Pour les pilotes qui doivent enregistrer des portions réservées du tampon d’image dans la mémoire système lors des 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 support d’isolation IOMMU, Dxgkrnl émettra un appel à DXGKDDI_QUERYADAPTERINFO avec ce qui suit immédiatement après avoir interrogé les capacités de l’adaptateur physique :

  • Type est DXGKQAITYPE_FRAMEBUFFERSAVESIZE
  • L’entrée est du type UINT, qui est l’indice de l’adaptateur physique.
  • La sortie est du type DXGK_FRAMEBUFFERSAVEAREA, et doit être la taille maximale requise par le pilote pour enregistrer la zone de réserve du tampon d’image lors des transitions d’alimentation.

Dxgkrnl prend une charge de validation sur la quantité spécifiée 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 non nulle pour la taille maximale.

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

Le transfert vers et depuis le tampon d’image peut être effectué au moment choisi par le pilote. Pour faciliter le transfert, Dxgkrnl fournit les quatre derniers rappels du tableau précédent au pilote en mode noyau. Ces rappels peuvent être utilisés pour mapper les portions appropriées de l’objet de section qui a été créé lors de l’initialisation de l’adaptateur.

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

Le pilote a deux options pour mettre en œuvre la réservation du tampon d’image :

  1. (Méthode préférée) Le pilote doit allouer de l’espace pour chaque adaptateur physique en utilisant l’appel DXGKDDI_QUERYADAPTERINFO pour spécifier la quantité de stockage nécessaire par adaptateur. 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 répartie sur plusieurs objets de section, un par adaptateur physique.

  2. En option, 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 tous les autres adaptateurs physiques. Le pilote peut alors verrouiller l’intégralité de l’objet de section une fois pour une utilisation dans toutes les opérations de sauvegarde/restauration, pour tous les adaptateurs physiques. Cette méthode présente l’inconvénient principal de nécessiter le verrouillage d’une plus grande quantité de mémoire à la fois, car elle ne prend pas en charge le verrouillage uniquement d’une sous-plage de la mémoire dans un MDL. En conséquence, cette opération est plus susceptible d’échouer sous pression mémoire. Le pilote devra également mapper les pages du MDL sur le GPU en utilisant les bons décalages de pages.

Le pilote doit effectuer les tâches suivantes pour terminer un transfert vers ou depuis le tampon d’image :

  • Lors de l’initialisation, le pilote doit préallouer un petit segment de mémoire accessible par le GPU en utilisant l’une des routines de rappel d’allocation. Cette mémoire est utilisée pour garantir la progression du processus si l’intégralité de l’objet de section ne peut pas être mappée/verrouillée d’un seul coup.

  • Au moment de la transition d’alimentation, le pilote doit d’abord appeler Dxgkrnl pour verrouiller le tampon d’image. En cas de succès, Dxgkrnl fournit au pilote un MDL pour des pages verrouillées qui sont mappées à l’IOMMU. Le pilote peut alors effectuer un transfert directement vers ces pages par les moyens les plus efficaces pour le matériel. Le pilote doit ensuite appeler Dxgkrnl pour déverrouiller/dé-mapper la mémoire.

  • Si Dxgkrnl ne peut pas verrouiller l’intégralité du tampon d’image d’un seul coup, le pilote doit tenter de progresser en utilisant le tampon préalloué pendant l’initialisation. Dans ce cas, le pilote effectue le transfert par petits segments. 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 laquelle ils peuvent copier les résultats. Le pilote doit ensuite dé-mapper la portion 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 au matériel

VidMm mappe la mémoire réservée au matériel avant que le périphérique ne soit attaché à l’IOMMU.

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

Pour les régions privées de mémoire réservée au matériel 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 valide qu’aucune telle intersection n’a lieu. Cette validation garantit que le pilote ne peut pas accidentellement signaler une région de mémoire physique 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 seule fois pour interroger le nombre de plages nécessaires, et est suivi d’un second appel pour remplir le tableau des plages réservées.

Test

Si le pilote opte pour cette fonctionnalité, un test HLK scanne 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

Toute allocation de mémoire pour la mémoire contiguë et les MDL doit plutôt passer par l’interface de rappel de Dxgkrnl en utilisant les fonctions répertoriées. Le pilote ne doit pas non plus verrouiller de mémoire. Dxgkrnl gère les pages verrouillées pour le pilote. Une fois la mémoire remappée, l’adresse logique des pages fournies au pilote peut ne plus correspondre aux adresses physiques.