Test a single-step trap delivered by hardware to set expectations, and then test a single-step trap synthesized by L0 to see if those expectations are met. Single-step traps in simulated MOVSS-shadow are tested as well. As a bonus, test a single-step trap in a transactional region to set expectations for that unusual case as well. Signed-off-by: Jim Mattson <jmattson@xxxxxxxxxx> Reviewed-by: Peter Shier <pshier@xxxxxxxxxx> --- x86/unittests.cfg | 6 ++ x86/vmx.h | 9 ++ x86/vmx_tests.c | 207 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 222 insertions(+) diff --git a/x86/unittests.cfg b/x86/unittests.cfg index 2d04806..e455d96 100644 --- a/x86/unittests.cfg +++ b/x86/unittests.cfg @@ -566,6 +566,12 @@ extra_params = -cpu host,+vmx -m 2560 -append vmx_nm_test arch = x86_64 groups = vmx +[vmx_db_test] +file = vmx.flat +extra_params = -cpu host,+vmx -m 2560 -append vmx_db_test +arch = x86_64 +groups = vmx + [vmx_eoi_bitmap_ioapic_scan] file = vmx.flat smp = 2 diff --git a/x86/vmx.h b/x86/vmx.h index 22b2892..8b60bf7 100644 --- a/x86/vmx.h +++ b/x86/vmx.h @@ -448,6 +448,15 @@ enum Intr_type { #define INTR_TYPE_SOFT_EXCEPTION (6 << 8) /* software exception */ #define INTR_TYPE_OTHER_EVENT (7 << 8) /* other event */ +/* + * Guest interruptibility state + */ +#define GUEST_INTR_STATE_STI (1 << 0) +#define GUEST_INTR_STATE_MOVSS (1 << 1) +#define GUEST_INTR_STATE_SMI (1 << 2) +#define GUEST_INTR_STATE_NMI (1 << 3) +#define GUEST_INTR_STATE_ENCLAVE (1 << 4) + /* * VM-instruction error numbers */ diff --git a/x86/vmx_tests.c b/x86/vmx_tests.c index 1574970..4a1c670 100644 --- a/x86/vmx_tests.c +++ b/x86/vmx_tests.c @@ -3,6 +3,9 @@ * * Author : Arthur Chunqi Li <yzt356@xxxxxxxxx> */ + +#include <asm/debugreg.h> + #include "vmx.h" #include "msr.h" #include "processor.h" @@ -4684,6 +4687,209 @@ static void vmx_nm_test(void) enter_guest(); } +static void vmx_db_test_guest(void) +{ + /* + * For a hardware generated single-step #DB. + */ + asm volatile("vmcall;" + "nop;" + ".Lpost_nop:"); + /* + * ...in a MOVSS shadow, with pending debug exceptions. + */ + asm volatile("vmcall;" + "nop;" + ".Lpost_movss_nop:"); + /* + * For an L0 synthesized single-step #DB. (L0 intercepts WBINVD and + * emulates it in software.) + */ + asm volatile("vmcall;" + "wbinvd;" + ".Lpost_wbinvd:"); + /* + * ...in a MOVSS shadow, with pending debug exceptions. + */ + asm volatile("vmcall;" + "wbinvd;" + ".Lpost_movss_wbinvd:"); + /* + * For a hardware generated single-step #DB in a transactional region. + */ + asm volatile("vmcall;" + ".Lxbegin: xbegin .Lskip_rtm;" + "xend;" + ".Lskip_rtm:"); +} + +/* + * Clear the pending debug exceptions and RFLAGS.TF and re-enter + * L2. No #DB is delivered and L2 continues to the next point of + * interest. + */ +static void dismiss_db(void) +{ + vmcs_write(GUEST_PENDING_DEBUG, 0); + vmcs_write(GUEST_RFLAGS, X86_EFLAGS_FIXED); + enter_guest(); +} + +/* + * Check a variety of VMCS fields relevant to an intercepted #DB exception. + * Then throw away the #DB exception and resume L2. + */ +static void check_db_exit(bool xfail_qual, bool xfail_dr6, bool xfail_pdbg, + void *expected_rip, u64 expected_exit_qual, + u64 expected_dr6) +{ + u32 reason = vmcs_read(EXI_REASON); + u32 intr_info = vmcs_read(EXI_INTR_INFO); + u64 exit_qual = vmcs_read(EXI_QUALIFICATION); + u64 guest_rip = vmcs_read(GUEST_RIP); + u64 guest_pending_dbg = vmcs_read(GUEST_PENDING_DEBUG); + u64 dr6 = read_dr6(); + const u32 expected_intr_info = INTR_INFO_VALID_MASK | + INTR_TYPE_HARD_EXCEPTION | DB_VECTOR; + + report("Expected #DB VM-exit", + reason == VMX_EXC_NMI && intr_info == expected_intr_info); + report("Expected RIP %p (actual %lx)", (u64)expected_rip == guest_rip, + expected_rip, guest_rip); + report_xfail("Expected pending debug exceptions 0 (actual %lx)", + xfail_pdbg, 0 == guest_pending_dbg, guest_pending_dbg); + report_xfail("Expected exit qualification %lx (actual %lx)", xfail_qual, + expected_exit_qual == exit_qual, + expected_exit_qual, exit_qual); + report_xfail("Expected DR6 %lx (actual %lx)", xfail_dr6, + expected_dr6 == dr6, expected_dr6, dr6); + dismiss_db(); +} + +/* + * Assuming the guest has just exited on a VMCALL instruction, skip + * over the vmcall, and set the guest's RFLAGS.TF in the VMCS. If + * pending debug exceptions are non-zero, set the VMCS up as if the + * previous instruction was a MOVSS that generated the indicated + * pending debug exceptions. Then enter L2. + */ +static void single_step_guest(const char *test_name, u64 starting_dr6, + u64 pending_debug_exceptions) +{ + printf("\n%s\n", test_name); + skip_exit_vmcall(); + write_dr6(starting_dr6); + vmcs_write(GUEST_RFLAGS, X86_EFLAGS_FIXED | X86_EFLAGS_TF); + if (pending_debug_exceptions) { + vmcs_write(GUEST_PENDING_DEBUG, pending_debug_exceptions); + vmcs_write(GUEST_INTR_STATE, GUEST_INTR_STATE_MOVSS); + } + enter_guest(); +} + +/* + * When L1 intercepts #DB, verify that a single-step trap clears + * pending debug exceptions, populates the exit qualification field + * properly, and that DR6 is not prematurely clobbered. In a + * (simulated) MOVSS shadow, make sure that the pending debug + * exception bits are properly accumulated into the exit qualification + * field. + */ +static void vmx_db_test(void) +{ + /* + * We are going to set a few arbitrary bits in DR6 to verify that + * (a) DR6 is not modified by an intercepted #DB, and + * (b) stale bits in DR6 (DR6.BD, in particular) don't leak into + * the exit qualification field for a subsequent #DB exception. + */ + const u64 starting_dr6 = DR6_RESERVED | BIT(13) | DR_TRAP3 | DR_TRAP1; + extern char post_nop asm(".Lpost_nop"); + extern char post_movss_nop asm(".Lpost_movss_nop"); + extern char post_wbinvd asm(".Lpost_wbinvd"); + extern char post_movss_wbinvd asm(".Lpost_movss_wbinvd"); + extern char xbegin asm(".Lxbegin"); + extern char skip_rtm asm(".Lskip_rtm"); + + /* + * L1 wants to intercept #DB exceptions encountered in L2. + */ + vmcs_write(EXC_BITMAP, BIT(DB_VECTOR)); + + /* + * Start L2 and run it up to the first point of interest. + */ + test_set_guest(vmx_db_test_guest); + enter_guest(); + + /* + * Hardware-delivered #DB trap for single-step sets the + * standard that L0 has to follow for emulated instructions. + */ + single_step_guest("Hardware delivered single-step", starting_dr6, 0); + check_db_exit(false, false, false, &post_nop, DR_STEP, starting_dr6); + + /* + * Hardware-delivered #DB trap for single-step in MOVSS shadow + * also sets the standard that L0 has to follow for emulated + * instructions. Here, we establish the VMCS pending debug + * exceptions to indicate that the simulated MOVSS triggered a + * data breakpoint as well as the single-step trap. + */ + single_step_guest("Hardware delivered single-step in MOVSS shadow", + starting_dr6, BIT(12) | DR_STEP | DR_TRAP0 ); + check_db_exit(false, false, false, &post_movss_nop, DR_STEP | DR_TRAP0, + starting_dr6); + + /* + * L0 synthesized #DB trap for single-step is buggy, because + * kvm (a) clobbers DR6 too early, and (b) tries its best to + * reconstitute the exit qualification from the prematurely + * modified DR6, but fails miserably. + */ + single_step_guest("Software synthesized single-step", starting_dr6, 0); + check_db_exit(true, true, false, &post_wbinvd, DR_STEP, starting_dr6); + + /* + * L0 synthesized #DB trap for single-step in MOVSS shadow is + * even worse, because L0 also leaves the pending debug + * exceptions in the VMCS instead of accumulating them into + * the exit qualification field for the #DB exception. + */ + single_step_guest("Software synthesized single-step in MOVSS shadow", + starting_dr6, BIT(12) | DR_STEP | DR_TRAP0); + check_db_exit(true, true, true, &post_movss_wbinvd, DR_STEP | DR_TRAP0, + starting_dr6); + + /* + * Optional RTM test for hardware that supports RTM, to + * demonstrate that the current volume 3 of the SDM + * (325384-067US), table 27-1 is incorrect. Bit 16 of the exit + * qualification for debug exceptions is not reserved. It is + * set to 1 if a debug exception (#DB) or a breakpoint + * exception (#BP) occurs inside an RTM region while advanced + * debugging of RTM transactional regions is enabled. + */ + if (cpuid(7).b & BIT(11)) { + vmcs_write(ENT_CONTROLS, + vmcs_read(ENT_CONTROLS) | ENT_LOAD_DBGCTLS); + /* + * Set DR7.RTM[bit 11] and IA32_DEBUGCTL.RTM[bit 15] + * in the guest to enable advanced debugging of RTM + * transactional regions. + */ + vmcs_write(GUEST_DR7, BIT(11)); + vmcs_write(GUEST_DEBUGCTL, BIT(15)); + single_step_guest("Hardware delivered single-step in " + "transactional region", starting_dr6, 0); + check_db_exit(false, false, false, &xbegin, BIT(16), + starting_dr6); + } else { + vmcs_write(GUEST_RIP, (u64)&skip_rtm); + enter_guest(); + } +} + static bool cpu_has_apicv(void) { return ((ctrl_cpu_rev[1].clr & CPU_APIC_REG_VIRT) && @@ -5245,6 +5451,7 @@ struct vmx_test vmx_tests[] = { /* Regression tests */ TEST(vmx_cr_load_test), TEST(vmx_nm_test), + TEST(vmx_db_test), /* EPT access tests. */ TEST(ept_access_test_not_present), TEST(ept_access_test_read_only), -- 2.19.0.444.g18242da7ef-goog