| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * Copyright (C) 2026, Google LLC. |
| */ |
| #include "kvm_util.h" |
| #include "vmx.h" |
| #include "svm_util.h" |
| #include "kselftest.h" |
| |
| /* |
| * Allocate two VMCB pages for testing. Both pages have different GVAs (shared |
| * by both L1 and L2) and L1 GPAs. A single L2 GPA is used such that: |
| * - L2 GPA == L1 GPA for VMCB0. |
| * - L2 GPA is mapped to L1 GPA for VMCB1 using NPT in L1. |
| * |
| * This allows testing whether the GPA used by VMSAVE/VMLOAD in L2 is |
| * interpreted as a direct L1 GPA or translated using NPT as an L2 GPA, depends |
| * on which VMCB is accessed. |
| */ |
| #define TEST_MEM_SLOT_INDEX 1 |
| #define TEST_MEM_PAGES 2 |
| #define TEST_MEM_BASE 0xc0000000 |
| |
| #define TEST_GUEST_ADDR(idx) (TEST_MEM_BASE + (idx) * PAGE_SIZE) |
| |
| #define TEST_VMCB_L1_GPA(idx) TEST_GUEST_ADDR(idx) |
| #define TEST_VMCB_GVA(idx) TEST_GUEST_ADDR(idx) |
| |
| #define TEST_VMCB_L2_GPA TEST_VMCB_L1_GPA(0) |
| |
| static void l2_guest_code_vmsave(void) |
| { |
| asm volatile("vmsave %0" : : "a"(TEST_VMCB_L2_GPA) : "memory"); |
| } |
| |
| static void l2_guest_code_vmload(void) |
| { |
| asm volatile("vmload %0" : : "a"(TEST_VMCB_L2_GPA) : "memory"); |
| } |
| |
| static void l2_guest_code_vmcb(int vmcb_idx) |
| { |
| wrmsr(MSR_KERNEL_GS_BASE, 0xaaaa); |
| l2_guest_code_vmsave(); |
| |
| /* Verify the VMCB used by VMSAVE and update KERNEL_GS_BASE to 0xbbbb */ |
| GUEST_SYNC(vmcb_idx); |
| |
| l2_guest_code_vmload(); |
| GUEST_ASSERT_EQ(rdmsr(MSR_KERNEL_GS_BASE), 0xbbbb); |
| |
| /* Reset MSR_KERNEL_GS_BASE */ |
| wrmsr(MSR_KERNEL_GS_BASE, 0); |
| l2_guest_code_vmsave(); |
| |
| vmmcall(); |
| } |
| |
| static void l2_guest_code_vmcb0(void) |
| { |
| l2_guest_code_vmcb(0); |
| } |
| |
| static void l2_guest_code_vmcb1(void) |
| { |
| l2_guest_code_vmcb(1); |
| } |
| |
| static void l1_guest_code(struct svm_test_data *svm) |
| { |
| /* Each test case initializes the guest RIP below */ |
| generic_svm_setup(svm, NULL); |
| |
| /* Set VMSAVE/VMLOAD intercepts and make sure they work with.. */ |
| svm->vmcb->control.intercept |= (BIT_ULL(INTERCEPT_VMSAVE) | |
| BIT_ULL(INTERCEPT_VMLOAD)); |
| |
| /* ..SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE cleared.. */ |
| svm->vmcb->control.misc_ctl2 &= ~SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE; |
| |
| svm->vmcb->save.rip = (u64)l2_guest_code_vmsave; |
| run_guest(svm->vmcb, svm->vmcb_gpa); |
| GUEST_ASSERT_EQ(svm->vmcb->control.exit_code, SVM_EXIT_VMSAVE); |
| |
| svm->vmcb->save.rip = (u64)l2_guest_code_vmload; |
| run_guest(svm->vmcb, svm->vmcb_gpa); |
| GUEST_ASSERT_EQ(svm->vmcb->control.exit_code, SVM_EXIT_VMLOAD); |
| |
| /* ..and SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE set */ |
| svm->vmcb->control.misc_ctl2 |= SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE; |
| |
| svm->vmcb->save.rip = (u64)l2_guest_code_vmsave; |
| run_guest(svm->vmcb, svm->vmcb_gpa); |
| GUEST_ASSERT_EQ(svm->vmcb->control.exit_code, SVM_EXIT_VMSAVE); |
| |
| svm->vmcb->save.rip = (u64)l2_guest_code_vmload; |
| run_guest(svm->vmcb, svm->vmcb_gpa); |
| GUEST_ASSERT_EQ(svm->vmcb->control.exit_code, SVM_EXIT_VMLOAD); |
| |
| /* Now clear the intercepts to test VMSAVE/VMLOAD behavior */ |
| svm->vmcb->control.intercept &= ~(BIT_ULL(INTERCEPT_VMSAVE) | |
| BIT_ULL(INTERCEPT_VMLOAD)); |
| |
| /* |
| * Without SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE, the GPA will be |
| * interpreted as an L1 GPA, so VMCB0 should be used. |
| */ |
| svm->vmcb->save.rip = (u64)l2_guest_code_vmcb0; |
| svm->vmcb->control.misc_ctl2 &= ~SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE; |
| run_guest(svm->vmcb, svm->vmcb_gpa); |
| GUEST_ASSERT_EQ(svm->vmcb->control.exit_code, SVM_EXIT_VMMCALL); |
| |
| /* |
| * With SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE, the GPA will be interpeted as |
| * an L2 GPA, and translated through the NPT to VMCB1. |
| */ |
| svm->vmcb->save.rip = (u64)l2_guest_code_vmcb1; |
| svm->vmcb->control.misc_ctl2 |= SVM_MISC2_ENABLE_V_VMLOAD_VMSAVE; |
| run_guest(svm->vmcb, svm->vmcb_gpa); |
| GUEST_ASSERT_EQ(svm->vmcb->control.exit_code, SVM_EXIT_VMMCALL); |
| |
| GUEST_DONE(); |
| } |
| |
| int main(int argc, char *argv[]) |
| { |
| gva_t nested_gva = 0; |
| struct vmcb *test_vmcb[2]; |
| struct kvm_vcpu *vcpu; |
| struct kvm_vm *vm; |
| int i; |
| |
| TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_SVM)); |
| TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_NPT)); |
| TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_V_VMSAVE_VMLOAD)); |
| |
| vm = vm_create_with_one_vcpu(&vcpu, l1_guest_code); |
| vm_enable_tdp(vm); |
| |
| vcpu_alloc_svm(vm, &nested_gva); |
| vcpu_args_set(vcpu, 1, nested_gva); |
| |
| vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, |
| TEST_MEM_BASE, TEST_MEM_SLOT_INDEX, |
| TEST_MEM_PAGES, 0); |
| |
| for (i = 0; i <= 1; i++) { |
| virt_map(vm, TEST_VMCB_GVA(i), TEST_VMCB_L1_GPA(i), 1); |
| test_vmcb[i] = (struct vmcb *)addr_gva2hva(vm, TEST_VMCB_GVA(i)); |
| } |
| |
| tdp_identity_map_default_memslots(vm); |
| |
| /* |
| * L2 GPA == L1_GPA(0), but map it to L1_GPA(1), to allow testing |
| * whether the L2 GPA is interpreted as an L1 GPA or translated through |
| * the NPT. |
| */ |
| TEST_ASSERT_EQ(TEST_VMCB_L2_GPA, TEST_VMCB_L1_GPA(0)); |
| tdp_map(vm, TEST_VMCB_L2_GPA, TEST_VMCB_L1_GPA(1), PAGE_SIZE); |
| |
| for (;;) { |
| struct ucall uc; |
| |
| vcpu_run(vcpu); |
| TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO); |
| |
| switch (get_ucall(vcpu, &uc)) { |
| case UCALL_ABORT: |
| REPORT_GUEST_ASSERT(uc); |
| case UCALL_SYNC: |
| i = uc.args[1]; |
| TEST_ASSERT(i == 0 || i == 1, "Unexpected VMCB idx: %d", i); |
| |
| /* |
| * Check that only the expected VMCB has KERNEL_GS_BASE |
| * set to 0xaaaa, and update it to 0xbbbb. |
| */ |
| TEST_ASSERT_EQ(test_vmcb[i]->save.kernel_gs_base, 0xaaaa); |
| TEST_ASSERT_EQ(test_vmcb[1-i]->save.kernel_gs_base, 0); |
| test_vmcb[i]->save.kernel_gs_base = 0xbbbb; |
| break; |
| case UCALL_DONE: |
| goto done; |
| default: |
| TEST_FAIL("Unknown ucall %lu", uc.cmd); |
| } |
| } |
| |
| done: |
| kvm_vm_free(vm); |
| return 0; |
| } |