Comment nous avons ajouté des indicateurs de processeur Intel SGX à libvirt

Plusieurs mois se sont écoulés depuis la publication de l' article sur l'implémentation d'Intel SGX dans notre cloud public. Pendant ce temps, la solution a été considérablement améliorée. Fondamentalement, les améliorations concernent l'élimination des bogues mineurs et des améliorations pour notre propre convenance.







Il y a cependant un point dont je voudrais parler plus en détail.






Dans l'article précédent, nous avons écrit que dans le cadre de l'implémentation de la prise en charge de SGX, il était nécessaire d'apprendre au service Nova à générer un fichier XML avec les paramètres nécessaires pour le domaine invité. Ce problème s'est avéré complexe et intéressant: lors du travail sur sa solution, nous avons dû comprendre en détail, à l'aide de l'exemple libvirt, comment les programmes en général interagissent avec les jeux d'instructions dans les processeurs x86. Il existe très, très peu de documents détaillés et, surtout, clairement écrits sur ce sujet. Nous espérons que notre expérience sera utile à toutes les personnes impliquées dans la virtualisation. Cependant, tout d'abord.



Premières tentatives



Répétons encore une fois la formulation de la tâche: nous devions passer les paramètres de support SGX au fichier de configuration XML de la machine virtuelle. Lorsque nous venons de commencer à résoudre ce problème, il n'y avait pas de support SGX dans OpenStack et libvirt, respectivement, il était impossible de les transférer nativement vers le XML de la machine virtuelle.



Nous avons d'abord essayé de résoudre ce problème en ajoutant un bloc de ligne de commande Qemu au script pour se connecter à l'hyperviseur via libvirt, comme décrit dans le guide du développeur Intel:



