С момента публикации статьи о внедрении Intel SGX в наше публичное облако прошло несколько месяцев. За это время решение было существенно доработано. В основном улучшения касаются устранения мелких багов и доработок для нашего же удобства.



Есть, однако, один момент, о котором хотелось бы рассказать подробнее.



В предыдущей статье мы писали, что в рамках реализации поддержки SGX нужно было научить сервис Nova генерировать XML-файл с необходимыми настройками гостевого домена. Проблема эта оказалась сложной и интересной: за время работы по её решению нам пришлось на примере libvirt подробно разбираться, как в целом программы взаимодействуют с наборами инструкций в процессорах x86. Подробных и самое главное — понятно написанных материалов на эту тему очень и очень немного. Надеемся, что наш опыт будет полезен всем, кто занимается виртуализацией. Впрочем, обо всём по порядку.

Первые попытки


Ещё раз повторим формулировку задачи: нам требовалось передать параметры поддержки SGX в конфигурационный XML-файл виртуальной машины. Когда мы только начинали эту задачу решать, в OpenStack и libvirt поддержки SGX не было, соответственно, передать их в XML виртуальной машины нативно было невозможно.

Сначала мы попытались решить эту проблему путем добавления блока Qemu command-line в скрипт подключение к гипервизору через libvirt, как это описано в руководстве 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>

Но после этого у виртуальной машины добавилась вторая процессорная опция:

[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

Первая опция задавалась штатно, а вторая была непосредственно нами добавлена в блоке Qemu command-line. Это приводило к неудобству при выборе модели эмуляции процессора: какую бы из моделей процессоров мы ни подставляли в cpu_model в конфигурационном файле вычислительного узла Nova, в виртуальной машине мы видели отображение хостового процессора.

Как решить эту проблему?

В поисках ответа мы сначала пробовали экспериментировать со строкой <qemu:arg value='host,+sgx,+sgxlc'/> и пытаться передать в неё модель процессора, но это не отменяло дублирование этой опции после запуска ВМ. Тогда было решено задействовать libvirt для присвоения флагов CPU и управлять ими через конфигурационный файл Nov’ы вычислительного узла с помощью параметра cpu_model_extra_flags.

Задача оказалась сложнее, чем мы предполагали: нам потребовалось изучить инструкцию Intel IA-32 — CPUID, а также найти информацию о нужных регистрах и битах в документации Intel об SGX.

Дальнейший поиск: углубляемся в libvirt


В документации для разработчиков сервиса Nova указано, что маппинг CPU флагов должен поддерживаться самим libvirt’ом.

Мы нашли файл, в котором описываются все флаги CPU — это x86_features.xml (актуален с версии libvirt 4.7.0). Ознакомившись с этим файлом, предположили (как выяснилось потом, ошибочно), что нам нужно лишь получить hex-адреса необходимых регистров в 7-м листе с помощью утилиты cpuid. Из документации Intel мы узнали, в каких регистрах вызываются нужные нам инструкции: sgx находится в EBX регистре, а sgxlc — в 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

После добавления флагов sgx и sgxlc со значениями, полученными с помощью утилиты cpuid, мы получили следующее сообщение об ошибке:

error : x86Compute:1952 : out of memory

Сообщение, прямо говоря, не очень информативное. Чтобы хоть как-то понять, в чём проблема, мы завели issue в gitlab’e libvirt’a. Разработчики libvirt’a заметили, что выводится неверная ошибка и исправили ее, указав на то, что libvirt не может найти нужную инструкцию, которую мы вызываем и предположили, где мы можем ошибаться. Но понять, что именно нам нужно было указывать, чтобы ошибки не было, нам так и не удалось.

Пришлось зарыться в источники и изучать, это заняло много времени. Разобраться удалось лишь после изучения кода в модифицированном Qemu от 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,
        },

