[RFC PATCH v2 26/31] KVM: selftests: Check that a VTL switch keeps bit 63 of RAX

From: David Woodhouse

Date: Fri Nov 13 2026 - 15:15:01 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

An in-kernel VTL call or VTL return hands the entered plane a full
64-bit RAX: the caller's RAX on a call, and on a non-fast return the
lower VTL's RAX from the returning plane's control area. Bit 63 of
that value is the guest's, and must arrive intact whatever the VMM has
enabled.

Make plane 1 set up its VP assist page and return to plane 0, then
have plane 0 make a VTL call with bit 63 of RAX set, and plane 1
answer with a non-fast return whose control area carries a RAX and an
RCX with bit 63 set. Check that each plane sees exactly the value it
was given, with and without KVM_HYPERV_HCALL_RAISE_UD.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../kvm/x86/planes_vtl_return_test.c | 204 ++++++++++++++++++
2 files changed, 205 insertions(+)
create mode 100644 tools/testing/selftests/kvm/x86/planes_vtl_return_test.c

diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 42b0bf7e8128..2b8a238c32e8 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -118,6 +118,7 @@ TEST_GEN_PROGS_x86 += x86/planes_vtl_entry_test
TEST_GEN_PROGS_x86 += x86/planes_vtl_timer_mask_test
TEST_GEN_PROGS_x86 += x86/planes_vtl_restore_test
TEST_GEN_PROGS_x86 += x86/planes_vtl_nmi_halt_test
+TEST_GEN_PROGS_x86 += x86/planes_vtl_return_test
TEST_GEN_PROGS_x86 += x86/pmu_counters_test
TEST_GEN_PROGS_x86 += x86/pmu_event_filter_test
TEST_GEN_PROGS_x86 += x86/private_mem_conversions_test
diff --git a/tools/testing/selftests/kvm/x86/planes_vtl_return_test.c b/tools/testing/selftests/kvm/x86/planes_vtl_return_test.c
new file mode 100644
index 000000000000..ed3e1cd0a085
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/planes_vtl_return_test.c
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Test that an in-kernel VTL call and VTL return hand the entered plane
+ * its full 64-bit RAX.
+ *
+ * Plane 1 enables its VP assist page and returns to plane 0, as a secure
+ * kernel does once it has booted. Plane 0 then makes a VTL call with bit
+ * 63 of RAX set, and plane 1 answers with a non-fast VTL return whose
+ * control area restores a RAX and RCX that also have bit 63 set. Both
+ * resume values are guest registers and must arrive intact, whether or
+ * not KVM_HYPERV_HCALL_RAISE_UD is enabled.
+ */
+#include <errno.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <unistd.h>
+
+#include "test_util.h"
+#include "kvm_util.h"
+#include "processor.h"
+#include "hyperv.h"
+
+#define HVCALL_VTL_CALL 0x0011
+#define HVCALL_VTL_RETURN 0x0012
+
+/* struct hv_vp_vtl_control sits at offset 8 of the VP assist page. */
+#define VTL_CONTROL_OFFSET 8
+#define VTL_ENTRY_REASON_CALL 1
+
+#define CALL_RAX 0x8000000000005a5aULL
+#define RETURN_RAX 0xffff8000deadbeefULL
+#define RETURN_RCX 0x8000000012345678ULL
+
+#define TWIN_STACK_PAGES 4
+
+struct vtl_control {
+ u32 vtl_entry_reason;
+ u8 vina_asserted;
+ u8 reserved[3];
+ u64 vtl_ret_x64rax;
+ u64 vtl_ret_x64rcx;
+} __packed;
+
+/* What plane 1 saw when plane 0's VTL call entered it. */
+static u64 twin_entry_rax;
+static u32 twin_entry_reason;
+
+/*
+ * Every GPR but RSP is shared with the other plane across the switch.
+ * RSP is private, so keep the callee-saved registers on this plane's
+ * stack (below the red zone) and declare the rest clobbered.
+ */
+static inline void vtl_hypercall(u64 code, u64 *rax, u64 *rcx)
+{
+ u64 a = *rax, c = code;
+
+ asm volatile("sub $128, %%rsp\n\t"
+ "push %%rbx\n\t"
+ "push %%rbp\n\t"
+ "push %%r12\n\t"
+ "push %%r13\n\t"
+ "push %%r14\n\t"
+ "push %%r15\n\t"
+ "vmcall\n\t"
+ "pop %%r15\n\t"
+ "pop %%r14\n\t"
+ "pop %%r13\n\t"
+ "pop %%r12\n\t"
+ "pop %%rbp\n\t"
+ "pop %%rbx\n\t"
+ "add $128, %%rsp\n\t"
+ : "+a"(a), "+c"(c)
+ :
+ : "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11",
+ "cc", "memory");
+ *rax = a;
+ *rcx = c;
+}
+
+static void twin_guest_code(gva_t assist_gva, gpa_t assist_gpa)
+{
+ struct vtl_control *ctl =
+ (void *)(assist_gva + VTL_CONTROL_OFFSET);
+ u64 rax = 0, rcx;
+
+ wrmsr(HV_X64_MSR_GUEST_OS_ID, HYPERV_LINUX_OS_ID);
+ wrmsr(HV_X64_MSR_VP_ASSIST_PAGE,
+ assist_gpa | HV_X64_MSR_VP_ASSIST_PAGE_ENABLE);
+
+ /* Set up: hand control to plane 0, which has not run yet. */
+ vtl_hypercall(HVCALL_VTL_RETURN, &rax, &rcx);
+
+ /* Entered by plane 0's VTL call. */
+ twin_entry_rax = rax;
+ twin_entry_reason = ctl->vtl_entry_reason;
+
+ ctl->vtl_ret_x64rax = RETURN_RAX;
+ ctl->vtl_ret_x64rcx = RETURN_RCX;
+ rax = 0;
+ vtl_hypercall(HVCALL_VTL_RETURN, &rax, &rcx);
+
+ for (;;)
+ asm volatile("hlt");
+}
+
+static void plane0_guest_code(void)
+{
+ u64 rax = CALL_RAX, rcx;
+
+ wrmsr(HV_X64_MSR_GUEST_OS_ID, HYPERV_LINUX_OS_ID);
+ vtl_hypercall(HVCALL_VTL_CALL, &rax, &rcx);
+
+ GUEST_ASSERT_EQ(rax, RETURN_RAX);
+ GUEST_ASSERT_EQ(rcx, RETURN_RCX);
+ GUEST_DONE();
+}
+
+static void run_test(u64 features)
+{
+ struct kvm_vcpu *vcpu0;
+ struct kvm_sregs sregs;
+ struct kvm_regs regs;
+ struct kvm_vm *vm;
+ gva_t assist;
+ int plane_fd, twin_fd, r;
+ struct ucall uc;
+ u64 state;
+
+ kvm_force_split_irqchip = true;
+ vm = __vm_create(VM_SHAPE_DEFAULT, 1, 0);
+ kvm_force_split_irqchip = false;
+
+ TEST_REQUIRE(__vm_enable_cap(vm, KVM_CAP_PLANES, 2) == 0);
+ vm_enable_cap(vm, KVM_CAP_HYPERV_FORWARD_HCALL, features);
+
+ vcpu0 = vm_vcpu_add(vm, 0, plane0_guest_code);
+ vcpu_set_hv_cpuid(vcpu0);
+
+ plane_fd = __vm_ioctl(vm, KVM_CREATE_PLANE, (void *)1UL);
+ TEST_ASSERT(plane_fd >= 0, "KVM_CREATE_PLANE failed: errno %d", errno);
+ twin_fd = __kvm_ioctl(plane_fd, KVM_CREATE_VCPU, NULL);
+ TEST_ASSERT(twin_fd >= 0, "KVM_CREATE_VCPU on plane 1 failed: errno %d",
+ errno);
+
+ assist = vm_alloc_page(vm);
+ memset(addr_gva2hva(vm, assist), 0, getpagesize());
+
+ /* Same mode and page tables as plane 0, own code and stack. */
+ vcpu_sregs_get(vcpu0, &sregs);
+ r = ioctl(twin_fd, KVM_SET_SREGS, &sregs);
+ TEST_ASSERT(!r, "KVM_SET_SREGS on twin failed: errno %d", errno);
+ r = ioctl(twin_fd, KVM_GET_REGS, &regs);
+ TEST_ASSERT(!r, "KVM_GET_REGS on twin failed: errno %d", errno);
+ regs.rip = (unsigned long)twin_guest_code;
+ regs.rsp = vm_alloc_stack(vm, TWIN_STACK_PAGES);
+ regs.rdi = assist;
+ regs.rsi = addr_gva2gpa(vm, assist);
+ regs.rflags = 0x2;
+ r = ioctl(twin_fd, KVM_SET_REGS, &regs);
+ TEST_ASSERT(!r, "KVM_SET_REGS on twin failed: errno %d", errno);
+
+ /* Plane 1 runs first; plane 0 is left stopped without a VMM hold. */
+ state = 0;
+ r = ioctl(twin_fd, KVM_SET_PLANE_RUN_STATE, &state);
+ TEST_ASSERT(!r, "KVM_SET_PLANE_RUN_STATE on twin failed: errno %d",
+ errno);
+
+ vcpu_run(vcpu0);
+ TEST_ASSERT_KVM_EXIT_REASON(vcpu0, KVM_EXIT_IO);
+ switch (get_ucall(vcpu0, &uc)) {
+ case UCALL_DONE:
+ break;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ default:
+ TEST_FAIL("Unexpected ucall %lu", uc.cmd);
+ }
+
+ sync_global_from_guest(vm, twin_entry_rax);
+ sync_global_from_guest(vm, twin_entry_reason);
+ TEST_ASSERT_EQ(twin_entry_reason, VTL_ENTRY_REASON_CALL);
+ TEST_ASSERT_EQ(twin_entry_rax, CALL_RAX);
+
+ close(twin_fd);
+ close(plane_fd);
+ kvm_vm_free(vm);
+}
+
+int main(int argc, char *argv[])
+{
+ u64 supported, vtl = KVM_HYPERV_HCALL_VTL_CALLS |
+ KVM_HYPERV_HCALL_VTL_SWITCH;
+
+ TEST_REQUIRE(kvm_has_cap(KVM_CAP_PLANES));
+ TEST_REQUIRE(kvm_has_cap(KVM_CAP_HYPERV_FORWARD_HCALL));
+ supported = kvm_check_cap(KVM_CAP_HYPERV_FORWARD_HCALL);
+ TEST_REQUIRE((supported & vtl) == vtl);
+
+ run_test(vtl);
+ if (supported & KVM_HYPERV_HCALL_RAISE_UD)
+ run_test(vtl | KVM_HYPERV_HCALL_RAISE_UD);
+
+ return 0;
+}
--
2.50.1