On 10/7/22 7:43 AM, Daniel P. Berrangé wrote:
The VMSA files contain the expected CPU register state for the VM.
Their
content varies based on a few pieces of the stack
- AMD CPU architectural initial state
- KVM hypervisor VM CPU initialization
- QEMU userspace VM CPU initialization
- AMD CPU SKU (family/model/stepping)
The first three pieces of information we can obtain through code
inspection. The last piece of information we can take on the command
line. This allows a user to validate a SEV-ES guest merely by providing
the CPU SKU information, using --cpu-family, --cpu-model,
--cpu-stepping. This avoids the need to obtain or construct VMSA files
directly.
Signed-off-by: Daniel P. Berrangé <berrange(a)redhat.com>
---
docs/manpages/virt-qemu-sev-validate.rst | 45 +++
tools/virt-qemu-sev-validate.py | 475 +++++++++++++++++++++++
2 files changed, 520 insertions(+)
diff --git a/docs/manpages/virt-qemu-sev-validate.rst
b/docs/manpages/virt-qemu-sev-validate.rst
index 24bca98d28..7ba7323e13 100644
--- a/docs/manpages/virt-qemu-sev-validate.rst
+++ b/docs/manpages/virt-qemu-sev-validate.rst
@@ -243,6 +243,24 @@ Validate the measurement of a SEV-ES SMP guest booting from disk:
--build-id 13 \
--policy 7
+Validate the measurement of a SEV-ES SMP guest booting from disk, with
+automatically constructed VMSA:
+
+::
+
+ # virt-dom-sev-validate \
+ --firmware OVMF.sev.fd \
+ --num-cpus 2 \
+ --cpu-family 23 \
+ --cpu-model 49 \
+ --cpu-stepping 0 \
+ --tk this-guest-tk.bin \
+ --measurement Zs2pf19ubFSafpZ2WKkwquXvACx9Wt/BV+eJwQ/taO8jhyIj/F8swFrybR1fZ2ID \
+ --api-major 0 \
+ --api-minor 24 \
+ --build-id 13 \
+ --policy 7
+
Fetch from remote libvirt
-------------------------
@@ -289,6 +307,20 @@ Validate the measurement of a SEV-ES SMP guest booting from disk:
--tk this-guest-tk.bin \
--domain fedora34x86_64
+Validate the measurement of a SEV-ES SMP guest booting from disk, with
+automatically constructed VMSA:
+
+::
+
+ # virt-dom-sev-validate \
+ --connect qemu+ssh://root@some.remote.host/system \
+ --firmware OVMF.sev.fd \
+ --cpu-family 23 \
+ --cpu-model 49 \
+ --cpu-stepping 0 \
+ --tk this-guest-tk.bin \
+ --domain fedora34x86_64
+
Fetch from local libvirt
------------------------
@@ -330,6 +362,19 @@ Validate the measurement of a SEV-ES SMP guest booting from disk:
--tk this-guest-tk.bin \
--domain fedora34x86_64
+Validate the measurement of a SEV-ES SMP guest booting from disk, with
+automatically constructed VMSA:
+
+::
+
+ # virt-dom-sev-validate \
+ --insecure \
+ --cpu-family 23 \
+ --cpu-model 49 \
+ --cpu-stepping 0 \
+ --tk this-guest-tk.bin \
+ --domain fedora34x86_64
+
EXIT STATUS
===========
diff --git a/tools/virt-qemu-sev-validate.py b/tools/virt-qemu-sev-validate.py
index ea5be80129..2505aff07f 100755
--- a/tools/virt-qemu-sev-validate.py
+++ b/tools/virt-qemu-sev-validate.py
@@ -44,6 +44,7 @@ import logging
from lxml import etree
import re
import socket
+from struct import pack
import sys
import traceback
from uuid import UUID
@@ -71,6 +72,435 @@ class InvalidStateException(Exception):
pass
+class Field(object):
+ U8 = 0
+ U16 = 2
+ U32 = 4
+ U64 = 8
+
+ SCALAR = 0
+ BITMASK = 1
+ ARRAY = 2
+
+ def __init__(self, name, size, format, value, order):
+ self.name = name
+ self.size = size
+ self.value = value
+ self.format = format
+ self.order = order
+
+
+class Struct(object):
+ def __init__(self, size):
+ self._fields = {}
+ self.size = size
+
+ def register_field(self, name, size, format=Field.SCALAR, defvalue=0):
+ self._fields[name] = Field(name, size, format,
+ defvalue, len(self.fields))
+
+ @property
+ def fields(self):
+ return sorted(self._fields.values(), key=lambda f: f.order)
+
+ def __getattr__(self, name):
+ return self._fields[name]
+
+ def __setattr__(self, name, value):
+ if name in ["_fields", "size"]:
+ super().__setattr__(name, value)
+ else:
+ self._fields[name].value = value
+
+ def binary_format(self):
+ fmt = ["<"]
+ datalen = 0
+ for field in self.fields:
+ if field.size == Field.U8:
+ if field.format == Field.ARRAY:
+ datalen += len(field.value)
+ fmt += ["%dB" % len(field.value)]
+ else:
+ datalen += 1
+ fmt += ["B"]
+ elif field.size == Field.U16:
+ datalen += 2
+ fmt += ["H"]
+ elif field.size == Field.U32:
+ datalen += 4
+ fmt += ["L"]
+ elif field.size == Field.U64:
+ datalen += 8
+ fmt += ["Q"]
+
+ pad = self.size - datalen
+ assert self.size >= 1
+ fmt += ["%dB" % pad]
+
+ return "".join(fmt), pad
+
+ def pack(self):
+ fmt, pad = self.binary_format()
+
+ values = []
+ for field in self.fields:
+ if field.size == Field.U8 and field.format == Field.ARRAY:
+ for k in range(len(field.value)):
+ values.append(field.value[k])
+ else:
+ values.append(field.value)
+ values.extend([0] * pad)
+
+ return pack(fmt, *values)
+
+
+class VMSA(Struct):
+ ATTR_G_SHIFT = 23
+ ATTR_G_MASK = (1 << ATTR_G_SHIFT)
+ ATTR_B_SHIFT = 22
+ ATTR_B_MASK = (1 << ATTR_B_SHIFT)
+ ATTR_L_SHIFT = 21
+ ATTR_L_MASK = (1 << ATTR_L_SHIFT)
+ ATTR_AVL_SHIFT = 20
+ ATTR_AVL_MASK = (1 << ATTR_AVL_SHIFT)
+ ATTR_P_SHIFT = 15
+ ATTR_P_MASK = (1 << ATTR_P_SHIFT)
+ ATTR_DPL_SHIFT = 13
+ ATTR_DPL_MASK = (3 << ATTR_DPL_SHIFT)
+ ATTR_S_SHIFT = 12
+ ATTR_S_MASK = (1 << ATTR_S_SHIFT)
+ ATTR_TYPE_SHIFT = 8
+ ATTR_TYPE_MASK = (15 << ATTR_TYPE_SHIFT)
+ ATTR_A_MASK = (1 << 8)
+
+ ATTR_CS_MASK = (1 << 11)
+ ATTR_C_MASK = (1 << 10)
+ ATTR_R_MASK = (1 << 9)
+
+ ATTR_E_MASK = (1 << 10)
+ ATTR_W_MASK = (1 << 9)
+
+ def __init__(self):
+ super().__init__(4096)
+
+ # From Linux arch/x86/include/asm/svm.h, we're unpacking the
+ # struct vmcb_save_area
+
+ self.register_field("es_selector", Field.U16)
+ self.register_field("es_attrib", Field.U16, Field.BITMASK)
+ self.register_field("es_limit", Field.U32)
+ self.register_field("es_base", Field.U64)
+
+ self.register_field("cs_selector", Field.U16)
+ self.register_field("cs_attrib", Field.U16, Field.BITMASK)
+ self.register_field("cs_limit", Field.U32)
+ self.register_field("cs_base", Field.U64)
+
+ self.register_field("ss_selector", Field.U16)
+ self.register_field("ss_attrib", Field.U16, Field.BITMASK)
+ self.register_field("ss_limit", Field.U32)
+ self.register_field("ss_base", Field.U64)
+
+ self.register_field("ds_selector", Field.U16)
+ self.register_field("ds_attrib", Field.U16, Field.BITMASK)
+ self.register_field("ds_limit", Field.U32)
+ self.register_field("ds_base", Field.U64)
+
+ self.register_field("fs_selector", Field.U16)
+ self.register_field("fs_attrib", Field.U16, Field.BITMASK)
+ self.register_field("fs_limit", Field.U32)
+ self.register_field("fs_base", Field.U64)
+
+ self.register_field("gs_selector", Field.U16)
+ self.register_field("gs_attrib", Field.U16, Field.BITMASK)
+ self.register_field("gs_limit", Field.U32)
+ self.register_field("gs_base", Field.U64)
+
+ self.register_field("gdtr_selector", Field.U16)
+ self.register_field("gdtr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("gdtr_limit", Field.U32)
+ self.register_field("gdtr_base", Field.U64)
+
+ self.register_field("ldtr_selector", Field.U16)
+ self.register_field("ldtr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("ldtr_limit", Field.U32)
+ self.register_field("ldtr_base", Field.U64)
+
+ self.register_field("idtr_selector", Field.U16)
+ self.register_field("idtr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("idtr_limit", Field.U32)
+ self.register_field("idtr_base", Field.U64)
+
+ self.register_field("tr_selector", Field.U16)
+ self.register_field("tr_attrib", Field.U16, Field.BITMASK)
+ self.register_field("tr_limit", Field.U32)
+ self.register_field("tr_base", Field.U64)
+
+ self.register_field("reserved_1",
+ Field.U8, Field.ARRAY, bytearray([0] * 43))
+
+ self.register_field("cpl", Field.U8)
+
+ self.register_field("reserved_2",
+ Field.U8, Field.ARRAY, bytearray([0] * 4))
+
+ self.register_field("efer", Field.U64)
+
+ self.register_field("reserved_3",
+ Field.U8, Field.ARRAY, bytearray([0] * 104))
+
+ self.register_field("xss", Field.U64)
+ self.register_field("cr4", Field.U64)
+ self.register_field("cr3", Field.U64)
+ self.register_field("cr0", Field.U64)
+ self.register_field("dr7", Field.U64)
+ self.register_field("dr6", Field.U64)
+ self.register_field("rflags", Field.U64)
+ self.register_field("rip", Field.U64)
+
+ self.register_field("reserved_4",
+ Field.U8, Field.ARRAY, bytearray([0] * 88))
+
+ self.register_field("rsp", Field.U64)
+
+ self.register_field("reserved_5",
+ Field.U8, Field.ARRAY, bytearray([0] * 24))
+
+ self.register_field("rax", Field.U64)
+ self.register_field("star", Field.U64)
+ self.register_field("lstar", Field.U64)
+ self.register_field("cstar", Field.U64)
+ self.register_field("sfmask", Field.U64)
+ self.register_field("kernel_gs_base", Field.U64)
+ self.register_field("sysenter_cs", Field.U64)
+ self.register_field("sysenter_esp", Field.U64)
+ self.register_field("sysenter_eip", Field.U64)
+ self.register_field("cr2", Field.U64)
+
+ self.register_field("reserved_6",
+ Field.U8, Field.ARRAY, bytearray([0] * 32))
+
+ self.register_field("g_pat", Field.U64)
+ self.register_field("dbgctl", Field.U64)
+ self.register_field("br_from", Field.U64)
+ self.register_field("br_to", Field.U64)
+ self.register_field("last_excp_from", Field.U64)
+ self.register_field("last_excp_to", Field.U64)
+
+ self.register_field("reserved_7",
+ Field.U8, Field.ARRAY, bytearray([0] * 72))
+
+ self.register_field("spec_ctrl", Field.U32)
+
+ self.register_field("reserved_7b",
+ Field.U8, Field.ARRAY, bytearray([0] * 4))
+
+ self.register_field("pkru", Field.U32)
+
+ self.register_field("reserved_7a",
+ Field.U8, Field.ARRAY, bytearray([0] * 20))
+
+ self.register_field("reserved_8", Field.U64) # rax duplicate
+
+ self.register_field("rcx", Field.U64)
+ self.register_field("rdx", Field.U64, Field.BITMASK)
+ self.register_field("rbx", Field.U64)
+
+ self.register_field("reserved_9", Field.U64) # rsp duplicate
+
+ self.register_field("rbp", Field.U64)
+ self.register_field("rsi", Field.U64)
+ self.register_field("rdi", Field.U64)
+ self.register_field("r8", Field.U64)
+ self.register_field("r9", Field.U64)
+ self.register_field("r10", Field.U64)
+ self.register_field("r11", Field.U64)
+ self.register_field("r12", Field.U64)
+ self.register_field("r13", Field.U64)
+ self.register_field("r14", Field.U64)
+ self.register_field("r15", Field.U64)
+
+ self.register_field("reserved_10",
+ Field.U8, Field.ARRAY, bytearray([0] * 16))
+
+ self.register_field("sw_exit_code", Field.U64)
+ self.register_field("sw_exit_info_1", Field.U64)
+ self.register_field("sw_exit_info_2", Field.U64)
+ self.register_field("sw_scratch", Field.U64)
+
+ self.register_field("reserved_11",
+ Field.U8, Field.ARRAY, bytearray([0] * 56))
+
+ self.register_field("xcr0", Field.U64)
+ self.register_field("valid_bitmap",
+ Field.U8, Field.ARRAY, bytearray([0] * 16))
+ self.register_field("x87_state_gpa",
+ Field.U64)
+
+ def amd64_cpu_init(self):
+ # AMD64 Architecture Programmer’s Manual
+ # Volume 2: System Programming.
+ #
+ # 14.1.3 Processor Initialization State
+ #
+ # Values after INIT
+
+ self.cr0 = (1 << 4)
+ self.rip = 0xfff0
+
+ self.cs_selector = 0xf000
+ self.cs_base = 0xffff0000
+ self.cs_limit = 0xffff
+
+ self.ds_limit = 0xffff
+
+ self.es_limit = 0xffff
+ self.fs_limit = 0xffff
+ self.gs_limit = 0xffff
+ self.ss_limit = 0xffff
+
+ self.gdtr_limit = 0xffff
+ self.idtr_limit = 0xffff
+
+ self.ldtr_limit = 0xffff
+ self.tr_limit = 0xffff
+
+ self.dr6 = 0xffff0ff0
+ self.dr7 = 0x0400
+ self.rflags = 0x2
+ self.xcr0 = 0x1
+
+ def kvm_cpu_init(self):
+ # svm_set_cr4() sets guest X86_CR4_MCE bit if host
+ # has X86_CR4_MCE enabled
+ self.cr4 = 0x40
+
+ # svm_set_efer sets guest EFER_SVME (Secure Virtual Machine enable)
+ self.efer = 0x1000
+
+ # init_vmcb + init_sys_seg() sets
+ # SVM_SELECTOR_P_MASK | SEG_TYPE_LDT
+ self.ldtr_attrib = 0x0082
+
+ # init_vmcb + init_sys_seg() sets
+ # SVM_SELECTOR_P_MASK | SEG_TYPE_BUSY_TSS16
+ self.tr_attrib = 0x0083
+
+ # kvm_arch_vcpu_create() in arch/x86/kvm/x86.c
+ self.g_pat = 0x0007040600070406
+
+ def qemu_cpu_init(self):
+ # Based on logic in x86_cpu_reset()
+ #
+ # file target/i386/cpu.c
+
+ def attr(mask):
+ return (mask >> VMSA.ATTR_TYPE_SHIFT)
+
+ self.ldtr_attrib = attr(VMSA.ATTR_P_MASK |
+ (2 << VMSA.ATTR_TYPE_SHIFT))
+ self.tr_attrib = attr(VMSA.ATTR_P_MASK |
+ (11 << VMSA.ATTR_TYPE_SHIFT))
+ self.cs_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_CS_MASK |
+ VMSA.ATTR_R_MASK |
+ VMSA.ATTR_A_MASK)
+ self.ds_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.es_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.ss_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.fs_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+ self.gs_attrib = attr(VMSA.ATTR_P_MASK |
+ VMSA.ATTR_S_MASK |
+ VMSA.ATTR_W_MASK |
+ VMSA.ATTR_A_MASK)
+
+ self.g_pat = 0x0007040600070406
+
+ def cpu_sku(self, family, model, stepping):
+ stepping &= 0xf
+ model &= 0xff
+ family &= 0xfff
+
+ self.rdx.value = stepping
+
+ if family > 0xf:
+ self.rdx.value |= 0xf00 | ((family - 0x0f) << 20)
+ else:
+ self.rdx.value |= family << 8
+
+ self.rdx.value |= ((model & 0xf) << 4) | ((model >> 4) <<
16)
+
+ def reset_addr(self, reset_addr):
+ reset_cs = reset_addr & 0xffff0000
+ reset_ip = reset_addr & 0x0000ffff
+
+ self.rip.value = reset_ip
+ self.cs_base.value = reset_cs
+
+
+class SevInfoBlock(Struct):
+
+ def __init__(self):
+ super().__init__(size=4)
+ self.register_field("reset_addr", Field.U32)
+
+
+class OVMF(object):
+
+ OVMF_TABLE_FOOTER_GUID = UUID("96b582de-1fb2-45f7-baea-a366c55a082d")
+ SEV_INFO_BLOCK_GUID = UUID("00f771de-1a7e-4fcb-890e-68c77e2fb44e")
+
+ def __init__(self):
+ self.entries = {}
+
+ def load(self, content):
+ expect = OVMF.OVMF_TABLE_FOOTER_GUID.bytes_le
+ actual = content[-48:-32]
+ if expect != actual:
+ raise Exception("OVMF footer GUID not found")
+
+ tablelen = int.from_bytes(content[-50:-48], byteorder='little')
+
+ if tablelen == 0:
+ raise Exception("OVMF tables zero length")
+
+ table = content[-(50 + tablelen):-50]
+
+ self.parse_table(table)
+
+ def parse_table(self, data):
+ while len(data) > 0:
+ entryuuid = UUID(bytes_le=data[-16:])
+ entrylen = int.from_bytes(data[-18:-16], byteorder='little')
+ entrydata = data[-entrylen:-18]
+
+ self.entries[str(entryuuid)] = entrydata
+
+ data = data[0:-(18 + entrylen)]
+
I noticed this with your older branch, but the parsing here only works
for the first entry, print(self.entries) will show you. That's all we
need for the script, but this will fix later entry parsing:
--- a/tools/virt-qemu-sev-validate.py
+++ b/tools/virt-qemu-sev-validate.py
@@ -480,7 +480,7 @@ class OVMF(object):
if tablelen == 0:
raise Exception("OVMF tables zero length")
- table = content[-(50 + tablelen):-50]
+ table = content[-(32 + tablelen):-50]
self.parse_table(table)
@@ -492,7 +492,7 @@ class OVMF(object):
self.entries[str(entryuuid)] = entrydata
- data = data[0:-(18 + entrylen)]
+ data = data[0:-entrylen]
+ def reset_addr(self):
+ if str(OVMF.SEV_INFO_BLOCK_GUID) not in self.entries:
+ raise Exception("SEV info block GUID not found")
+
+ info = SevInfoBlock()
+ info.unpack(self.entries[str(OVMF.SEV_INFO_BLOCK_GUID)])
+
+ return info.reset_addr.value
unpack() isn't implemented, so this will error. You could implement it
but it's kinda overkill. All you need is:
diff --git a/tools/virt-qemu-sev-validate.py
b/tools/virt-qemu-sev-validate.py
index 2c5ad9083d..78d94604d5 100755
--- a/tools/virt-qemu-sev-validate.py
+++ b/tools/virt-qemu-sev-validate.py
@@ -454,13 +454,6 @@ class VMSA(Struct):
self.cs_base.value = reset_cs
-class SevInfoBlock(Struct):
-
- def __init__(self):
- super().__init__(size=4)
- self.register_field("reset_addr", Field.U32)
-
-
class OVMF(object):
OVMF_TABLE_FOOTER_GUID = UUID("96b582de-1fb2-45f7-baea-a366c55a082d")
@@ -498,10 +491,9 @@ class OVMF(object):
if str(OVMF.SEV_INFO_BLOCK_GUID) not in self.entries:
raise Exception("SEV info block GUID not found")
- info = SevInfoBlock()
- info.unpack(self.entries[str(OVMF.SEV_INFO_BLOCK_GUID)])
-
- return info.reset_addr.value
+ reset_addr = int.from_bytes(
+ self.entries[str(OVMF.SEV_INFO_BLOCK_GUID)], "little")
+ return reset_addr
- Cole