From: David Woodhouse Cover KVM_CAP_HYPERV_FORWARD_HCALL and the completion cancel: - the feature mask reports forwarding, and rejects bits it does not report; - with nothing enabled, an unknown hypercall fails in KVM; - with forwarding enabled, it exits to userspace in memory-based and fast form with the control value and parameters, and the guest sees the status userspace supplies (and, for the fast form, the XMM output userspace writes through KVM_SET_FPU); - the result reaches the guest as the full 64-bit value userspace wrote, bit 63 included; - a cancel with a queued #UD faults the hypercall, leaving RIP and RAX untouched; - a cancel alone makes the same hypercall exit a second time, and completing the second exit works; - the cancel fails with EBUSY at a hypercall-less ucall exit, whose PIO completion is not cancellable, and leaves the vCPU's events unchanged; - disabling everything restores the in-kernel failure. Signed-off-by: David Woodhouse --- tools/testing/selftests/kvm/Makefile.kvm | 1 + .../selftests/kvm/x86/hyperv_forward_hcall.c | 337 ++++++++++++++++++ 2 files changed, 338 insertions(+) create mode 100644 tools/testing/selftests/kvm/x86/hyperv_forward_hcall.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index 7a51ff975cfa..ca68943f99d4 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -89,6 +89,7 @@ TEST_GEN_PROGS_x86 += x86/hyperv_cpuid TEST_GEN_PROGS_x86 += x86/hyperv_evmcs TEST_GEN_PROGS_x86 += x86/hyperv_extended_hypercalls TEST_GEN_PROGS_x86 += x86/hyperv_features +TEST_GEN_PROGS_x86 += x86/hyperv_forward_hcall TEST_GEN_PROGS_x86 += x86/hyperv_ipi TEST_GEN_PROGS_x86 += x86/hyperv_svm_test TEST_GEN_PROGS_x86 += x86/hyperv_tlb_flush diff --git a/tools/testing/selftests/kvm/x86/hyperv_forward_hcall.c b/tools/testing/selftests/kvm/x86/hyperv_forward_hcall.c new file mode 100644 index 000000000000..4c290b6a1012 --- /dev/null +++ b/tools/testing/selftests/kvm/x86/hyperv_forward_hcall.c @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Test KVM_CAP_HYPERV_FORWARD_HCALL: the feature mask KVM reports and + * accepts, Hyper-V hypercalls that KVM does not implement exiting to + * userspace in memory-based and fast form, with userspace supplying the + * result and the XMM output, and a pending completion cancelled through + * KVM_SET_VCPU_EVENTS - alone, so that the guest re-executes the + * hypercall, and together with a queued #UD, so that the hypercall + * faults instead. + */ +#include "test_util.h" +#include "kvm_util.h" +#include "processor.h" +#include "hyperv.h" + +/* A hypercall code KVM does not implement (HvCallGetVpRegisters). */ +#define HVCALL_TEST_CODE 0x50 + +#define FAST_PARAM0 0x1122334455667788ULL +#define FAST_PARAM1 0x99aabbccddeeff00ULL +#define XMM_IN_MARKER 0x5157ca11c0ffee00ULL +#define XMM_OUT_MARKER 0x0de1ba5eba5eba11ULL +#define USERSPACE_STATUS 0x55AA +#define FULL_RESULT (BIT_ULL(63) | USERSPACE_STATUS) + +enum { + STAGE_DISABLED = 1, + STAGE_MEMORY, + STAGE_FAST, + STAGE_RESULT_FULL, + STAGE_CANCEL_UD, + STAGE_CANCEL_RETRY, + STAGE_AFTER_CANCEL, + STAGE_REDISABLED, +}; + +static void guest_code(gpa_t pgs_gpa) +{ + __sse128_u in, out; + u64 status; + u8 vector; + + wrmsr(HV_X64_MSR_GUEST_OS_ID, HYPERV_LINUX_OS_ID); + wrmsr(HV_X64_MSR_HYPERCALL, pgs_gpa); + + /* Nothing enabled: KVM fails the call itself. */ + vector = __hyperv_hypercall(HVCALL_TEST_CODE, pgs_gpa + PAGE_SIZE, + pgs_gpa + 2 * PAGE_SIZE, &status); + GUEST_ASSERT(!vector); + GUEST_ASSERT_EQ(status & 0xffff, HV_STATUS_INVALID_HYPERCALL_CODE); + GUEST_SYNC(STAGE_DISABLED); + + /* Forwarding enabled: userspace supplies the status. */ + vector = __hyperv_hypercall(HVCALL_TEST_CODE, pgs_gpa + PAGE_SIZE, + pgs_gpa + 2 * PAGE_SIZE, &status); + GUEST_ASSERT(!vector); + GUEST_ASSERT_EQ(status, USERSPACE_STATUS); + GUEST_SYNC(STAGE_MEMORY); + + /* + * Fast form: the register parameters and XMM0 reach userspace, and + * the XMM1 value userspace writes reaches the guest. + */ + in.as_u64[0] = XMM_IN_MARKER; + in.as_u64[1] = XMM_IN_MARKER + 1; + out.as_u64[0] = 0; + out.as_u64[1] = 0; + write_sse_reg(0, &in.vec); + write_sse_reg(1, &out.vec); + vector = __hyperv_hypercall(HVCALL_TEST_CODE | HV_HYPERCALL_FAST_BIT, + FAST_PARAM0, FAST_PARAM1, &status); + read_sse_reg(1, &out.vec); + GUEST_ASSERT(!vector); + GUEST_ASSERT_EQ(status, USERSPACE_STATUS); + GUEST_ASSERT_EQ(out.as_u64[0], XMM_OUT_MARKER); + GUEST_ASSERT_EQ(out.as_u64[1], XMM_OUT_MARKER + 1); + GUEST_SYNC(STAGE_FAST); + + /* The result reaches the guest as all 64 bits userspace wrote. */ + vector = __hyperv_hypercall(HVCALL_TEST_CODE, pgs_gpa + PAGE_SIZE, + pgs_gpa + 2 * PAGE_SIZE, &status); + GUEST_ASSERT(!vector); + GUEST_ASSERT_EQ(status, FULL_RESULT); + GUEST_SYNC(STAGE_RESULT_FULL); + + /* Userspace cancels the completion and queues #UD: the call faults. */ + vector = __hyperv_hypercall(HVCALL_TEST_CODE, pgs_gpa + PAGE_SIZE, + pgs_gpa + 2 * PAGE_SIZE, &status); + GUEST_ASSERT_EQ(vector, UD_VECTOR); + GUEST_SYNC(STAGE_CANCEL_UD); + + /* + * Userspace cancels the completion without queueing anything: this + * one hypercall re-executes and exits to userspace a second time, + * which then completes it. + */ + vector = __hyperv_hypercall(HVCALL_TEST_CODE, pgs_gpa + PAGE_SIZE, + pgs_gpa + 2 * PAGE_SIZE, &status); + GUEST_ASSERT(!vector); + GUEST_ASSERT_EQ(status, USERSPACE_STATUS); + GUEST_SYNC(STAGE_CANCEL_RETRY); + + /* An ordinary completion still works after the cancels. */ + vector = __hyperv_hypercall(HVCALL_TEST_CODE, pgs_gpa + PAGE_SIZE, + pgs_gpa + 2 * PAGE_SIZE, &status); + GUEST_ASSERT(!vector); + GUEST_ASSERT_EQ(status, USERSPACE_STATUS); + GUEST_SYNC(STAGE_AFTER_CANCEL); + + /* Everything disabled again. */ + vector = __hyperv_hypercall(HVCALL_TEST_CODE, pgs_gpa + PAGE_SIZE, + pgs_gpa + 2 * PAGE_SIZE, &status); + GUEST_ASSERT(!vector); + GUEST_ASSERT_EQ(status & 0xffff, HV_STATUS_INVALID_HYPERCALL_CODE); + GUEST_SYNC(STAGE_REDISABLED); + + GUEST_DONE(); +} + +static void check_hcall_exit(struct kvm_vcpu *vcpu, u64 expect_control, + u64 expect_in, u64 expect_out) +{ + struct kvm_run *run = vcpu->run; + + TEST_ASSERT_EQ(run->exit_reason, KVM_EXIT_HYPERV); + TEST_ASSERT_EQ(run->hyperv.type, KVM_EXIT_HYPERV_HCALL); + TEST_ASSERT_EQ(run->hyperv.u.hcall.input, expect_control); + TEST_ASSERT_EQ(run->hyperv.u.hcall.params[0], expect_in); + TEST_ASSERT_EQ(run->hyperv.u.hcall.params[1], expect_out); +} + +/* + * Cancel the pending hypercall completion, optionally queueing a #UD to + * be delivered at the (unadvanced) hypercall instruction. The cancel + * must leave RIP and RAX alone. + */ +static void cancel_hcall(struct kvm_vcpu *vcpu, bool queue_ud) +{ + struct kvm_vcpu_events events; + struct kvm_regs before, after; + + vcpu_events_get(vcpu, &events); + events.flags |= KVM_VCPUEVENT_CANCEL_HCALL; + if (queue_ud) { + events.flags |= KVM_VCPUEVENT_VALID_PAYLOAD; + events.exception.pending = 1; + events.exception.injected = 0; + events.exception.nr = UD_VECTOR; + events.exception.has_error_code = 0; + events.exception_has_payload = 0; + } + + vcpu_regs_get(vcpu, &before); + vcpu_events_set(vcpu, &events); + vcpu_regs_get(vcpu, &after); + TEST_ASSERT_EQ(before.rip, after.rip); + TEST_ASSERT_EQ(before.rax, after.rax); +} + +/* + * With no cancellable completion pending (nothing pending at all, or a + * non-cancellable completion such as the ucall PIO's), the cancel must + * fail with EBUSY and leave the vCPU's event state as + * KVM_GET_VCPU_EVENTS reported it. + */ +static void check_cancel_refused(struct kvm_vcpu *vcpu) +{ + struct kvm_vcpu_events events, after; + int r; + + vcpu_events_get(vcpu, &events); + events.flags |= KVM_VCPUEVENT_CANCEL_HCALL; + r = __vcpu_ioctl(vcpu, KVM_SET_VCPU_EVENTS, &events); + TEST_ASSERT(r == -1 && errno == EBUSY, + "Cancel with nothing to cancel: r = %d, errno = %d", + r, errno); + + events.flags &= ~KVM_VCPUEVENT_CANCEL_HCALL; + vcpu_events_get(vcpu, &after); + TEST_ASSERT(!memcmp(&events, &after, sizeof(events)), + "Failed cancel changed the vCPU's events"); +} + +static void handle_fast_hcall(struct kvm_vcpu *vcpu) +{ + struct kvm_fpu fpu; + + check_hcall_exit(vcpu, HVCALL_TEST_CODE | HV_HYPERCALL_FAST_BIT, + FAST_PARAM0, FAST_PARAM1); + + vcpu_fpu_get(vcpu, &fpu); + TEST_ASSERT_EQ(*(u64 *)&fpu.xmm[0][0], XMM_IN_MARKER); + TEST_ASSERT_EQ(*(u64 *)&fpu.xmm[0][8], XMM_IN_MARKER + 1); + *(u64 *)&fpu.xmm[1][0] = XMM_OUT_MARKER; + *(u64 *)&fpu.xmm[1][8] = XMM_OUT_MARKER + 1; + vcpu_fpu_set(vcpu, &fpu); + + vcpu->run->hyperv.u.hcall.result = USERSPACE_STATUS; +} + +static void test_feature_mask(struct kvm_vm *vm) +{ + u64 supported = kvm_check_cap(KVM_CAP_HYPERV_FORWARD_HCALL); + int r; + + TEST_ASSERT(supported & KVM_HYPERV_HCALL_FORWARD_UNKNOWN, + "KVM_HYPERV_HCALL_FORWARD_UNKNOWN not reported"); + r = __vm_enable_cap(vm, KVM_CAP_HYPERV_FORWARD_HCALL, BIT_ULL(63)); + TEST_ASSERT(r && errno == EINVAL, + "Enabling an undefined feature should fail with EINVAL"); + + /* Enabling nothing is allowed. */ + vm_enable_cap(vm, KVM_CAP_HYPERV_FORWARD_HCALL, 0); +} + +int main(int argc, char *argv[]) +{ + struct kvm_vcpu *vcpu; + struct kvm_vm *vm; + gva_t hcall_page; + gpa_t hcall_gpa; + struct ucall uc; + int stage = STAGE_DISABLED; + bool retried = false; + + TEST_REQUIRE(kvm_has_cap(KVM_CAP_HYPERV_FORWARD_HCALL)); + TEST_REQUIRE(kvm_check_cap(KVM_CAP_VCPU_EVENTS) & + KVM_VCPU_EVENTS_CANCEL_HCALL); + TEST_REQUIRE(kvm_has_cap(KVM_CAP_EXCEPTION_PAYLOAD)); + + vm = vm_create_with_one_vcpu(&vcpu, guest_code); + vcpu_set_hv_cpuid(vcpu); + vm_enable_cap(vm, KVM_CAP_EXCEPTION_PAYLOAD, 1); + + test_feature_mask(vm); + + /* Hypercall page plus an input and an output page. */ + hcall_page = vm_alloc_pages(vm, 3); + memset(addr_gva2hva(vm, hcall_page), 0x0, 3 * getpagesize()); + hcall_gpa = addr_gva2gpa(vm, hcall_page); + + vcpu_args_set(vcpu, 1, hcall_gpa); + + for (;;) { + vcpu_run(vcpu); + + if (vcpu->run->exit_reason == KVM_EXIT_HYPERV) { + switch (stage) { + case STAGE_MEMORY: + check_hcall_exit(vcpu, HVCALL_TEST_CODE, + hcall_gpa + getpagesize(), + hcall_gpa + 2 * getpagesize()); + vcpu->run->hyperv.u.hcall.result = USERSPACE_STATUS; + break; + case STAGE_FAST: + handle_fast_hcall(vcpu); + break; + case STAGE_RESULT_FULL: + check_hcall_exit(vcpu, HVCALL_TEST_CODE, + hcall_gpa + getpagesize(), + hcall_gpa + 2 * getpagesize()); + vcpu->run->hyperv.u.hcall.result = FULL_RESULT; + break; + case STAGE_CANCEL_UD: + check_hcall_exit(vcpu, HVCALL_TEST_CODE, + hcall_gpa + getpagesize(), + hcall_gpa + 2 * getpagesize()); + cancel_hcall(vcpu, true); + /* Cancelled: the result must never reach the guest. */ + vcpu->run->hyperv.u.hcall.result = USERSPACE_STATUS; + break; + case STAGE_CANCEL_RETRY: + check_hcall_exit(vcpu, HVCALL_TEST_CODE, + hcall_gpa + getpagesize(), + hcall_gpa + 2 * getpagesize()); + if (!retried) { + /* + * Cancel with nothing queued: the same + * hypercall must exit a second time. + */ + cancel_hcall(vcpu, false); + vcpu->run->hyperv.u.hcall.result = 0; + retried = true; + } else { + vcpu->run->hyperv.u.hcall.result = USERSPACE_STATUS; + } + break; + case STAGE_AFTER_CANCEL: + check_hcall_exit(vcpu, HVCALL_TEST_CODE, + hcall_gpa + getpagesize(), + hcall_gpa + 2 * getpagesize()); + vcpu->run->hyperv.u.hcall.result = USERSPACE_STATUS; + break; + default: + TEST_FAIL("Unexpected hypercall exit in stage %d", stage); + } + continue; + } + + TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO); + + switch (get_ucall(vcpu, &uc)) { + case UCALL_SYNC: + TEST_ASSERT_EQ(uc.args[1], stage); + switch (stage) { + case STAGE_DISABLED: + vm_enable_cap(vm, KVM_CAP_HYPERV_FORWARD_HCALL, + KVM_HYPERV_HCALL_FORWARD_UNKNOWN); + break; + case STAGE_CANCEL_RETRY: + /* + * The ucall PIO's own completion is pending + * here: not cancellable, even with the + * feature enabled. + */ + check_cancel_refused(vcpu); + break; + case STAGE_AFTER_CANCEL: + vm_enable_cap(vm, KVM_CAP_HYPERV_FORWARD_HCALL, 0); + break; + } + stage++; + break; + case UCALL_ABORT: + REPORT_GUEST_ASSERT(uc); + case UCALL_DONE: + goto done; + default: + TEST_FAIL("Unhandled ucall: %ld", uc.cmd); + } + } + +done: + TEST_ASSERT_EQ(stage, STAGE_REDISABLED + 1); + kvm_vm_free(vm); + return 0; +} -- 2.50.1