Ensure that the delivery of a "VMX-preemption timer expired" VM-exit doesn't disrupt single-stepping in the guest. Note that passing this test doesn't ensure correctness. Signed-off-by: Jim Mattson <jmattson@xxxxxxxxxx> Reviewed-by: Oliver Upton <oupton@xxxxxxxxxx> Reviewed-by: Peter Shier <pshier@xxxxxxxxxx> --- x86/vmx_tests.c | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/x86/vmx_tests.c b/x86/vmx_tests.c index fccb27f..86b8880 100644 --- a/x86/vmx_tests.c +++ b/x86/vmx_tests.c @@ -8438,6 +8438,109 @@ static void vmx_preemption_timer_zero_test(void) handle_exception(DB_VECTOR, old_db); } +static u64 vmx_preemption_timer_tf_test_prev_rip; + +static void vmx_preemption_timer_tf_test_db_handler(struct ex_regs *regs) +{ + extern char vmx_preemption_timer_tf_test_endloop; + + if (vmx_get_test_stage() == 2) { + /* + * Stage 2 means that we're done, one way or another. + * Arrange for the iret to drop us out of the wbinvd + * loop and stop single-stepping. + */ + regs->rip = (u64)&vmx_preemption_timer_tf_test_endloop; + regs->rflags &= ~X86_EFLAGS_TF; + } else if (regs->rip == vmx_preemption_timer_tf_test_prev_rip) { + /* + * The RIP should alternate between the wbinvd and the + * jmp instruction in the code below. If we ever see + * the same instruction twice in a row, that means a + * single-step trap has been dropped. Let the + * hypervisor know about the failure by executing a + * VMCALL. + */ + vmcall(); + } + vmx_preemption_timer_tf_test_prev_rip = regs->rip; +} + +static void vmx_preemption_timer_tf_test_guest(void) +{ + /* + * The hypervisor doesn't intercept WBINVD, so the loop below + * shouldn't be a problem--it's just two instructions + * executing in VMX non-root mode. However, when the + * hypervisor is running in a virtual environment, the parent + * hypervisor might intercept WBINVD and emulate it. If the + * parent hypervisor is broken, the single-step trap after the + * WBINVD might be lost. + */ + asm volatile("vmcall\n\t" + "0: wbinvd\n\t" + "1: jmp 0b\n\t" + "vmx_preemption_timer_tf_test_endloop:"); +} + +/* + * Ensure that the delivery of a "VMX-preemption timer expired" + * VM-exit doesn't disrupt single-stepping in the guest. Note that + * passing this test doesn't ensure correctness, because the test will + * only fail if the VMX-preemtion timer fires at the right time (or + * the wrong time, as it were). + */ +static void vmx_preemption_timer_tf_test(void) +{ + handler old_db; + u32 reason; + int i; + + if (!(ctrl_pin_rev.clr & PIN_PREEMPT)) { + report_skip("'Activate VMX-preemption timer' not supported"); + return; + } + + old_db = handle_exception(DB_VECTOR, + vmx_preemption_timer_tf_test_db_handler); + + test_set_guest(vmx_preemption_timer_tf_test_guest); + + enter_guest(); + skip_exit_vmcall(); + + vmx_set_test_stage(1); + vmcs_set_bits(PIN_CONTROLS, PIN_PREEMPT); + vmcs_write(PREEMPT_TIMER_VALUE, 50000); + vmcs_write(GUEST_RFLAGS, X86_EFLAGS_FIXED | X86_EFLAGS_TF); + + /* + * The only exit we should see is "VMX-preemption timer + * expired." If we get a VMCALL exit, that means the #DB + * handler has detected a missing single-step trap. It doesn't + * matter where the guest RIP is when the VMX-preemption timer + * expires (whether it's in the WBINVD loop or in the #DB + * handler)--a single-step trap should never be discarded. + */ + for (i = 0; i < 10000; i++) { + enter_guest(); + reason = (u32)vmcs_read(EXI_REASON); + if (reason == VMX_PREEMPT) + continue; + TEST_ASSERT(reason == VMX_VMCALL); + skip_exit_insn(); + break; + } + + report(reason == VMX_PREEMPT, "No single-step traps skipped"); + + vmx_set_test_stage(2); + vmcs_clear_bits(PIN_CONTROLS, PIN_PREEMPT); + enter_guest(); + + handle_exception(DB_VECTOR, old_db); +} + static void vmx_db_test_guest(void) { /* @@ -9743,6 +9846,7 @@ struct vmx_test vmx_tests[] = { TEST(vmx_pending_event_hlt_test), TEST(vmx_store_tsc_test), TEST(vmx_preemption_timer_zero_test), + TEST(vmx_preemption_timer_tf_test), /* EPT access tests. */ TEST(ept_access_test_not_present), TEST(ept_access_test_read_only), -- 2.26.0.110.g2183baf09c-goog