Intel SGXCPUフラグをlibvirtに追加した方法

パブリッククラウドでのIntelSGXの実装に関する記事が公開されて から数か月が経過しました 。この間、ソリューションは大幅に改善されました。基本的に、改善はマイナーなバグの排除と私たち自身の便宜のための改善に関連しています。







ただし、もう少し詳しくお話ししたいことが1つあります。






前回の記事では、SGXサポートの実装の一環として、ゲストドメインに必要な設定でXMLファイルを生成するようにNovaサービスに教える必要があると書きました。この問題は複雑で興味深いことが判明しました。そのソリューションの作業中に、libvirtの例を使用して、プログラムが一般にx86プロセッサの命令セットとどのように相互作用するかを詳細に理解する必要がありました。詳細で最も重要なことは、このトピックに関する明確に書かれた資料が非常に少ないことです。私たちの経験が仮想化に関わるすべての人に役立つことを願っています。ただし、まず最初に。



最初の試み



タスクの定式化をもう一度繰り返しましょう。SGXサポートパラメーターを仮想マシンのXML構成ファイルに渡す必要がありました。この問題の解決を始めたばかりのとき、OpenStackとlibvirtにはそれぞれSGXのサポートがなく、仮想マシンのXMLにネイティブに転送することはできませんでした。 Intel開発者ガイドで説明されいるように



最初にスクリプトにQemuコマンドラインブロック追加してlibvirt経由でハイパーバイザー接続することにより、この問題を解決しようとし まし



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





しかしその後、2番目のプロセッサオプションが仮想マシンに追加されました。



[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
      
      





最初のオプションは通常どおりに設定され、2番目のオプションはQemuコマンドラインブロックで直接追加されました 。これにより、プロセッサエミュレーションモデルを選択する際に不便が生じました。Nova計算ノードの構成ファイルでcpu_modelに置き換えたプロセッサモデルに関係なく、仮想マシンにホストプロセッサが表示されました。



この問題を解決する方法は?



答えを探すために、最初に次の行を試してみました< qemu:arg value = 'host、+ sgx、+ sgxlc'/>そしてプロセッサモデルをそれに転送しようとしましたが、これはVMの起動後にこのオプションの複製をキャンセルしませんでした。次に、libvirtを使用してCPUフラグを割り当て、cpu_model_extra_flagsパラメーターを使用して計算ノードのNov'y構成ファイルを介してそれらを制御することが決定されました



このタスクは、予想よりも難しいことが判明しました。IntelIA-32--CPUID命令を調べ、必要なレジスタとビットに関する情報をSGXのIntelドキュメントで見つける必要がありました。



さらに検索:libvirtをさらに深く掘り下げる



Novaサービスの開発者向けドキュメントには、CPUフラグマッピングはlibvirt自体でサポートされている必要があると記載されています。



すべてのCPUフラグを記述したファイルが見つかりました-これは x86_features.xmlです(libvirt 4.7.0以降に関連)。このファイルを確認した後、cpuidユーティリティを使用して7番目のシートで必要なレジスタの16進アドレスを取得するだけでよいと(後で誤って判明したように)想定しました。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
      
      





cpuidユーティリティを使用して取得した値でsgxフラグとsgxlcフラグを追加した後、次のエラーメッセージが表示されました:



error : x86Compute:1952 : out of memory
      
      





率直に言って、メッセージはあまり有益ではありません。どういうわけか問題が何であるかを理解するために、gitlablibvirtで問題を開きました 。 libvirt開発者は、誤ったエラーが表示されたことに気づき、それを修正しました。これは、libvirtが呼び出している正しい命令を見つけられなかったことを示し、どこが間違っている可能性があるかを示唆しました。しかし、エラーが発生しないように正確に何を示す必要があるかを理解するために、成功しませんでした。



私は情報源を掘り下げて研究しなければなりませんでした、それは長い時間がかかりました。Intelから変更されたQemuのコード研究した後でのみ、それを理解することができました



    [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ブロックに、7番目のシートのEBX / ECXレジスタからの命令がビットごとに(0から31まで)リストされていることがわかり ます。命令がQemuでサポートされていない場合、またはこのビットが予約されている場合は、NULL値で埋められます 。この例のおかげで、次の仮定を行いました。おそらく、libvirtで必要なレジスタの16進アドレスではなく、具体的にはこの命令のビットを指定する必要があります。ウィキペディアの表を読むと、これを理解しやすくなります 。左側にはビットと3つのレジスタがあります。その中に私たちの指示があります-sgx。表では、EBXレジスタの2番目のビットの下に示されています。







次に、Qemuコードでこの命令の場所を確認します。ご覧のとおり、彼女はfeat_namesのリストの3番目ですが、これはビット番号が0から始まるためです。



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





この表の他の命令を見て、0から数えるとき、それらが与えられたリストの独自のビットの下にあることを確認できます。次に例を示します 。fsgsbaseはEBXレジスタのビット0の下にあり、最初にリストされます。



Intelのドキュメントでは、これを確認し、cpuidを使用して必要な命令セットを呼び出し、目的のシートのレジスタ、場合によってはサブリストにアクセスするときに正しいビットを渡すことができることを確認しました。



32ビットプロセッサのアーキテクチャをより詳細に理解し始め、そのようなプロセッサには、メインの4つのレジスタ(EAX、EBX、ECX、EDX)を含むシートがあることがわかりました。これらの各レジスタには、特定のCPU命令セット用に予約された32ビットが含まれています。ビットは2の累乗であり、libvirtで行われるように、ほとんどの場合、16進形式でプログラムに渡すことができます。



より良く理解するために、ネストされたからVMX仮想化フラグを使用して別の例を考えてみx86_features.xmlのファイル のlibvirtで使用される:



<⁣feature名= ⁣'vmx「 >⁣

          <⁣cpuideax_in = 」0×01「 ECX = 」0x00000020 "/>#2 5 = 32 10 = 20 16

</feature⁣>



この命令への参照 は、ビット5のECXレジスタへの1枚目のシートで実行されます。これは、ウィキペディアの機能情報のを参照して確認できます



これに対処し、フラグが最終的にlibvirtに追加される方法を理解したので、変更されたQemuに存在する他のSGXフラグ(主要なフラグ:sgxとsgxlcに加えて)を追加することにしました。



[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属性のセットがサブリスト1のシート0x12にあることがわかりました。



[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の下にあります。 問題



の回答からも理解できました 。libvirtには、計算ノードのプロセッサが直接サポートするフラグをチェックするためのマクロがあります。これは、libvirt自体が命令セットシートをサポートしていない場合、x86_features.xmlファイルで必要なシート、ビット、およびレジスタを指定するだけでは不十分であることを意味します。しかし幸いなことに libvirtコードにはこのシートを処理する機能があることがわかりました



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





このリストから、7番目のリーフレジスタの2番目のEBXビット(つまり、SGX命令)にアクセスするときに、libvirtがリーフ0x12を使用して、サブリスト0、1、および2で使用可能な属性を確認できることがわかります。



結論



調査が終わった後、x86_features.xmlファイルを適切に追加する方法を見つけました。必要なビットを16進形式に変換しました-これが私たちが得たものです:



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





ここで、これらのフラグを仮想マシンに渡すために、cpu_model_extra_flagsを使用してNova構成ファイルでそれらを指定できます



[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命令に精通していることは、独自のソリューションを作成するときにも役立ちます。



ご不明な点がございましたら、コメントへようこそ。回答させていただきます。そして、何か追加するものがあれば、それ以上に書いてください。私たちは非常に感謝します。



All Articles