diff --git a/build/common.mk b/build/common.mk index a6e270f5..8e314cd1 100644 --- a/build/common.mk +++ b/build/common.mk @@ -1,4 +1,4 @@ -ALL_CATEGORIES := special functional xsa utility in-development +ALL_CATEGORIES := special functional xsa utility in-development nested-svm ALL_ENVIRONMENTS := pv64 pv32pae hvm64 hvm32pae hvm32pse hvm32 @@ -6,6 +6,7 @@ PV_ENVIRONMENTS := $(filter pv%,$(ALL_ENVIRONMENTS)) HVM_ENVIRONMENTS := $(filter hvm%,$(ALL_ENVIRONMENTS)) 32BIT_ENVIRONMENTS := $(filter pv32% hvm32%,$(ALL_ENVIRONMENTS)) 64BIT_ENVIRONMENTS := $(filter pv64% hvm64%,$(ALL_ENVIRONMENTS)) +SVM_ENVIRONMENTS := hvm64 # $(env)_guest => pv or hvm mapping $(foreach env,$(PV_ENVIRONMENTS),$(eval $(env)_guest := pv)) diff --git a/common/nested-svm/setup-l2.c b/common/nested-svm/setup-l2.c new file mode 100644 index 00000000..803e88d1 --- /dev/null +++ b/common/nested-svm/setup-l2.c @@ -0,0 +1,136 @@ +#include + +/* AMD MSRs. */ + +/* The VMRUN host-save area */ +#define MSR_VM_HSAVE_PA 0xc0010117U + +static uint16_t user_desc_vmcb_attr(const user_desc *desc) +{ + return desc->type | + (desc->s << 4) | + (desc->dpl << 5) | + (desc->p << 7) | + (desc->limit1 << 8) | + (desc->avl << 12) | + (desc->l << 13) | + (desc->d << 14) | + (desc->g << 15); +} + +static bool selector_is_null(uint16_t sel) +{ + return !(sel & ~(X86_SEL_TI | X86_SEL_RPL_MASK)); +} + +static void vmcb_set_seg_unusable(struct vmcb_seg *seg, uint16_t sel) +{ + seg->sel = sel; + seg->attr = 0; + seg->limit = 0; + seg->base = 0; +} + +static void vmcb_set_seg_desc(struct vmcb_seg *seg, const user_desc *gdt, + uint16_t gdt_limit, uint16_t sel) +{ + uint16_t sel_offset = sel & ~(X86_SEL_TI | X86_SEL_RPL_MASK); + unsigned int gdt_desc_bytes = sizeof(*gdt); + const user_desc *desc; + + if ( selector_is_null(sel) ) + { + vmcb_set_seg_unusable(seg, sel); + return; + } + + if ( (sel & X86_SEL_TI) || + (sel_offset + gdt_desc_bytes - 1 > gdt_limit) ) + { + vmcb_set_seg_unusable(seg, 0); + return; + } + + desc = (const user_desc *)((const char *)gdt + sel_offset); + + if ( !desc->s ) + gdt_desc_bytes *= 2; + + if ( sel_offset + gdt_desc_bytes - 1 > gdt_limit ) + { + vmcb_set_seg_unusable(seg, 0); + return; + } + + seg->sel = sel; + seg->attr = user_desc_vmcb_attr(desc); + seg->limit = user_desc_limit(desc); + seg->base = user_desc_base(desc); +} + +void svm_l1_prepare_for_vmrun(const void *hsave) +{ + wrmsr(MSR_EFER, rdmsr(MSR_EFER) | EFER_SVME); + wrmsr(MSR_VM_HSAVE_PA, _u(hsave)); +} + +void svm_l1_finish_vmrun(void) +{ + wrmsr(MSR_EFER, rdmsr(MSR_EFER) & ~EFER_SVME); +} + +void svm_l2_build_vmcb(struct vmcb *vmcb, const struct svm_l2_config *cfg) +{ + desc_ptr gdt_desc, idt_desc; + const user_desc *gdt; + + memset(vmcb, 0, sizeof(*vmcb)); + + vmcb->intercept_insns_vec3 = cfg->intercept_insns_vec3; + vmcb->intercept_insns_vec4 = cfg->intercept_insns_vec4; + vmcb->intercept_insns_vec5 = cfg->intercept_insns_vec5; + vmcb->asid = cfg->asid ? cfg->asid : 1; + + vmcb->cr0 = read_cr0(); + vmcb->cr3 = read_cr3(); + vmcb->cr4 = read_cr4(); + vmcb->efer = cfg->efer; + vmcb->rflags = read_flags(); + + vmcb->rsp = cfg->rsp; + vmcb->rip = cfg->rip; + + sgdt(&gdt_desc); + sidt(&idt_desc); + vmcb->gdtr.base = gdt_desc.base; + vmcb->gdtr.limit = gdt_desc.limit; + vmcb->idtr.base = idt_desc.base; + vmcb->idtr.limit = idt_desc.limit; + gdt = (const user_desc *)gdt_desc.base; + + vmcb_set_seg_desc(&vmcb->ldtr, gdt, gdt_desc.limit, sldt()); + vmcb_set_seg_desc(&vmcb->tr, gdt, gdt_desc.limit, str()); + + vmcb->cs.sel = __KERN_CS; + vmcb->cs.attr = 0xa9b; + vmcb->cs.limit = ~0u; + + vmcb->ds.sel = __USER_DS; + vmcb->ds.attr = 0xcf3; + vmcb->ds.limit = ~0u; + vmcb->es = vmcb->fs = vmcb->gs = vmcb->ds; + + vmcb->ss.sel = __KERN_DS; + vmcb->ss.attr = 0; + vmcb->ss.limit = 0; +} + +/* + * Local variables: + * mode: C + * c-file-style: "BSD" + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/common/nested-svm/test-harness.c b/common/nested-svm/test-harness.c new file mode 100644 index 00000000..59722e55 --- /dev/null +++ b/common/nested-svm/test-harness.c @@ -0,0 +1,104 @@ +#include + +static uint64_t svm_negative_paddr(const void *vmcb_page, + enum svm_negative_paddr_kind kind) +{ + switch ( kind ) + { + case SVM_NEGATIVE_PADDR_ALIGNED: + return _u(vmcb_page); + + case SVM_NEGATIVE_PADDR_UNALIGNED: + return _u(vmcb_page) | 1ul; + + case SVM_NEGATIVE_PADDR_TOO_WIDE: + return 1ull << maxphysaddr; + } + + unreachable(); +} + +/* set or clear EFER.SVME and return the original EFER value */ +static uint64_t update_efer_svme(bool enable) +{ + uint64_t efer = rdmsr(MSR_EFER); + uint64_t new_efer = enable ? (efer | EFER_SVME) : (efer & ~EFER_SVME); + + wrmsr(MSR_EFER, new_efer); + + return efer; +} + +static bool svm_negative_run(const struct svm_negative_case *t, + const struct svm_negative_runner *runner, + exinfo_t *res) +{ + uint64_t paddr = svm_negative_paddr(runner->vmcb_page, t->paddr_kind); + uint64_t orig_efer = 0; + + if ( runner->setup && !runner->setup(t, paddr, runner->data) ) + return false; + + if ( runner->manage_host_svme ) + orig_efer = update_efer_svme(t->svme); + + *res = 0; + + switch ( t->mode ) + { + case SVM_NEGATIVE_KERNEL: + *res = runner->ops->kernel(paddr); + break; + + case SVM_NEGATIVE_USER: + *res = exec_user_param(runner->ops->user, paddr); + break; + } + + if ( runner->manage_host_svme ) + wrmsr(MSR_EFER, orig_efer); + + if ( runner->teardown ) + runner->teardown(t, paddr, runner->data); + + return true; +} + +static void svm_negative_check_case(const struct svm_negative_case *t, + const struct svm_negative_runner *runner) +{ + exinfo_t got; + + if ( !svm_negative_run(t, runner, &got) ) + return; + + if ( got == t->expected ) + return; + + xtf_failure("Fail: %s, got %pe, expected %pe\n", + t->name, _p(got), _p(t->expected)); +} + +bool svm_negative_supported(void) +{ + if ( !cpu_has_svm ) + { + xtf_skip("Skip: SVM not available\n"); + return false; + } + + if ( !vendor_is_amd ) + xtf_warning("Warning: SVM found on non-AMD processor\n"); + + return true; +} + +void svm_negative_check_cases(const struct svm_negative_case *cases, + unsigned int nr_cases, + const struct svm_negative_runner *runner) +{ + unsigned int i; + + for ( i = 0; i < nr_cases; ++i ) + svm_negative_check_case(&cases[i], runner); +} diff --git a/common/nested-svm/vmrun-shared-minimal.S b/common/nested-svm/vmrun-shared-minimal.S new file mode 100644 index 00000000..083708ce --- /dev/null +++ b/common/nested-svm/vmrun-shared-minimal.S @@ -0,0 +1,14 @@ +/* + * Shared SVM nested-virt VMRUN trampoline for nested-SVM tests. + */ +#include + +ENTRY(svm_vmrun) + mov %rdi, %rax + vmload %rax + stgi + vmrun %rax + vmsave %rax + stgi + ret +ENDFUNC(svm_vmrun) diff --git a/docs/all-tests.dox b/docs/all-tests.dox index ff387476..f4e84b64 100644 --- a/docs/all-tests.dox +++ b/docs/all-tests.dox @@ -203,7 +203,11 @@ states. @subpage test-debug-regs - Debugging facility tests. -@subpage test-nested-svm - Nested SVM tests. +@subpage test-nested-svm-vmrun - Nested SVM VMRUN tests. + +@subpage test-nested-svm-vmload-negative - Negative Nested SVM VMLOAD tests. + +@subpage test-nested-svm-vmsave-negative - Negative Nested SVM VMSAVE tests. @subpage test-nested-vmx - Nested VT-x tests. */ diff --git a/include/nested-svm/setup-l2.h b/include/nested-svm/setup-l2.h new file mode 100644 index 00000000..6a2bde05 --- /dev/null +++ b/include/nested-svm/setup-l2.h @@ -0,0 +1,31 @@ +#ifndef XTF_NESTED_SVM_SETUP_L2_H +#define XTF_NESTED_SVM_SETUP_L2_H + +#include + +#include "vmcb.h" + +/* Minimal caller-supplied state for building an L2 VMCB from L1. */ +struct svm_l2_config { + unsigned long rip; + unsigned long rsp; + uint32_t asid; + uint32_t intercept_insns_vec3; + uint32_t intercept_insns_vec4; + uint32_t intercept_insns_vec5; + uint64_t efer; +}; + +/* Enable SVM in L1 and program the host-save area used by VMRUN. */ +void svm_l1_prepare_for_vmrun(const void *hsave); + +/* Leave L1 out of nested-hypervisor mode before guest shutdown. */ +void svm_l1_finish_vmrun(void); + +/* Build a minimal long-mode L2 VMCB that reuses the current L1 environment. */ +void svm_l2_build_vmcb(struct vmcb *vmcb, const struct svm_l2_config *cfg); + +/* Enter an L2 guest via the shared VMLOAD/VMRUN/VMSAVE trampoline. */ +void svm_vmrun(unsigned long l2_vmcb_pa); + +#endif /* XTF_TESTS_NESTED_SVM_L2_H */ diff --git a/include/nested-svm/test-harness.h b/include/nested-svm/test-harness.h new file mode 100644 index 00000000..b046a15f --- /dev/null +++ b/include/nested-svm/test-harness.h @@ -0,0 +1,63 @@ +#ifndef XTF_NESTED_SVM_TEST_HARNESS_H +#define XTF_NESTED_SVM_TEST_HARNESS_H + +#include + +enum svm_negative_mode { + SVM_NEGATIVE_KERNEL, + SVM_NEGATIVE_USER, +}; + +enum svm_negative_paddr_kind { + SVM_NEGATIVE_PADDR_ALIGNED, + SVM_NEGATIVE_PADDR_UNALIGNED, + SVM_NEGATIVE_PADDR_TOO_WIDE, +}; + +struct svm_negative_case { + const char *name; + bool svme; + enum svm_negative_mode mode; + enum svm_negative_paddr_kind paddr_kind; + exinfo_t expected; +}; + +struct svm_negative_ops { + exinfo_t (*kernel)(uint64_t paddr); + unsigned long (*user)(unsigned long paddr); +}; + +/* Return false when setup already reported a skip or failure for the case. */ +typedef bool (*svm_negative_setup_fn)(const struct svm_negative_case *t, + uint64_t paddr, void *data); +typedef void (*svm_negative_teardown_fn)(const struct svm_negative_case *t, + uint64_t paddr, void *data); + +/* + * Shared execution context for a family of SVM negative cases. + * + * Optional setup/teardown hooks let higher-level tests prepare nested state + * such as L2 or L3 control structures outside the fixed case matrix before + * the helper dispatches the actual instruction. L1-style callers can leave + * manage_host_svme enabled so the helper toggles live EFER.SVME itself, + * while nested callers can keep host SVME fixed and use setup/teardown to + * manage guest-visible SVME state explicitly. + */ +struct svm_negative_runner { + const void *vmcb_page; + const struct svm_negative_ops *ops; + bool manage_host_svme; + void *data; + svm_negative_setup_fn setup; + svm_negative_teardown_fn teardown; +}; + +/* Check basic nested-SVM availability in the current environment. */ +bool svm_negative_supported(void); + +/* Execute and check the supplied case matrix with the shared runner. */ +void svm_negative_check_cases(const struct svm_negative_case *cases, + unsigned int nr_cases, + const struct svm_negative_runner *runner); + +#endif /* TESTS_NESTED_SVM_SVM_NEGATIVE_H */ diff --git a/include/nested-svm/vmcb.h b/include/nested-svm/vmcb.h new file mode 100644 index 00000000..1a88c719 --- /dev/null +++ b/include/nested-svm/vmcb.h @@ -0,0 +1,115 @@ +/* Shared minimal VMCB definitions for nested-SVM tests. */ +#ifndef XTF_TESTS_NESTED_SVM_VMCB_H +#define XTF_TESTS_NESTED_SVM_VMCB_H + +#include + +struct vmcb_seg { + uint16_t sel; + uint16_t attr; + uint32_t limit; + uint64_t base; +}; + +struct vmcb { + uint16_t intercept_read_cr; + uint16_t intercept_write_cr; + uint16_t intercept_read_dr; + uint16_t intercept_write_dr; + uint32_t intercept_exceptions; + uint32_t intercept_insns_vec3; + uint32_t intercept_insns_vec4; + uint32_t intercept_insns_vec5; + uint8_t _pad_018[0x03C - 0x018]; + uint16_t pause_filter_threshold; + uint16_t pause_filter_count; + uint64_t iopm_base_pa; + uint64_t msrpm_base_pa; + uint64_t tsc_offset; + uint32_t asid; + uint8_t tlb_control; + uint8_t _pad_05d[3]; + uint64_t vintr; + uint64_t int_state; + uint64_t exitcode; + uint64_t exitinfo1; + uint64_t exitinfo2; + uint64_t exit_int_info; + uint64_t np_enable; + uint8_t _pad_098[0x0a8 - 0x098]; + uint64_t event_inj; + uint64_t h_cr3; + uint8_t _pad_0b8[0x400 - 0x0b8]; + struct vmcb_seg es; + struct vmcb_seg cs; + struct vmcb_seg ss; + struct vmcb_seg ds; + struct vmcb_seg fs; + struct vmcb_seg gs; + struct vmcb_seg gdtr; + struct vmcb_seg ldtr; + struct vmcb_seg idtr; + struct vmcb_seg tr; + uint8_t _pad_4a0[0x4cb - 0x4a0]; + uint8_t cpl; + uint32_t _pad_4cc; + uint64_t efer; + uint8_t _pad_4d8[0x548 - 0x4d8]; + uint64_t cr4; + uint64_t cr3; + uint64_t cr0; + uint64_t dr7; + uint64_t dr6; + uint64_t rflags; + uint64_t rip; + uint8_t _pad_580[0x5d8 - 0x580]; + uint64_t rsp; + uint8_t _pad_5e0[0x5f8 - 0x5e0]; + uint64_t rax; + uint8_t _pad_tail[0x1000 - 0x600]; +}; + +#define VMCB_CHECK(field, offset) \ + _Static_assert(__builtin_offsetof(struct vmcb, field) == (offset), \ + "VMCB layout mismatch: " #field) +VMCB_CHECK(intercept_insns_vec3, 0x00c); +VMCB_CHECK(intercept_insns_vec4, 0x010); +VMCB_CHECK(asid, 0x058); +VMCB_CHECK(exitcode, 0x070); +VMCB_CHECK(es, 0x400); +VMCB_CHECK(gdtr, 0x460); +VMCB_CHECK(idtr, 0x480); +VMCB_CHECK(tr, 0x490); +VMCB_CHECK(efer, 0x4d0); +VMCB_CHECK(cr4, 0x548); +VMCB_CHECK(cr3, 0x550); +VMCB_CHECK(cr0, 0x558); +VMCB_CHECK(rflags, 0x570); +VMCB_CHECK(rip, 0x578); +VMCB_CHECK(rsp, 0x5d8); +VMCB_CHECK(rax, 0x5f8); +_Static_assert(sizeof(struct vmcb) == 0x1000, "VMCB size != 4 KiB"); +#undef VMCB_CHECK + +#define GENERAL1_INTERCEPT_HLT (1u << 24) +#define GENERAL1_INTERCEPT_SHUTDOWN_EVT (1u << 31) + +#define GENERAL2_INTERCEPT_VMRUN (1u << 0) +#define GENERAL2_INTERCEPT_VMMCALL (1u << 1) + +#define VMEXIT_HLT 0x078 +#define VMEXIT_SHUTDOWN 0x07f +#define VMEXIT_VMRUN 0x080 +#define VMEXIT_VMMCALL 0x081 + +#endif /* XTF_TESTS_NESTED_SVM_VMCB_H */ + +/* + * Local variables: + * mode: C + * c-file-style: "BSD" + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/tests/nested-svm-vmload-negative/Makefile b/tests/nested-svm-vmload-negative/Makefile new file mode 100644 index 00000000..115d44d6 --- /dev/null +++ b/tests/nested-svm-vmload-negative/Makefile @@ -0,0 +1,11 @@ +include $(ROOT)/build/common.mk + +NAME := nested-svm-vmload-negative +CATEGORY := nested-svm +TEST-ENVS := $(SVM_ENVIRONMENTS) + +TEST-EXTRA-CFG := extra.cfg.in + +obj-perenv += main.o $(ROOT)/common/nested-svm/test-harness.o + +include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm/extra.cfg.in b/tests/nested-svm-vmload-negative/extra.cfg.in similarity index 100% rename from tests/nested-svm/extra.cfg.in rename to tests/nested-svm-vmload-negative/extra.cfg.in diff --git a/tests/nested-svm-vmload-negative/index.rst b/tests/nested-svm-vmload-negative/index.rst new file mode 100644 index 00000000..bd05121b --- /dev/null +++ b/tests/nested-svm-vmload-negative/index.rst @@ -0,0 +1,39 @@ +Nested SVM VMLOAD Negative Test +=============================== + +This test exercises the architectural VMLOAD failure cases that +are reachable from an hvm64 L1 guest using Xen nested SVM. + +What It Verifies +---------------- + +The test verifies the distinct VMLOAD error classes that are reachable from +this hvm64 harness: + +* VMLOAD with EFER.SVME clear. +* VMLOAD executed at CPL > 0. +* VMLOAD executed with malformed VMCB physical addresses in RAX. + +How The Verification Functions Work +----------------------------------- + +``test_main()`` supplies the VMLOAD-specific negative-case matrix to the shared +runner and checks that Xen reports the same exceptions that the AMD +architecture defines for the same preconditions, within the limits of an hvm64 +long-mode harness. + +``svm_negative_check_cases()`` toggles EFER.SVME as required by each subtest, +dispatches the instruction in kernel or user context, and restores the original +EFER value afterwards. + +``stub_vmload()`` executes VMLOAD directly in L1 with a caller-supplied RAX +value and records any fault via the XTF exception-table helpers. + +``user_vmload()`` executes the same instruction through ``exec_user_param()`` +so the test can verify the CPL > 0 cases with the exact operand chosen by L1. + +The AMD manuals also describe failure conditions for execution outside the +protected-mode environment required by SVM instructions, but those contexts are +not practically reachable from this hvm64 XTF harness without switching out of +the environment under test. The test therefore covers all distinct error +classes that are reachable here. diff --git a/tests/nested-svm-vmload-negative/main.c b/tests/nested-svm-vmload-negative/main.c new file mode 100644 index 00000000..e9e71225 --- /dev/null +++ b/tests/nested-svm-vmload-negative/main.c @@ -0,0 +1,183 @@ +/** + * @file tests/nested-svm-vmload-negative/main.c + * @ref test-nested-svm-vmload-negative + * + * @page test-nested-svm-vmload-negative nested-svm-vmload-negative + * + * Negative testing of AMD SVM VMLOAD from an hvm64 L1 guest. + * + * The test exercises the reachable architectural VMLOAD failure cases in this + * harness: + * 1. SVM disabled via EFER.SVME clear. + * 2. VMLOAD executed at CPL > 0. + * 3. VMLOAD executed with malformed VMCB physical addresses in RAX. + * + * The test passes if Xen reflects the expected bare-metal exception class for + * each case. + * + * @include tests/nested-svm-vmload-negative/index.rst + * + * @see tests/nested-svm-vmload-negative/main.c + */ +#include + +const char test_title[] = "Nested SVM VMLOAD negative"; + +/* Aligned scratch page used to form candidate VMCB physical addresses. */ +static uint8_t vmcb_page[PAGE_SIZE] __page_aligned_bss; + +/** + * Execute VMLOAD at CPL0 with a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static exinfo_t stub_vmload(uint64_t paddr) +{ + exinfo_t fault = 0; + + asm volatile ("1: vmload %%rax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+D" (fault) + : "a" (paddr), [rec] "p" (ex_record_fault_edi) + : "memory"); + + return fault; +} + +/** + * Execute VMLOAD at CPL3 using a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static unsigned long __user_text user_vmload(unsigned long paddr) +{ + unsigned long fault = 0; + + asm volatile ("mov %[paddr], %%rax;" + "1: vmload %%rax; xor %%eax, %%eax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+a" (fault) + : [paddr] "r" (paddr), + [rec] "p" (ex_record_fault_eax) + : "memory"); + + return fault; +} + +static const struct svm_negative_ops vmload_ops = { + .kernel = stub_vmload, + .user = user_vmload, +}; + +static const struct svm_negative_runner vmload_runner = { + .vmcb_page = vmcb_page, + .ops = &vmload_ops, + .manage_host_svme = true, +}; + +/** + * Execute the reachable VMLOAD negative-case matrix. + * + * The matrix covers the distinct bare-metal failure classes that can be + * observed from an hvm64 L1 harness: EFER.SVME clear, CPL > 0, and malformed + * VMCB physical addresses in RAX. + */ +void test_main(void) +{ + static const struct svm_negative_case cases[] = { + { + .name = "vmload with SVME clear at CPL0", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmload with SVME clear at CPL0 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME clear at CPL0 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmload with SVME clear at CPL3", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME clear at CPL3 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME clear at CPL3 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL3", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL3 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL3 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL0 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmload with SVME set at CPL0 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + }; + + if ( !svm_negative_supported() ) + return; + + svm_negative_check_cases(cases, ARRAY_SIZE(cases), &vmload_runner); + + xtf_success(NULL); +} + +/* + * Local variables: + * mode: C + * c-file-style: "BSD" + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/tests/nested-svm-vmrun/Makefile b/tests/nested-svm-vmrun/Makefile new file mode 100644 index 00000000..14af37b6 --- /dev/null +++ b/tests/nested-svm-vmrun/Makefile @@ -0,0 +1,13 @@ +include $(ROOT)/build/common.mk + +NAME := nested-svm-vmrun +CATEGORY := nested-svm +TEST-ENVS := $(SVM_ENVIRONMENTS) + +TEST-EXTRA-CFG := extra.cfg.in + +obj-perenv += main.o +obj-perenv += $(ROOT)/common/nested-svm/setup-l2.o +obj-perenv += $(ROOT)/common/nested-svm/vmrun-shared-minimal.o + +include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm-vmrun/extra.cfg.in b/tests/nested-svm-vmrun/extra.cfg.in new file mode 100644 index 00000000..ae494f84 --- /dev/null +++ b/tests/nested-svm-vmrun/extra.cfg.in @@ -0,0 +1 @@ +nestedhvm = 1 diff --git a/tests/nested-svm-vmrun/index.rst b/tests/nested-svm-vmrun/index.rst new file mode 100644 index 00000000..eccb6ef7 --- /dev/null +++ b/tests/nested-svm-vmrun/index.rst @@ -0,0 +1,60 @@ +Nested SVM VMRUN +================ + +This test exercises a minimal AMD nested-SVM guest entry path on hvm64. + +An L1 guest enables SVM, builds an L2 VMCB that reuses L1's page tables and +descriptor tables, mirrors the active TR and LDTR state into the VMCB, and +enters L2 with VMRUN. The L2 payload runs on its own stack, writes a sentinel +into shared memory, and halts. + +The test passes when L1 observes a HLT VMEXIT from L2 and the expected +sentinel value. + +What It Verifies +---------------- + +The test verifies that Xen's nested-SVM implementation accepts a minimal but +architecturally valid L2 VMCB built by an L1 guest running in long mode. + +More specifically, it verifies that: + +* L1 can enable SVM and program the host-save area needed by VMRUN. +* L1 can populate an L2 VMCB with inherited control state, descriptor-table + state, and system-segment state taken from the live L1 environment. +* VMRUN succeeds in entering L2 rather than failing immediately because of + malformed guest state. +* L2 executes the supplied payload on its own stack, updates shared memory, + and exits through HLT. +* L1 receives a VMEXIT with exit code VMEXIT_HLT and observes the sentinel + value written by L2. + +How The Verification Functions Work +----------------------------------- + +The verification logic is split between the helpers that build a +valid VMCB and the final checks performed after returning from VMRUN. + +``test_main()`` performs the end-to-end verification. It enables SVM, +writes the host-save area address to MSR_VM_HSAVE_PA, builds the VMCB, +and enters L2 through ``svm_vmrun()``. + +When execution returns to L1, the test checks two conditions: the VMEXIT +reason must be ``VMEXIT_HLT``, and the shared handshake value must match +``L2_SENTINEL``. Both checks must pass for the test to report success. + +``l2_entry()`` is the L2 payload. It avoids using VMMCALL, because +in Xen's nested-SVM model that would unconditionally cause a VMEXIT +to L1. Instead, it writes a known sentinel value into shared memory +and halts in a loop so that L1 sees a clean HLT exit reason. + +``build_l2_vmcb()`` prepares the nested guest state. It programs the +required intercepts, reuses L1's paging and descriptor-table state, +assigns the L2 RIP and stack, and copies LDTR and TR from the current +GDT into the VMCB. + +The helper ``vmcb_set_seg_desc()`` translates an L1 selector into the +VMCB segment format, while rejecting selectors that are null, LDT-based, +or out of bounds for the current GDT limit. This matters because VMRUN +consumes the VMCB's segment state directly, including the system +descriptors needed for long-mode execution. diff --git a/tests/nested-svm-vmrun/main.c b/tests/nested-svm-vmrun/main.c new file mode 100644 index 00000000..b4c9b094 --- /dev/null +++ b/tests/nested-svm-vmrun/main.c @@ -0,0 +1,141 @@ +/** + * @file tests/nested-svm-vmrun/main.c + * @ref test-nested-svm-vmrun + * + * @page test-nested-svm-vmrun nested-svm-vmrun + * + * Smoke-test of AMD SVM nested virtualisation: + * + * An L1 guest: + * 1. enables SVM, + * 2. builds a minimal L2 VMCB that re-uses L1's address space, and + * 3. uses VMRUN to enter an L2 callback. + * + * L2: + * 1. writes a sentinel into shared memory, and + * 2. signals completion with HLT, which causes a #VMEXIT back to L1. + * + * The test passes if: + * 1. VMRUN succeeds, + * 2. the exit reason is HLT, and + * 3. L1 observes the expected sentinel value. + * + * @include tests/nested-svm-vmrun/index.rst + * + * @see tests/nested-svm-vmrun/main.c + */ +#include + +const char test_title[] = "Nested SVM VMRUN"; + +/* + * The L2 VMCB lives here. VMRUN auto-saves and restores the bulk of + * L1's state via the host-save area pointed to by MSR_VM_HSAVE_PA. + */ +static struct vmcb l2_vmcb __page_aligned_bss; + +/* Backing store for the VMRUN host-save area (MSR_VM_HSAVE_PA). */ +static uint8_t hsave[PAGE_SIZE] __page_aligned_bss; + +/* Stack used by L2. Two pages of backing store. */ +static uint8_t l2_stack[2 * PAGE_SIZE] __page_aligned_bss; + +/* Sentinel written by L2 and verified by L1. */ +#define L2_SENTINEL 0xc0ffeeULL +static volatile uint64_t l2_handshake; + +/** + * Run a minimal L2 payload and report success back to L1. + * + * L2 cannot use the inherited Xen hypercall page here because VMMCALL from L2 + * unconditionally causes a #VMEXIT to L1 in Xen's nested-SVM model. Instead, + * L2 writes a sentinel into shared memory and halts so L1 observes a clean + * HLT exit reason. + */ +static void __used l2_entry(void) +{ + l2_handshake = L2_SENTINEL; + for ( ;; ) + asm volatile ("hlt"); +} + +/** + * Build the VMCB state used for the nested L2 guest. + * + * The test reuses L1's paging structures and descriptor tables, but supplies + * its own RIP and stack so L2 can execute a small payload in L1's address + * space. The resulting VMCB is intentionally minimal: it only carries the + * control and segment state needed for the VMRUN smoke test. + */ +static void build_l2_vmcb(void) +{ + const struct svm_l2_config cfg = { + .rip = _u(l2_entry), + .rsp = _u(&l2_stack[sizeof(l2_stack)]), + .asid = 1, + .intercept_insns_vec3 = + GENERAL1_INTERCEPT_HLT | GENERAL1_INTERCEPT_SHUTDOWN_EVT, + .intercept_insns_vec4 = GENERAL2_INTERCEPT_VMRUN, + .efer = rdmsr(MSR_EFER), + }; + + svm_l2_build_vmcb(&l2_vmcb, &cfg); +} + +/** + * Execute the nested-SVM VMRUN smoke test. + * + * L1 enables SVM, prepares a minimal L2 VMCB, enters L2 once with VMRUN and + * verifies that L2 reports success by writing the expected sentinel before + * exiting with HLT. + */ +void test_main(void) +{ + bool passed = false; + + if ( !cpu_has_svm ) + return xtf_skip("Skip: SVM not available\n"); + + /* Enable SVM and arm the host-save area. */ + svm_l1_prepare_for_vmrun(hsave); + + build_l2_vmcb(); + + printk("L1: entering L2 via VMRUN\n"); + svm_vmrun(_u(&l2_vmcb)); + printk("L1: returned from L2 (exitcode 0x%lx, handshake 0x%lx)\n", + l2_vmcb.exitcode, (unsigned long)l2_handshake); + + if ( l2_vmcb.exitcode != VMEXIT_HLT ) + { + xtf_failure("Fail: unexpected L2 exit 0x%lx (expected HLT 0x%x)\n", + l2_vmcb.exitcode, VMEXIT_HLT); + goto out; + } + + if ( l2_handshake != L2_SENTINEL ) + { + xtf_failure("Fail: L2 handshake 0x%lx != expected 0x%lx\n", + (unsigned long)l2_handshake, + (unsigned long)L2_SENTINEL); + goto out; + } + + passed = true; + + out: + svm_l1_finish_vmrun(); + + if ( passed ) + xtf_success(NULL); +} + +/* + * Local variables: + * mode: C + * c-file-style: "BSD" + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/tests/nested-svm-vmsave-negative/Makefile b/tests/nested-svm-vmsave-negative/Makefile new file mode 100644 index 00000000..7cb5a8f1 --- /dev/null +++ b/tests/nested-svm-vmsave-negative/Makefile @@ -0,0 +1,11 @@ +include $(ROOT)/build/common.mk + +NAME := nested-svm-vmsave-negative +CATEGORY := nested-svm +TEST-ENVS := $(SVM_ENVIRONMENTS) + +TEST-EXTRA-CFG := extra.cfg.in + +obj-perenv += main.o $(ROOT)/common/nested-svm/test-harness.o + +include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm-vmsave-negative/extra.cfg.in b/tests/nested-svm-vmsave-negative/extra.cfg.in new file mode 100644 index 00000000..ae494f84 --- /dev/null +++ b/tests/nested-svm-vmsave-negative/extra.cfg.in @@ -0,0 +1 @@ +nestedhvm = 1 diff --git a/tests/nested-svm-vmsave-negative/index.rst b/tests/nested-svm-vmsave-negative/index.rst new file mode 100644 index 00000000..88df51f2 --- /dev/null +++ b/tests/nested-svm-vmsave-negative/index.rst @@ -0,0 +1,38 @@ +Nested SVM VMSAVE Negative Test +=============================== + +This test exercises the architectural VMSAVE failure cases that are reachable +from an hvm64 L1 guest using Xen nested SVM. + +What It Verifies +---------------- + +The test verifies the distinct VMSAVE error classes that are reachable from +this hvm64 harness: + +* VMSAVE with EFER.SVME clear. +* VMSAVE executed at CPL > 0. +* VMSAVE executed with malformed VMCB physical addresses in RAX. + +How The Verification Functions Work +----------------------------------- + +``test_main()`` supplies the VMSAVE-specific negative-case matrix to the shared +runner and checks that Xen reports the expected exception class for each +reachable precondition from this hvm64 long-mode harness. + +``svm_negative_check_cases()`` from the shared nested-SVM negative helper layer +toggles EFER.SVME as required by each subtest, dispatches the instruction in +kernel or user context, and restores the original EFER value afterwards. + +``stub_vmsave()`` executes VMSAVE directly in L1 with a caller-supplied RAX +value and records any fault via the XTF exception-table helpers. + +``user_vmsave()`` executes the same instruction through ``exec_user_param()`` +so the test can verify the CPL > 0 cases with the exact operand chosen by L1. + +The AMD manuals also describe failures for contexts outside the protected-mode +environment required by SVM instructions, but those cases are not practically +reachable from this hvm64 XTF harness without leaving the environment under +test. This test therefore focuses on the distinct negative cases that are +reachable here. diff --git a/tests/nested-svm-vmsave-negative/main.c b/tests/nested-svm-vmsave-negative/main.c new file mode 100644 index 00000000..532e8bc5 --- /dev/null +++ b/tests/nested-svm-vmsave-negative/main.c @@ -0,0 +1,182 @@ +/** + * @file tests/nested-svm-vmsave-negative/main.c + * @ref test-nested-svm-vmsave-negative + * + * @page test-nested-svm-vmsave-negative nested-svm-vmsave-negative + * + * Negative testing of AMD SVM VMSAVE from an hvm64 L1 guest. + * + * The test exercises the reachable architectural VMSAVE failure cases in this + * harness: + * 1. SVM disabled via EFER.SVME clear. + * 2. VMSAVE executed at CPL > 0. + * 3. VMSAVE executed with malformed VMCB physical addresses in RAX. + * + * The test passes if Xen reflects the expected exception class for each case. + * + * @include tests/nested-svm-vmsave-negative/index.rst + * + * @see tests/nested-svm-vmsave-negative/main.c + */ +#include + +const char test_title[] = "Nested SVM VMSAVE negative"; + +/* Aligned scratch page used to form candidate VMCB physical addresses. */ +static uint8_t vmcb_page[PAGE_SIZE] __page_aligned_bss; + +/** + * Execute VMSAVE at CPL0 with a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static exinfo_t stub_vmsave(uint64_t paddr) +{ + exinfo_t fault = 0; + + asm volatile ("1: vmsave %%rax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+D" (fault) + : "a" (paddr), [rec] "p" (ex_record_fault_edi) + : "memory"); + + return fault; +} + +/** + * Execute VMSAVE at CPL3 using a caller-supplied VMCB physical address. + * @param paddr Candidate VMCB physical address for RAX. + * @return Recorded exception information, or zero on success. + */ +static unsigned long __user_text user_vmsave(unsigned long paddr) +{ + unsigned long fault = 0; + + asm volatile ("mov %[paddr], %%rax;" + "1: vmsave %%rax; xor %%eax, %%eax; 2:" + _ASM_EXTABLE_HANDLER(1b, 2b, %P[rec]) + : "+a" (fault) + : [paddr] "r" (paddr), + [rec] "p" (ex_record_fault_eax) + : "memory"); + + return fault; +} + +static const struct svm_negative_ops vmsave_ops = { + .kernel = stub_vmsave, + .user = user_vmsave, +}; + +static const struct svm_negative_runner vmsave_runner = { + .vmcb_page = vmcb_page, + .ops = &vmsave_ops, + .manage_host_svme = true, +}; + +/** + * Execute the reachable VMSAVE negative-case matrix. + * + * The matrix covers the distinct bare-metal failure classes that can be + * observed from an hvm64 L1 harness: EFER.SVME clear, CPL > 0, and malformed + * VMCB physical addresses in RAX. + */ +void test_main(void) +{ + static const struct svm_negative_case cases[] = { + { + .name = "vmsave with SVME clear at CPL0", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmsave with SVME clear at CPL0 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME clear at CPL0 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(UD, 0), + }, + { + .name = "vmsave with SVME clear at CPL3", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME clear at CPL3 and unaligned paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME clear at CPL3 and overly wide paddr", + .svme = false, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL3", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_ALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL3 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL3 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_USER, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL0 and unaligned paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_UNALIGNED, + .expected = EXINFO_SYM(GP, 0), + }, + { + .name = "vmsave with SVME set at CPL0 and overly wide paddr", + .svme = true, + .mode = SVM_NEGATIVE_KERNEL, + .paddr_kind = SVM_NEGATIVE_PADDR_TOO_WIDE, + .expected = EXINFO_SYM(GP, 0), + }, + }; + + if ( !svm_negative_supported() ) + return; + + svm_negative_check_cases(cases, ARRAY_SIZE(cases), &vmsave_runner); + + xtf_success(NULL); +} + +/* + * Local variables: + * mode: C + * c-file-style: "BSD" + * c-basic-offset: 4 + * tab-width: 4 + * indent-tabs-mode: nil + * End: + */ diff --git a/tests/nested-svm/Makefile b/tests/nested-svm/Makefile deleted file mode 100644 index a457d8a1..00000000 --- a/tests/nested-svm/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -include $(ROOT)/build/common.mk - -NAME := nested-svm -CATEGORY := in-development -TEST-ENVS := $(HVM_ENVIRONMENTS) - -TEST-EXTRA-CFG := extra.cfg.in - -obj-perenv += main.o - -include $(ROOT)/build/gen.mk diff --git a/tests/nested-svm/main.c b/tests/nested-svm/main.c deleted file mode 100644 index 3dc0ff1f..00000000 --- a/tests/nested-svm/main.c +++ /dev/null @@ -1,34 +0,0 @@ -/** - * @file tests/nested-svm/main.c - * @ref test-nested-svm - * - * @page test-nested-svm Nested SVM - * - * Functional testing of the SVM features in a nested-virt environment. - * - * @see tests/nested-svm/main.c - */ -#include - -const char test_title[] = "Nested SVM testing"; - -void test_main(void) -{ - if ( !cpu_has_svm ) - return xtf_skip("Skip: SVM not available\n"); - - if ( !vendor_is_amd ) - xtf_warning("Warning: SVM found on non-AMD processor\n"); - - xtf_success(NULL); -} - -/* - * Local variables: - * mode: C - * c-file-style: "BSD" - * c-basic-offset: 4 - * tab-width: 4 - * indent-tabs-mode: nil - * End: - */ diff --git a/xtf-runner b/xtf-runner index 8e27a484..0eb23a75 100755 --- a/xtf-runner +++ b/xtf-runner @@ -48,7 +48,7 @@ def exit_code(state): }[state] # All test categories -default_categories = {"functional", "xsa"} +default_categories = {"functional", "xsa", "nested-svm"} non_default_categories = {"special", "utility", "in-development"} all_categories = default_categories | non_default_categories