Из приведенного листинга видно, что в блоках .feat_names побитово (от 0 до 31) перечисляются инструкции из EBX/ECX-регистров 7-го листа; если инструкция не поддерживается Qemu или этот бит зарезервирован, то он заполняется значением NULL. Благодаря этому примеру мы сделали такое предположение: возможно, нужно указывать не hex-адрес необходимого регистра в libvirt, а конкретно бит этой инструкции. Проще это понять, ознакомившись с таблицей из Википедии. Слева указан бит и три регистра. Находим в ней нашу инструкцию — sgx. В таблице она указана под вторым битом в регистре EBX:



Далее сверяем расположение этой инструкции в коде Qemu. Как мы видим, она указана третьей в списке feat_names, но это потому, что нумерация битов начинается от 0:

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

Можно посмотреть другие инструкции в этой таблице и убедиться при подсчете от 0, что они находятся под своим битом в приведенном листинге. Например: fsgsbase идет под 0 битом регистра EBX, и он указан в этом списке первым.

В документации Intel мы нашли этому подтверждение и убедились, что необходимый набор инструкций можно вызвать с использованием cpuid, передавая правильный бит при обращении к регистру нужного листа, а в некоторых случаях — подлиста.

Мы стали более подробно разбираться в архитектуре 32-битных процессоров и увидели, что в таких процессорах имеются листы, которые содержат основные 4 регистра: EAX, EBX, ECX, EDX. Каждый из этих регистров содержит по 32 бита, отведенных под определенный набор инструкций CPU. Бит является степенью двойки и чаще всего может передаваться программе в hex-формате, как это сделано в libvirt.

Для лучшего понимания, рассмотрим еще пример c флагом вложенной виртуализации VMX из файла x86_features.xml, используемый libvirt’ом:

<?feature name= ?'vmx'>?
          <?cpuid eax_in='0x01' ecx='0x00000020'/> # 25 = 3210 = 2016
</feature?>

Обращение к этой инструкции осуществляется в 1-м листе к регистру ECX под 5 битом и убедиться в этом можно посмотрев таблицу Feature Information в Википедии.

Разобравшись с этим и сформировав понимание, как в итоге добавляются флаги в libvirt, мы решили добавить и другие флаги SGX (помимо основных: sgx и sgxlc), которые имелись в модифицированном Qemu:

[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

Некоторые из этих флагов являются уже не инструкциями, а атрибутами структуры управления данными анклавов (SECS); подробнее об этом можно прочитать в документации Intel. В ней мы нашли, что необходимый нам набор атрибутов SGX находится в листе 0x12 в подлисте 1:

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



На скриншоте таблицы 38-3 можно найти необходимые нам биты атрибутов, которые мы укажем позже в качестве флагов в libvirt: sgx-debug, sgx-mode64, sgx-provisionkey, sgx-tokenkey. Они находятся под битами 1, 2, 4 и 5.

Так же мы поняли из ответа в нашем issue: libvirt имеет макрос проверки флагов на предмет их поддержки непосредственно процессором вычислительного узла. Это означает то, что недостаточно указать в файле x86_features.xml необходимые листы, биты и регистры, если сам libvirt не поддерживает лист набора инструкций. Но к нашему счастью выяснилось, что в коде libvirt’a имеется возможность работы с этим листом:

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

Из этого листинга видно, что при обращении к 2-му биту EBX регистра 7-го листа (т.е. к инструкции SGX), libvirt может задействовать лист 0x12 для проверки имеющихся атрибутов в подлистах 0, 1 и 2.

Заключение


После проделанного исследования мы поняли, как правильно дополнить файл x86_features.xml. Мы перевели необходимые биты в hex-формат — и вот что у нас получилось:

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

Теперь для передачи этих флагов виртуальной машине мы можем указать их в конфигурационном файле Nova с помощью 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

Проделав сложный путь, мы научились добавлять в libvirt поддержку флагов SGX. Это нам помогло решить проблему дублирования процессорных опций в XML-файле виртуальной машины. Полученный опыт мы будем использовать и в дальнейшей работе: если в процессорах Intel или AMD появится новый набор инструкций, мы сможем их аналогичным образом добавить в libvirt. Знакомство с инструкцией CPUID так же будет нам полезно при написании своих собственных решений.

Если у вас есть вопросы — добро пожаловать в комментарии, постараемся ответить. А если у вас есть, что дополнить — тем более пишите, будем очень признательны.