Exercise delivery of an asynchronous page fault from L2 to L1 as a synthetic #PF VM-Exit. Hold L2's backing page missing with userfaultfd and bound the critical KVM_RUN with a signal-based timeout. On an affected kernel, KVM queues the nested VM-Exit without requesting event processing, re-enters L2, and blocks on the same missing page. The test detects the timeout and verifies that L1's APF record still contains KVM_PV_REASON_PAGE_NOT_PRESENT. On a fixed kernel, verify that L1 receives an exception VM-Exit with a #PF, error code zero, a nonzero APF token, and a matching page-not-present reason. Skip the test when VMX, EPT, the nested APF features, or userfaultfd fault handling are unavailable. Signed-off-by: Loc Nguyen --- tools/testing/selftests/kvm/Makefile.kvm | 1 + .../selftests/kvm/x86/nested_apf_event_test.c | 306 ++++++++++++++++++ 2 files changed, 307 insertions(+) create mode 100644 tools/testing/selftests/kvm/x86/nested_apf_event_test.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index b31ee61af284..a882c1f8a5fb 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -92,6 +92,7 @@ TEST_GEN_PROGS_x86 += x86/kvm_pv_test TEST_GEN_PROGS_x86 += x86/kvm_buslock_test TEST_GEN_PROGS_x86 += x86/monitor_mwait_test TEST_GEN_PROGS_x86 += x86/msrs_test +TEST_GEN_PROGS_x86 += x86/nested_apf_event_test TEST_GEN_PROGS_x86 += x86/nested_close_kvm_test TEST_GEN_PROGS_x86 += x86/nested_dirty_log_test TEST_GEN_PROGS_x86 += x86/nested_emulation_test diff --git a/tools/testing/selftests/kvm/x86/nested_apf_event_test.c b/tools/testing/selftests/kvm/x86/nested_apf_event_test.c new file mode 100644 index 000000000000..97e23ef91ca6 --- /dev/null +++ b/tools/testing/selftests/kvm/x86/nested_apf_event_test.c @@ -0,0 +1,306 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright (C) 2026, Ryzome + * + * Reproduce a lost nested asynchronous page-fault notification. + * + * L2 faults on a page held missing by userfaultfd. KVM writes + * PAGE_NOT_PRESENT to L1's APF record and queues a synthetic #PF VM-exit. + * The vulnerable path omits KVM_REQ_EVENT, re-enters L2, and loses the + * notification before L1 can consume it. A fixed L0 exits to L1 immediately. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "kvm_util.h" +#include "processor.h" +#include "test_util.h" +#include "vmx.h" + +#define APF_VECTOR 0xF1U +#define RUN_TIMEOUT_SECONDS 3U +#define VMX_INTR_INFO_VECTOR_MASK 0xFFU +#define VMX_INTR_INFO_TYPE_MASK 0x700U +#define VMX_INTR_INFO_HARD_EXCEPTION (3U << 8) +#define VMX_INTR_INFO_DELIVER_CODE BIT(11) +#define VMX_INTR_INFO_VALID BIT(31) + +enum test_stage { + STAGE_APF_ARMED = 1, + STAGE_APF_DELIVERED, +}; + +static struct kvm_vcpu_pv_apf_data apf_data __aligned(64); +static gva_t l2_target_gva; + +static void l2_guest_code(void) +{ + u64 value; + + value = READ_ONCE(*(u64 *)READ_ONCE(l2_target_gva)); + GUEST_FAIL("L2 resumed after the missing page was resolved: %#lx", value); +} + +static void l1_guest_code(struct vmx_pages *vmx, gpa_t apf_data_gpa) +{ + u64 apf_msr; + u64 intr_info; + u64 reason; + u64 token; + + GUEST_ASSERT(this_cpu_has(X86_FEATURE_KVM_ASYNC_PF)); + GUEST_ASSERT(this_cpu_has(X86_FEATURE_KVM_ASYNC_PF_VMEXIT)); + GUEST_ASSERT(this_cpu_has(X86_FEATURE_KVM_ASYNC_PF_INT)); + prepare_for_vmx_operation(vmx); + load_vmcs(vmx); + prepare_vmcs(vmx, l2_guest_code); + vmwrite(GUEST_RFLAGS, + X86_EFLAGS_FIXED | X86_EFLAGS_IF); + + wrmsr(MSR_KVM_ASYNC_PF_INT, APF_VECTOR); + apf_msr = apf_data_gpa | KVM_ASYNC_PF_ENABLED | + KVM_ASYNC_PF_SEND_ALWAYS | + KVM_ASYNC_PF_DELIVERY_AS_PF_VMEXIT | + KVM_ASYNC_PF_DELIVERY_AS_INT; + wrmsr(MSR_KVM_ASYNC_PF_EN, apf_msr); + + /* + * Let userspace discard and userfaultfd-register the target only after + * all L1 setup exits have completed. The following KVM_RUN therefore + * has no pending event request to accidentally deliver the nested APF. + */ + GUEST_SYNC(STAGE_APF_ARMED); + + vmlaunch(); + GUEST_ASSERT_EQ(vmread(VM_EXIT_REASON), EXIT_REASON_EXCEPTION_NMI); + + intr_info = vmread(VM_EXIT_INTR_INFO); + GUEST_ASSERT(intr_info & VMX_INTR_INFO_VALID); + GUEST_ASSERT_EQ(intr_info & VMX_INTR_INFO_VECTOR_MASK, PF_VECTOR); + GUEST_ASSERT_EQ(intr_info & VMX_INTR_INFO_TYPE_MASK, + VMX_INTR_INFO_HARD_EXCEPTION); + GUEST_ASSERT(intr_info & VMX_INTR_INFO_DELIVER_CODE); + GUEST_ASSERT_EQ(vmread(VM_EXIT_INTR_ERROR_CODE), 0); + + token = vmread(EXIT_QUALIFICATION); + reason = READ_ONCE(apf_data.flags); + GUEST_ASSERT(token); + GUEST_ASSERT_EQ(reason, KVM_PV_REASON_PAGE_NOT_PRESENT); + + /* Release the APF record slot before userspace resolves the page. */ + WRITE_ONCE(apf_data.flags, 0); + GUEST_SYNC_ARGS(STAGE_APF_DELIVERED, token, reason, 0, 0); +} + +#ifdef __NR_userfaultfd +static int register_missing_page(void *hva) +{ + struct uffdio_register uffd_register = { + .range = { + .start = (uintptr_t)hva, + .len = PAGE_SIZE, + }, + .mode = UFFDIO_REGISTER_MODE_MISSING, + }; + struct uffdio_api uffd_api = { + .api = UFFD_API, + }; + int uffd; + + uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); + if (uffd < 0 && (errno == EPERM || errno == EACCES)) + ksft_exit_skip("userfaultfd is unavailable: %s\n", strerror(errno)); + TEST_ASSERT(uffd >= 0, "userfaultfd failed: %s", strerror(errno)); + + TEST_ASSERT(ioctl(uffd, UFFDIO_API, &uffd_api) == 0, + "UFFDIO_API failed: %s", strerror(errno)); + TEST_ASSERT(ioctl(uffd, UFFDIO_REGISTER, &uffd_register) == 0, + "UFFDIO_REGISTER failed: %s", strerror(errno)); + TEST_ASSERT(uffd_register.ioctls & (1ULL << _UFFDIO_COPY), + "userfaultfd does not support UFFDIO_COPY"); + + return uffd; +} + +static bool read_missing_page_event(int uffd, void *target_hva) +{ + struct pollfd pollfd = { + .fd = uffd, + .events = POLLIN, + }; + struct uffd_msg msg; + ssize_t bytes; + int ret; + + ret = poll(&pollfd, 1, 3000); + if (ret != 1 || !(pollfd.revents & POLLIN)) + return false; + + bytes = read(uffd, &msg, sizeof(msg)); + if (bytes != sizeof(msg) || msg.event != UFFD_EVENT_PAGEFAULT) + return false; + + return (msg.arg.pagefault.address & ~(PAGE_SIZE - 1ULL)) == + (uintptr_t)target_hva; +} + +static void resolve_missing_page(int uffd, void *target_hva) +{ + struct uffdio_copy uffd_copy = { + .dst = (uintptr_t)target_hva, + .len = PAGE_SIZE, + }; + void *page = NULL; + int ret; + + ret = posix_memalign(&page, PAGE_SIZE, PAGE_SIZE); + TEST_ASSERT(!ret, "posix_memalign failed: %s", strerror(ret)); + memset(page, 0, PAGE_SIZE); + uffd_copy.src = (uintptr_t)page; + + TEST_ASSERT(ioctl(uffd, UFFDIO_COPY, &uffd_copy) == 0, + "UFFDIO_COPY failed: %s", strerror(errno)); + free(page); +} + +static void alarm_handler(int signal) +{ + (void)signal; +} + +static void assert_sync_stage(struct kvm_vcpu *vcpu, u64 expected_stage) +{ + struct ucall uc; + + TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO); + switch (get_ucall(vcpu, &uc)) { + case UCALL_SYNC: + TEST_ASSERT_EQ(uc.args[1], expected_stage); + break; + case UCALL_ABORT: + REPORT_GUEST_ASSERT(uc); + break; + default: + TEST_FAIL("Unexpected ucall command %lu", uc.cmd); + } +} + +static void run_test(void) +{ + struct kvm_vcpu_pv_apf_data *apf_hva; + struct sigaction action = { + .sa_handler = alarm_handler, + }; + struct kvm_vcpu *vcpu; + struct kvm_vm *vm; + struct ucall uc = {}; + sigset_t blocked_signals; + sigset_t old_signals; + gva_t nested_gva; + gva_t target_gva; + gpa_t apf_gpa; + void *target_hva; + u32 stale_reason; + bool uffd_event; + bool run_timed_out; + int run_errno = 0; + int run_ret; + int uffd; + u64 ucall_cmd = UCALL_NONE; + + vm = vm_create_with_one_vcpu(&vcpu, l1_guest_code); + vm_enable_tdp(vm); + vcpu_alloc_vmx(vm, &nested_gva); + target_gva = vm_alloc_page(vm); + tdp_identity_map_default_memslots(vm); + + l2_target_gva = target_gva; + sync_global_to_guest(vm, l2_target_gva); + apf_gpa = addr_gva2gpa(vm, (gva_t)&apf_data); + TEST_ASSERT(!(apf_gpa & 63U), "APF data GPA %#lx is misaligned", + apf_gpa); + apf_hva = addr_gva2hva(vm, (gva_t)&apf_data); + target_hva = addr_gva2hva(vm, target_gva); + vcpu_args_set(vcpu, 2, nested_gva, apf_gpa); + + vcpu_run(vcpu); + assert_sync_stage(vcpu, STAGE_APF_ARMED); + + TEST_ASSERT(madvise(target_hva, PAGE_SIZE, MADV_DONTNEED) == 0, + "madvise failed: %s", strerror(errno)); + uffd = register_missing_page(target_hva); + + TEST_ASSERT(sigaction(SIGALRM, &action, NULL) == 0, + "sigaction failed: %s", strerror(errno)); + sigfillset(&blocked_signals); + sigdelset(&blocked_signals, SIGALRM); + TEST_ASSERT(sigprocmask(SIG_SETMASK, &blocked_signals, &old_signals) == 0, + "sigprocmask failed: %s", strerror(errno)); + + alarm(RUN_TIMEOUT_SECONDS); + run_ret = __vcpu_run(vcpu); + run_errno = errno; + alarm(0); + run_timed_out = run_ret < 0 && run_errno == EINTR; + TEST_ASSERT(sigprocmask(SIG_SETMASK, &old_signals, NULL) == 0, + "sigprocmask restore failed: %s", strerror(errno)); + + stale_reason = READ_ONCE(apf_hva->flags); + uffd_event = read_missing_page_event(uffd, target_hva); + if (uffd_event) + resolve_missing_page(uffd, target_hva); + close(uffd); + + if (!run_ret) + ucall_cmd = get_ucall(vcpu, &uc); + if (ucall_cmd == UCALL_ABORT) + REPORT_GUEST_ASSERT(uc); + kvm_vm_free(vm); + + TEST_ASSERT(uffd_event, + "L2 access did not produce the held userfaultfd fault"); + TEST_ASSERT(!run_timed_out || + stale_reason == KVM_PV_REASON_PAGE_NOT_PRESENT, + "KVM_RUN timed out without a stale APF reason: reason=%u", + stale_reason); + TEST_ASSERT(!run_timed_out, + "Lost nested APF event: KVM re-entered L2 and blocked; APF reason=%u", + stale_reason); + TEST_ASSERT(!run_ret, "KVM_RUN failed: %s", strerror(run_errno)); + TEST_ASSERT_EQ(ucall_cmd, UCALL_SYNC); + TEST_ASSERT_EQ(uc.args[1], STAGE_APF_DELIVERED); + TEST_ASSERT_EQ(uc.args[3], KVM_PV_REASON_PAGE_NOT_PRESENT); + TEST_ASSERT(uc.args[2], "KVM returned a zero APF token"); +} +#endif + +int main(int argc, char *argv[]) +{ + (void)argc; + (void)argv; + +#ifndef __NR_userfaultfd + ksft_exit_skip("__NR_userfaultfd is unavailable\n"); +#else + TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_VMX)); + TEST_REQUIRE(kvm_cpu_has_ept()); + TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_KVM_ASYNC_PF)); + TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_KVM_ASYNC_PF_VMEXIT)); + TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_KVM_ASYNC_PF_INT)); + + run_test(); +#endif + return 0; +} -- 2.43.0