С момента публикации статьи о внедрении 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; }'-cpuSkylake-Client-IBRS-cpuhost,+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 -rCPU: 0x00000007 0x00: eax=0x00000000 ebx=0x029c6fbf ecx=0x40000000 edx=0xbc000600
После добавления флагов sgx и sgxlc со значениями, полученными с
помощью утилиты cpuid, мы получили следующее сообщение об
ошибке:
error : x86Compute:1952 : out of memory
Сообщение, прямо говоря, не очень информативное. Чтобы хоть как-то
понять, в чём проблема, мы завели
issue в gitlabe libvirta. Разработчики libvirta заметили, что
выводится неверная ошибка и исправили ее, указав на то, что 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'/> # 2
5 = 32
10
=
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 sgxsgxsgx-debugsgx-exinfosgx-ksssgx-mode64sgx-provisionkeysgx-tokenkeysgx1sgx2sgxlc
Некоторые из этих флагов являются уже не инструкциями, а атрибутами
структуры управления данными анклавов (SECS); подробнее об этом
можно прочитать в
документации Intel. В ней мы нашли, что необходимый нам набор
атрибутов SGX находится в листе 0x12 в подлисте 1:
[root@compute-sgx ~] cpuid -l 0x12 -s 1 -1CPU: 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 не
поддерживает лист набора инструкций. Но к нашему счастью
выяснилось, что в
коде libvirta имеется возможность работы с этим листом:
/* 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 intcpuidSetLeaf12(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.confcpu_mode = customcpu_model = Skylake-Client-IBRScpu_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; }'-cpuSkylake-Client-IBRS,sgx=on,sgx-mode64=on,sgx-provisionkey=on,sgx-tokenkey=on,sgx1=on,sgxlc=on
Проделав сложный путь, мы научились добавлять в libvirt поддержку
флагов SGX. Это нам помогло решить проблему дублирования
процессорных опций в XML-файле виртуальной машины. Полученный опыт
мы будем использовать и в дальнейшей работе: если в процессорах
Intel или AMD появится новый набор инструкций, мы сможем их
аналогичным образом добавить в libvirt. Знакомство с инструкцией
CPUID так же будет нам полезно при написании своих собственных
решений.
Если у вас есть вопросы добро пожаловать в комментарии, постараемся
ответить. А если у вас есть, что дополнить тем более пишите, будем
очень признательны.