Cómo agregamos indicadores de CPU Intel SGX a libvirt

Han pasado varios meses desde que se publicó el artículo sobre la implementación de Intel SGX en nuestra nube pública. Durante este tiempo, la solución se ha mejorado significativamente. Básicamente, las mejoras se relacionan con la eliminación de errores menores y mejoras para nuestra propia conveniencia.







Sin embargo, hay un punto del que me gustaría hablar con más detalle.






En el artículo anterior, escribimos que como parte de la implementación del soporte SGX, era necesario enseñar al servicio Nova a generar un archivo XML con la configuración necesaria para el dominio invitado. Este problema resultó ser complejo e interesante: durante el trabajo en su solución, tuvimos que entender en detalle, usando el ejemplo de libvirt, cómo los programas en general interactúan con conjuntos de instrucciones en procesadores x86. Hay muy, muy pocos materiales detallados y, lo que es más importante, claramente escritos sobre este tema. Esperamos que nuestra experiencia sea de utilidad para todos los involucrados en la virtualización. Sin embargo, lo primero es lo primero.



Primeros intentos



Repitamos la formulación de la tarea una vez más: necesitábamos pasar parámetros de soporte SGX al archivo de configuración XML de la máquina virtual. Cuando recién comenzamos a resolver este problema, no había soporte SGX en OpenStack y libvirt, respectivamente, era imposible transferirlos al XML de la máquina virtual de forma nativa.



Primero intentamos resolver este problema agregando un bloque de línea de comandos Qemu al script para conectarnos al hipervisor a través de libvirt, como se describe en la guía para desarrolladores de 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>
      
      





Pero después de eso, se agregó una segunda opción de procesador a la máquina virtual:



[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 primera opción se configuró normalmente y la segunda fue agregada directamente por nosotros en el bloque de línea de comandos de Qemu . Esto generó un inconveniente al elegir un modelo de emulación de procesador: cualquiera que sea el modelo de procesador que sustituyamos por cpu_model en el archivo de configuración del nodo de cálculo de Nova, vimos la visualización del procesador host en la máquina virtual.



¿Cómo resolver este problema?



En busca de una respuesta, primero intentamos experimentar con la línea < qemu: arg value = 'host, + sgx, + sgxlc'/> e intente transferirle el modelo de procesador, pero esto no canceló la duplicación de esta opción después de que se inició la VM. Luego se decidió usar libvirt para asignar indicadores de CPU y controlarlos a través del archivo de configuración Nov'y del nodo computacional usando el parámetro cpu_model_extra_flags .



La tarea resultó ser más difícil de lo que esperábamos: necesitábamos estudiar la instrucción Intel IA-32 - CPUID, así como encontrar información sobre los registros y bits necesarios en la documentación de Intel sobre SGX.



Búsqueda adicional: profundizando en libvirt



La documentación para desarrolladores del servicio Nova establece que la asignación de indicadores de la CPU debe ser compatible con libvirt.



Encontramos un archivo que describe todos los indicadores de la CPU: este es x86_features.xml (relevante desde libvirt 4.7.0). Después de revisar este archivo, asumimos (como resultó más tarde, erróneamente) que solo necesitamos obtener las direcciones hexadecimales de los registros necesarios en la séptima hoja usando la utilidad cpuid. De la documentación de Intel, aprendimos en qué registros se llaman las instrucciones que necesitamos: sgx está en el registro EBX y sgxlc está en el 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
      
      





Después de agregar las banderas sgx y sgxlc con los valores obtenidos usando la utilidad cpuid, recibimos el siguiente mensaje de error:



error : x86Compute:1952 : out of memory
      
      





El mensaje, para decirlo sin rodeos, no es muy informativo. Para entender de alguna manera cuál es el problema, abrimos un problema en gitlab libvirt. Los desarrolladores de libvirt notaron que se mostraba un error incorrecto y lo arreglaron, lo que indica que libvirt no pudo encontrar la instrucción correcta que estábamos llamando y sugirieron dónde podríamos estar equivocados. Pero para entender qué era exactamente lo que necesitábamos indicar para que no hubiera error, no lo logramos.



Tuve que indagar en las fuentes y estudiar, tomó mucho tiempo. Fue posible resolverlo solo después de estudiar el código en un Qemu modificado de 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,
        },
      
      