<qemu:commandline>
     <qemu:arg value='-cpu'/>
     <qemu:arg value='host,+sgx,+sgxlc'/>
     <qemu:arg value='-object'/>
     <qemu:arg value='memory-backend-epc,id=mem1,size=''' + epc + '''M,prealloc'/>
     <qemu:arg value='-sgx-epc'/>
     <qemu:arg value='id=epc1,memdev=mem1'/>
</qemu:commandline>
      
      





Mais après cela, une deuxième option de processeur a été ajoutée à la machine virtuelle:



[root@compute-sgx ~] cat /proc/$PID/cmdline |xargs -0 printf "%s\n" |awk '/cpu/ { getline x; print $0 RS x; }'
-cpu
Skylake-Client-IBRS
-cpu
host,+sgx,+sgxlc
      
      





La première option a été définie normalement et la seconde a été ajoutée directement par nous dans le bloc de ligne de commande Qemu . Cela a conduit à un inconvénient lors du choix d'un modèle d'émulation de processeur: quel que soit le modèle de processeur que nous avons substitué à cpu_model dans le fichier de configuration du nœud de calcul Nova, nous avons vu l'affichage du processeur hôte dans la machine virtuelle.



Comment résoudre ce problème?



A la recherche d'une réponse, nous avons d'abord essayé d'expérimenter la ligne < qemu: arg value = 'host, + sgx, + sgxlc'/> et essayez de lui transférer le modèle de processeur, mais cela n'a pas annulé la duplication de cette option après le démarrage de la machine virtuelle. Ensuite, il a été décidé d'utiliser libvirt pour attribuer des drapeaux CPU et les contrôler via le fichier de configuration Nov'y du nœud de calcul en utilisant le paramètre cpu_model_extra_flags .



La tâche s'est avérée plus difficile que prévu: nous devions étudier l'instruction Intel IA-32 - CPUID, ainsi que trouver des informations sur les registres et bits requis dans la documentation Intel sur SGX.



Recherche supplémentaire: approfondir la recherche dans libvirt



La documentation du développeur pour le service Nova indique que le mappage des indicateurs CPU doit être pris en charge par libvirt lui-même.



Nous avons trouvé un fichier qui décrit tous les drapeaux du processeur - il s'agit de x86_features.xml (pertinent depuis libvirt 4.7.0). Après avoir examiné ce fichier, nous avons supposé (comme il s'est avéré plus tard, à tort) que nous n'avons besoin d'obtenir que les adresses hexadécimales des registres nécessaires dans la 7ème feuille à l'aide de l'utilitaire cpuid. À partir de la documentation Intel, nous avons appris dans quels registres les instructions dont nous avons besoin sont appelées: sgx est dans le registre EBX et sgxlc est dans l'ECX.



[root@compute-sgx ~] cpuid -l 7 -1 |grep SGX
      SGX: Software Guard Extensions supported = true
      SGX_LC: SGX launch config supported      = true

[root@compute-sgx ~] cpuid -l 7 -1 -r
CPU:
   0x00000007 0x00: eax=0x00000000 ebx=0x029c6fbf ecx=0x40000000 edx=0xbc000600
      
      





Après avoir ajouté les indicateurs sgx et sgxlc avec les valeurs obtenues à l'aide de l'utilitaire cpuid, nous avons reçu le message d'erreur suivant:



error : x86Compute:1952 : out of memory
      
      





Le message, pour le dire franchement, n'est pas très instructif. Pour comprendre en quelque sorte quel est le problème, nous avons ouvert un problème dans gitlab libvirt. Les développeurs de libvirt ont remarqué qu'une erreur incorrecte était affichée et l'ont corrigée, indiquant que libvirt ne pouvait pas trouver l'instruction correcte que nous appelions et suggérant où nous nous trompions. Mais pour comprendre exactement ce que nous devions indiquer pour qu'il n'y ait pas d'erreur, nous n'avons pas réussi.



J'ai dû fouiller dans les sources et étudier, cela a pris du temps. Il n'a été possible de le comprendre qu'après avoir étudié le code dans un Qemu modifié d'Intel:



    [FEAT_7_0_EBX] = {
        .type = CPUID_FEATURE_WORD,
        .feat_names = {
            "fsgsbase", "tsc-adjust", "sgx", "bmi1",
            "hle", "avx2", NULL, "smep",
            "bmi2", "erms", "invpcid", "rtm",
            NULL, NULL, "mpx", NULL,
            "avx512f", "avx512dq", "rdseed", "adx",
            "smap", "avx512ifma", "pcommit", "clflushopt",
            "clwb", "intel-pt", "avx512pf", "avx512er",
            "avx512cd", "sha-ni", "avx512bw", "avx512vl",
        },
        .cpuid = {
            .eax = 7,
            .needs_ecx = true, .ecx = 0,
            .reg = R_EBX,
        },
        .tcg_features = TCG_7_0_EBX_FEATURES,
    },
    [FEAT_7_0_ECX] = {
        .type = CPUID_FEATURE_WORD,
        .feat_names = {
            NULL, "avx512vbmi", "umip", "pku",
            NULL /* ospke */, "waitpkg", "avx512vbmi2", NULL,
            "gfni", "vaes", "vpclmulqdq", "avx512vnni",
            "avx512bitalg", NULL, "avx512-vpopcntdq", NULL,
            "la57", NULL, NULL, NULL,
            NULL, NULL, "rdpid", NULL,
            NULL, "cldemote", NULL, "movdiri",
            "movdir64b", NULL, "sgxlc", NULL,
        },
        .cpuid = {
            .eax = 7,
            .needs_ecx = true, .ecx = 0,
            .reg = R_ECX,
        },
      
      





Dans la liste ci-dessus, vous pouvez voir que dans les blocs .feat_names , les instructions des registres EBX / ECX de la 7ème feuille sont listées petit à petit (de 0 à 31); si l'instruction n'est pas prise en charge par Qemu ou si ce bit est réservé, alors il est rempli avec une valeur NULL . Grâce à cet exemple, nous avons fait l'hypothèse suivante: peut-être devons-nous spécifier non pas l'adresse hexadécimale du registre requis dans libvirt, mais spécifiquement le bit de cette instruction. Il est plus facile de comprendre cela en lisant le tableau de Wikipedia . Sur la gauche se trouve un peu et trois registres. Nous y trouvons notre instruction - sgx. Dans le tableau, il est indiqué sous le deuxième bit du registre EBX:







Ensuite, nous vérifions l'emplacement de cette instruction dans le code Qemu. Comme on peut le voir, elle est la troisième dans la liste des feat_names, mais c'est parce que la numérotation des bits commence à 0:



    [FEAT_7_0_EBX] = {
        .type = CPUID_FEATURE_WORD,
        .feat_names = {
            "fsgsbase", "tsc-adjust", "sgx", "bmi1",
      
      





Vous pouvez consulter les autres instructions de ce tableau et vous assurer, en comptant à partir de 0, qu'elles sont sous leur propre bit dans la liste donnée. Par exemple: fsgsbase passe sous le bit 0 du registre EBX et est répertorié en premier.



Dans la documentation Intel, nous avons trouvé la confirmation de cela et nous nous sommes assurés que le jeu d'instructions requis peut être appelé à l'aide de cpuid, en passant le bit correct lors de l'accès au registre de la feuille souhaitée et, dans certains cas, à la sous-liste.



Nous avons commencé à comprendre plus en détail l'architecture des processeurs 32 bits et avons vu que ces processeurs ont des feuilles qui contiennent les 4 registres principaux: EAX, EBX, ECX, EDX. Chacun de ces registres contient 32 bits réservés pour un ensemble spécifique d'instructions CPU. Un bit est une puissance de deux et peut le plus souvent être passé à un programme au format hexadécimal, comme cela se fait dans libvirt.



Pour une meilleure compréhension, considérons un autre exemple avec l'indicateur de virtualisation VMX imbriqué du fichier x86_features.xml utilisé par libvirt:



<⁣feature name = ⁣'vmx ' > ⁣

          <⁣cpuid eax_in = ' 0x01 ' ecx = ' 0x00000020 '/> # 2 5 = 32 10 = 20 16

</ feature⁣>



La référence à cette instruction est effectuée dans la 1ère feuille du registre ECX sous le bit 5 et vous pouvez le vérifier en consultant le tableau des informations sur les fonctionnalités de Wikipédia.



Après avoir traité de cela et avoir compris comment les indicateurs sont finalement ajoutés à libvirt, nous avons décidé d'ajouter d'autres indicateurs SGX (en plus des principaux: sgx et sgxlc) qui étaient présents dans le Qemu modifié:



[root@compute-sgx ~] /usr/libexec/qemu-kvm -cpu help |xargs printf '%s\n' |grep sgx
sgx
sgx-debug
sgx-exinfo
sgx-kss
sgx-mode64
sgx-provisionkey
sgx-tokenkey
sgx1
sgx2
sgxlc
      
      





Certains de ces indicateurs ne sont plus des instructions, mais des attributs de l'Enclave Data Control Structure (SECS); vous pouvez en savoir plus à ce sujet dans la documentation Intel. Dans ce document, nous avons constaté que l'ensemble des attributs SGX dont nous avons besoin se trouve dans la feuille 0x12 dans la sous-liste 1:



[root@compute-sgx ~] cpuid -l 0x12 -s 1 -1
CPU:
   SGX attributes (0x12/1):
      ECREATE SECS.ATTRIBUTES valid bit mask = 0x000000000000001f0000000000000036

      
      









Dans la capture d'écran du tableau 38-3, vous pouvez trouver les bits d'attribut dont nous avons besoin, que nous spécifierons plus tard comme indicateurs dans libvirt: sgx-debug, sgx-mode64, sgx-provisionkey, sgx-tokenkey. Ils sont situés sous les bits 1, 2, 4 et 5.



Nous avons également compris de la réponse de notre problème : libvirt a une macro pour vérifier les drapeaux pour leur prise en charge directement par le processeur du nœud de calcul. Cela signifie qu'il ne suffit pas de spécifier les feuilles, bits et registres nécessaires dans le fichier x86_features.xml si libvirt elle-même ne prend pas en charge une feuille de jeu d'instructions. Mais heureusement pour nous, il s'est avéré que le code libvirt a la capacité de fonctionner avec cette feuille:



/* Leaf 0x12: SGX capability enumeration
 *
 * Sub leaves 0 and 1 is supported if ebx[2] from leaf 0x7 (SGX) is set.
 * Sub leaves n >= 2 are valid as long as eax[3:0] != 0.
 */
static int
cpuidSetLeaf12(virCPUDataPtr data,
               virCPUx86DataItemPtr subLeaf0)
{
    virCPUx86DataItem item = CPUID(.eax_in = 0x7);
    virCPUx86CPUIDPtr cpuid = &item.data.cpuid;
    virCPUx86DataItemPtr leaf7;

    if (!(leaf7 = virCPUx86DataGet(&data->data.x86, &item)) ||
        !(leaf7->data.cpuid.ebx & (1 << 2)))
        return 0;

    if (virCPUx86DataAdd(data, subLeaf0) < 0)
        return -1;

    cpuid->eax_in = 0x12;
    cpuid->ecx_in = 1;
    cpuidCall(cpuid);
    if (virCPUx86DataAdd(data, &item) < 0)
        return -1;

    cpuid->ecx_in = 2;
    cpuidCall(cpuid);
    while (cpuid->eax & 0xf) {
        if (virCPUx86DataAdd(data, &item) < 0)
            return -1;
        cpuid->ecx_in++;
        cpuidCall(cpuid);
    }
    return 0;
}
      
      





À partir de cette liste, vous pouvez voir que lors de l'accès au 2ème bit EBX du 7ème registre feuille (c'est-à-dire l'instruction SGX), libvirt peut utiliser la feuille 0x12 pour vérifier les attributs disponibles dans les sous-listes 0, 1 et 2.



Conclusion



Une fois la recherche terminée, nous avons compris comment ajouter correctement le fichier x86_features.xml. Nous avons converti les bits nécessaires au format hexadécimal - et voici ce que nous avons obtenu:



  <!-- SGX features -->
  <feature name='sgx'>
    <cpuid eax_in='0x07' ecx_in='0x00' ebx='0x00000004'/>
  </feature>
  <feature name='sgxlc'>
    <cpuid eax_in='0x07' ecx_in='0x00' ecx='0x40000000'/>
  </feature>
  <feature name='sgx1'>
    <cpuid eax_in='0x12' ecx_in='0x00' eax='0x00000001'/>
  </feature>
  <feature name='sgx-debug'>
    <cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000002'/>
  </feature>
  <feature name='sgx-mode64'>
    <cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000004'/>
  </feature>
  <feature name='sgx-provisionkey'>
    <cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000010'/>
  </feature>
  <feature name='sgx-tokenkey'>
    <cpuid eax_in='0x12' ecx_in='0x01' eax='0x00000020'/>
  </feature>
      
      





Maintenant, pour passer ces drapeaux à la machine virtuelle, nous pouvons les spécifier dans le fichier de configuration Nova en utilisant cpu_model_extra_flags :



[root@compute-sgx nova] grep cpu_mode nova.conf
cpu_mode = custom
cpu_model = Skylake-Client-IBRS
cpu_model_extra_flags = sgx,sgxlc,sgx1,sgx-provisionkey,sgx-tokenkey,sgx-debug,sgx-mode64

[root@compute-sgx ~] cat /proc/$PID/cmdline |xargs -0 printf "%s\n" |awk '/cpu/ { getline x; print $0 RS x; }'
-cpu
Skylake-Client-IBRS,sgx=on,sgx-mode64=on,sgx-provisionkey=on,sgx-tokenkey=on,sgx1=on,sgxlc=on

      
      





Après avoir fait les gros efforts, nous avons appris comment ajouter la prise en charge des indicateurs SGX à libvirt. Cela nous a aidés à résoudre le problème de la duplication des options de processeur dans le fichier XML de la machine virtuelle. Nous utiliserons l'expérience acquise dans nos futurs travaux: si un nouvel ensemble d'instructions apparaît dans les processeurs Intel ou AMD, nous pouvons les ajouter à libvirt de la même manière. La connaissance de l'instruction CPUID nous sera également utile lors de l'écriture de nos propres solutions.



Si vous avez des questions - bienvenue dans les commentaires, nous essaierons de répondre. Et si vous avez quelque chose à ajouter - à plus forte raison, écrivez, nous vous en serons très reconnaissants.



All Articles