As requested, a half-baked selftest. :) I dropped stable from the cc list. When a vCPU is destroyed while L2 is active and VMCS shadowing is enabled, KVM synthesizes a nested VM-Exit. This flushes the cached shadow VMCS12 back to guest memory. However, on process exit, do_exit() calls exit_mm() before closing file descriptors. KVM teardown runs with current->mm == NULL on a borrowed lazy TLB active_mm. Consequently, nested_flush_cached_shadow_vmcs12() writes the shadow VMCS12 into whatever address space is active on that CPU. Add a selftest to verify this behavior. The test runs a victim process and a nested VMM process on the same physical CPU. The victim process maps a page and fills it with canary bytes. The VMM sets up an L2 guest with VMCS shadowing mapped at the identical host virtual address using MAP_FIXED_NOREPLACE. When the VMM exits with open file descriptors, the victim yields the CPU while the VMM terminates. This scheduling heuristic attempts to hit the race window where VMM teardown runs under the victim's active_mm and flushes the shadow VMCS into the victim's address space. --- tools/testing/selftests/kvm/Makefile.kvm | 1 + .../kvm/x86/vmx_shadow_vmcs_teardown_test.c | 317 ++++++++++++++++++ 2 files changed, 318 insertions(+) create mode 100644 tools/testing/selftests/kvm/x86/vmx_shadow_vmcs_teardown_test.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index 5e2fcee198f2..2aa9cc75fbd7 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -143,6 +143,7 @@ TEST_GEN_PROGS_x86 += x86/vmx_exception_with_invalid_guest_state TEST_GEN_PROGS_x86 += x86/vmx_msrs_test TEST_GEN_PROGS_x86 += x86/vmx_invalid_nested_guest_state TEST_GEN_PROGS_x86 += x86/vmx_nested_la57_state_test +TEST_GEN_PROGS_x86 += x86/vmx_shadow_vmcs_teardown_test TEST_GEN_PROGS_x86 += x86/apic_bus_clock_test TEST_GEN_PROGS_x86 += x86/xapic_ipi_test TEST_GEN_PROGS_x86 += x86/xapic_state_test diff --git a/tools/testing/selftests/kvm/x86/vmx_shadow_vmcs_teardown_test.c b/tools/testing/selftests/kvm/x86/vmx_shadow_vmcs_teardown_test.c new file mode 100644 index 000000000000..e0863ef20d3e --- /dev/null +++ b/tools/testing/selftests/kvm/x86/vmx_shadow_vmcs_teardown_test.c @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * vmx_shadow_vmcs_teardown_test + * + * Verify that destroying a nested VMM with active shadow VMCS12 does not + * flush the cached shadow VMCS into an unrelated process memory space + * during process exit in lazy TLB mode. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "test_util.h" +#include "kvm_util.h" +#include "processor.h" +#include "vmx.h" +#include "kselftest.h" + +#define PORT_L0_EXIT 0x2000 +#define SHADOW_VMCS_GPA 0x80000000ULL +#define SHADOW_VMCS_GVA 0x80000000ULL +#define CANARY_BYTE 0x5a +#define L2_GUEST_STACK_SIZE 64 + +struct test_result { + bool corrupted; + size_t corrupt_offset; + uint8_t corrupt_val; +}; + +static void l2_guest_code(void) +{ + /* Exit to L0 */ + asm volatile("inb %%dx, %%al" + : : [port] "d" (PORT_L0_EXIT) : "rax"); +} + +static void l1_guest_code(struct vmx_pages *vmx_pages) +{ + unsigned long l2_guest_stack[L2_GUEST_STACK_SIZE]; + + GUEST_ASSERT(prepare_for_vmx_operation(vmx_pages)); + GUEST_ASSERT(load_vmcs(vmx_pages)); + + /* Prepare the VMCS for L2 execution. */ + prepare_vmcs(vmx_pages, l2_guest_code, + &l2_guest_stack[L2_GUEST_STACK_SIZE]); + + /* Enable VMCS shadowing and set the shadow VMCS link pointer. */ + vmwrite(SECONDARY_VM_EXEC_CONTROL, + vmreadz(SECONDARY_VM_EXEC_CONTROL) | SECONDARY_EXEC_SHADOW_VMCS); + vmwrite(VMCS_LINK_POINTER, vmx_pages->shadow_vmcs_gpa); + + vmlaunch(); + GUEST_FAIL("L1 guest must not regain control"); +} + +static bool kvm_cpu_has_shadow_vmcs(void) +{ + uint64_t ctrl; + + ctrl = kvm_get_feature_msr(MSR_IA32_VMX_TRUE_PROCBASED_CTLS) >> 32; + if (!(ctrl & CPU_BASED_ACTIVATE_SECONDARY_CONTROLS)) + return false; + + ctrl = kvm_get_feature_msr(MSR_IA32_VMX_PROCBASED_CTLS2) >> 32; + return ctrl & SECONDARY_EXEC_SHADOW_VMCS; +} + +static void run_vmm(int pcpu, void *target_hva, int c2_to_c1_fd) +{ + vm_vaddr_t vmx_pages_gva; + struct kvm_vcpu *vcpu; + struct kvm_vm *vm; + struct vmx_pages *vmx; + void *shadow_hva; + + pin_self_to_cpu(pcpu); + + vm = vm_create_with_one_vcpu(&vcpu, l1_guest_code); + + /* Allocate VMX pages and shared descriptors. */ + vcpu_alloc_vmx(vm, &vmx_pages_gva); + vmx = addr_gva2hva(vm, vmx_pages_gva); + + /* + * Map the shadow VMCS page at the exact target_hva allocated by the + * victim so it collides with the victim process's mapping. + */ + shadow_hva = mmap(target_hva, PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0); + TEST_ASSERT(shadow_hva == target_hva, "mmap target_hva failed in VMM"); + memset(shadow_hva, 0, PAGE_SIZE); + + /* Add a caller-managed memslot for the shadow VMCS backing page. */ + vm_userspace_mem_region_add_caller_managed(vm, shadow_hva, + SHADOW_VMCS_GPA, 10, 1, 0); + virt_pg_map(vm, SHADOW_VMCS_GVA, SHADOW_VMCS_GPA); + + /* Override the shadow VMCS pointers in vmx_pages. */ + vmx->shadow_vmcs = (void *)SHADOW_VMCS_GVA; + vmx->shadow_vmcs_hva = shadow_hva; + vmx->shadow_vmcs_gpa = SHADOW_VMCS_GPA; + + vcpu_args_set(vcpu, 1, vmx_pages_gva); + + for (;;) { + struct kvm_run *run = vcpu->run; + struct ucall uc; + + vcpu_run(vcpu); + TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO); + + if (run->io.port == PORT_L0_EXIT) + break; + + switch (get_ucall(vcpu, &uc)) { + case UCALL_ABORT: + REPORT_GUEST_ASSERT(uc); + /* NOT REACHED */ + default: + TEST_FAIL("Unknown ucall %lu", uc.cmd); + } + } + + /* + * Signal the victim process that the VMM is about to terminate + * while L2 is active with VMCS shadowing enabled. + */ + TEST_ASSERT(write(c2_to_c1_fd, "X", 1) == 1, "write to victim failed"); + + /* + * Exit immediately without closing VM or vCPU file descriptors. + * The kernel closes them in exit_files() after exit_mm() sets + * current->mm = NULL. + */ + _exit(0); +} + +static void run_victim(int pcpu, int ready_fd, int c2_to_c1_fd, + int p_to_c1_fd, int result_fd) +{ + struct test_result result = { .corrupted = false }; + void *victim_hva; + char sync_byte; + size_t i; + int flags; + + pin_self_to_cpu(pcpu); + + victim_hva = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + TEST_ASSERT(victim_hva != MAP_FAILED, "mmap failed in victim"); + memset(victim_hva, CANARY_BYTE, PAGE_SIZE); + + /* Send mapped virtual address to parent so VMM can collide with it. */ + TEST_ASSERT(write(ready_fd, &victim_hva, sizeof(victim_hva)) == sizeof(victim_hva), + "write ready failed"); + + /* Wait for VMM to launch L2 and reach exit to userspace. */ + TEST_ASSERT(read(c2_to_c1_fd, &sync_byte, 1) == 1, "read sync failed"); + + /* Set parent-to-victim pipe to non-blocking. */ + flags = fcntl(p_to_c1_fd, F_GETFL, 0); + TEST_ASSERT(flags >= 0, "fcntl F_GETFL failed"); + TEST_ASSERT(fcntl(p_to_c1_fd, F_SETFL, flags | O_NONBLOCK) == 0, + "fcntl F_SETFL failed"); + + /* + * Loop yielding the CPU while the VMM terminates. When the VMM + * sleeps in synchronize_srcu() during teardown, the scheduler + * runs this victim process on the CPU. When the VMM wakes up, + * the scheduler context switches from this victim to the VMM + * with current->mm == NULL, leaving the victim's CR3 active. + */ + for (;;) { + ssize_t ret = read(p_to_c1_fd, &sync_byte, 1); + + if (ret == 1) + break; + if (ret < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + sched_yield(); + continue; + } + TEST_FAIL("read from parent pipe failed: ret=%zd, errno=%d", + ret, errno); + } + + /* Verify that the entire page remained untouched. */ + for (i = 0; i < PAGE_SIZE; i++) { + if (((uint8_t *)victim_hva)[i] != CANARY_BYTE) { + result.corrupted = true; + result.corrupt_offset = i; + result.corrupt_val = ((uint8_t *)victim_hva)[i]; + break; + } + } + + TEST_ASSERT(write(result_fd, &result, sizeof(result)) == sizeof(result), + "write result failed"); + _exit(0); +} + +static bool run_iteration(int pcpu) +{ + int pipe_ready[2]; + int pipe_c2_to_c1[2]; + int pipe_p_to_c1[2]; + int pipe_result[2]; + pid_t pid_victim; + pid_t pid_vmm; + struct test_result result = {}; + void *target_hva = NULL; + int status; + + TEST_ASSERT(pipe(pipe_ready) == 0, "pipe failed"); + TEST_ASSERT(pipe(pipe_c2_to_c1) == 0, "pipe failed"); + TEST_ASSERT(pipe(pipe_p_to_c1) == 0, "pipe failed"); + TEST_ASSERT(pipe(pipe_result) == 0, "pipe failed"); + + pid_victim = fork(); + TEST_ASSERT(pid_victim >= 0, "fork victim failed"); + if (pid_victim == 0) { + close(pipe_ready[0]); + close(pipe_c2_to_c1[1]); + close(pipe_p_to_c1[1]); + close(pipe_result[0]); + run_victim(pcpu, pipe_ready[1], pipe_c2_to_c1[0], + pipe_p_to_c1[0], pipe_result[1]); + } + + close(pipe_ready[1]); + close(pipe_result[1]); + close(pipe_p_to_c1[0]); + + /* Wait for victim to initialize its mapping and retrieve its address. */ + TEST_ASSERT(read(pipe_ready[0], &target_hva, sizeof(target_hva)) == sizeof(target_hva), + "wait ready failed"); + close(pipe_ready[0]); + + pid_vmm = fork(); + TEST_ASSERT(pid_vmm >= 0, "fork VMM failed"); + if (pid_vmm == 0) { + close(pipe_c2_to_c1[0]); + close(pipe_p_to_c1[1]); + close(pipe_result[0]); + run_vmm(pcpu, target_hva, pipe_c2_to_c1[1]); + } + + close(pipe_c2_to_c1[0]); + close(pipe_c2_to_c1[1]); + + /* Wait for VMM to completely terminate. */ + waitpid(pid_vmm, &status, 0); + TEST_ASSERT(WIFEXITED(status) && WEXITSTATUS(status) == 0, + "VMM child failed with status %d", status); + + /* Signal victim to exit yield loop. */ + TEST_ASSERT(write(pipe_p_to_c1[1], "D", 1) == 1, "signal victim failed"); + close(pipe_p_to_c1[1]); + + /* Read result from victim. */ + TEST_ASSERT(read(pipe_result[0], &result, sizeof(result)) == sizeof(result), + "read result failed"); + close(pipe_result[0]); + + waitpid(pid_victim, &status, 0); + TEST_ASSERT(WIFEXITED(status) && WEXITSTATUS(status) == 0, + "Victim child failed with status %d", status); + + if (result.corrupted) + pr_info("Memory check: corruption at offset 0x%zx: expected 0x%02x, actual 0x%02x\n", + result.corrupt_offset, CANARY_BYTE, result.corrupt_val); + else + pr_info("Memory check: entire page intact (canary = 0x%02x)\n", + CANARY_BYTE); + + return result.corrupted; +} + +int main(int argc, char *argv[]) +{ + bool corrupted = false; + int pcpu; + int i; + const int iterations = 5; + + TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_VMX)); + TEST_REQUIRE(kvm_cpu_has_shadow_vmcs()); + + pcpu = pin_self_to_any_cpu(); + + pr_info("Testing shadow VMCS teardown on pCPU %d (%d iterations)...\n", + pcpu, iterations); + + for (i = 0; i < iterations; i++) { + if (run_iteration(pcpu)) { + corrupted = true; + break; + } + } + + TEST_ASSERT(!corrupted, + "Memory corruption detected! Active MM was corrupted during vCPU teardown: canary byte 0x%02x overwritten\n", + CANARY_BYTE); + + pr_info("PASS: No memory corruption detected during teardown.\n"); + return 0; +} -- 2.55.0.1032.g73a4cd73de-goog