En la lista anterior, puede ver que en los bloques .feat_names , las instrucciones de los registros EBX / ECX de la 7ª hoja se enumeran bit a bit (de 0 a 31); si la instrucción no es compatible con Qemu o este bit está reservado, entonces se rellena con un valor NULL . Gracias a este ejemplo, hicimos la siguiente suposición: quizás necesitemos especificar no la dirección hexadecimal del registro requerido en libvirt, sino específicamente el bit de esta instrucción. Es más fácil de entender esto leyendo la tabla de Wikipedia . A la izquierda hay un bit y tres registros. Encontramos nuestra instrucción en él - sgx. En la tabla, se indica debajo del segundo bit del registro EBX:







A continuación, verificamos la ubicación de esta instrucción en el código Qemu. Como podemos ver, ella es la tercera en la lista de feat_names, pero esto se debe a que la numeración de bits comienza desde 0:



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





Puede mirar otras instrucciones en esta tabla y asegurarse, al contar desde 0, que están por debajo de su propio bit en la lista dada. Por ejemplo: fsgsbase pasa por debajo del bit 0 del registro EBX y aparece en primer lugar.



En la documentación de Intel, encontramos confirmación de esto y nos aseguramos de que el conjunto de instrucciones requerido se pueda llamar usando cpuid, pasando el bit correcto al acceder al registro de la hoja deseada y, en algunos casos, a la sublista.



Comenzamos a comprender con más detalle la arquitectura de los procesadores de 32 bits y vimos que dichos procesadores tienen hojas que contienen los 4 registros principales: EAX, EBX, ECX, EDX. Cada uno de estos registros contiene 32 bits reservados para un conjunto específico de instrucciones de la CPU. Un bit es una potencia de dos y la mayoría de las veces se puede pasar a un programa en formato hexadecimal, como se hace en libvirt.



Para una mejor comprensión, considere otro ejemplo con el indicador de virtualización VMX anidado del archivo x86_features.xml utilizado por libvirt:



<⁣feature name = ⁣'vmx ' > ⁣

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

</ feature⁣>



La referencia a esta instrucción se lleva a cabo en la primera hoja del registro ECX en el bit 5 y puede verificar esto mirando la tabla de información de características en Wikipedia.



Habiendo lidiado con esto y habiendo comprendido cómo se agregan eventualmente las banderas a libvirt, decidimos agregar otras banderas SGX (además de las principales: sgx y sgxlc) que estaban presentes en el Qemu modificado:



[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
      
      





Algunas de estas banderas ya no son instrucciones, sino atributos de la Estructura de control de datos del enclave (SECS); puede leer más sobre esto en la documentación de Intel. En él, encontramos que el conjunto de atributos SGX que necesitamos está en la hoja 0x12 en la sublista 1:



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

      
      









En la captura de pantalla de la Tabla 38-3, puede encontrar los bits de atributo que necesitamos, que especificaremos más adelante como indicadores en libvirt: sgx-debug, sgx-mode64, sgx-provisionkey, sgx-tokenkey. Están ubicados en los bits 1, 2, 4 y 5.



También entendimos de la respuesta en nuestro problema : libvirt tiene una macro para verificar las banderas para su soporte directamente por el procesador del nodo computacional. Esto significa que no es suficiente especificar las hojas, bits y registros necesarios en el archivo x86_features.xml si libvirt en sí no admite una hoja de conjunto de instrucciones. Pero, afortunadamente para nosotros, resultó que el código libvirt tiene la capacidad de trabajar con esta hoja:



/* 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;
}
      
      





En esta lista, puede ver que al acceder al segundo bit EBX del registro de la séptima hoja (es decir, la instrucción SGX), libvirt puede usar la hoja 0x12 para verificar los atributos disponibles en las sublistas 0, 1 y 2.



Conclusión



Después de la investigación realizada, descubrimos cómo agregar correctamente el archivo x86_features.xml. Convertimos los bits necesarios a formato hexadecimal, y esto es lo que obtuvimos:



  <!-- 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>
      
      





Ahora, para pasar estos indicadores a la máquina virtual, podemos especificarlos en el archivo de configuración de Nova usando 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

      
      





Habiendo ido por el camino difícil, aprendimos cómo agregar soporte para banderas SGX a libvirt. Esto nos ayudó a resolver el problema de duplicar las opciones del procesador en el archivo XML de la máquina virtual. Usaremos la experiencia adquirida en nuestro trabajo futuro: si aparece un nuevo conjunto de instrucciones en los procesadores Intel o AMD, podemos agregarlas a libvirt de la misma manera. La familiaridad con la instrucción CPUID también nos será útil al escribir nuestras propias soluciones.



Si tiene alguna pregunta, bienvenido a los comentarios, intentaremos responder. Y si tiene algo que agregar, aún más, escriba, estaremos muy agradecidos.



All Articles