From: David Woodhouse <[email protected]> Race a guest EVTCHNOP_send loop against a host thread which deletes and recreates the shared_info memslot. Delivery which hits the update window now returns -EAGAIN and the hypercall is re-executed; if KVM instead waited in the vCPU's exit-handling context — under vcpu_run's kvm->srcu read section — it would deadlock against kvm_swap_active_memslots() waiting out that reader, and this test livelocks within seconds.
Signed-off-by: David Woodhouse <[email protected]> Assisted-by: LLM --- tools/testing/selftests/kvm/Makefile.kvm | 1 + .../kvm/x86/xen_evtchn_memslot_race_test.c | 178 ++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 tools/testing/selftests/kvm/x86/xen_evtchn_memslot_race_test.c diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm index 96bab7002d39..e7e17d19d6cb 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -144,6 +144,7 @@ TEST_GEN_PROGS_x86 += x86/xss_msr_test TEST_GEN_PROGS_x86 += x86/debug_regs TEST_GEN_PROGS_x86 += x86/tsc_msrs_test TEST_GEN_PROGS_x86 += x86/vmx_pmu_caps_test +TEST_GEN_PROGS_x86 += x86/xen_evtchn_memslot_race_test TEST_GEN_PROGS_x86 += x86/xen_shinfo_test TEST_GEN_PROGS_x86 += x86/xen_vmcall_test TEST_GEN_PROGS_x86 += x86/sev_dbg_test diff --git a/tools/testing/selftests/kvm/x86/xen_evtchn_memslot_race_test.c b/tools/testing/selftests/kvm/x86/xen_evtchn_memslot_race_test.c new file mode 100644 index 000000000000..2b6974d3d464 --- /dev/null +++ b/tools/testing/selftests/kvm/x86/xen_evtchn_memslot_race_test.c @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * xen_evtchn_memslot_race_test + * + * A guest EVTCHNOP_send whose delivery races a DELETE/re-CREATE of the + * memslot holding the shared_info page. The delivery path runs in the + * vCPU's exit-handling context, under vcpu_run's kvm->srcu read + * section; if it waits there for the memslot update to complete, it + * deadlocks against kvm_swap_active_memslots()'s + * synchronize_srcu_expedited() waiting out that very read section. + * Regression test for that deadlock: KVM must defer the delivery to + * the next guest entry rather than spin in exit context. + * + * The guest sends events in a tight loop; a host thread deletes and + * recreates the shared_info slot in a tight loop. On a broken host + * this livelocks within a second or two (the test then dies by + * timeout); on a fixed host every send eventually delivers and the + * test completes N iterations. + */ +#include <pthread.h> +#include <stdatomic.h> + +#include "test_util.h" +#include "kvm_util.h" +#include "processor.h" + +#include <stdint.h> + +#define SHINFO_REGION_GPA 0xc0000000ULL +#define SHINFO_REGION_SLOT 10 + +#define EVTCHN_PORT 15 +#define TEST_ITERATIONS 4000 + +#define EVTCHNOP_send 4 +#define __HYPERVISOR_event_channel_op 32 + +#define XEN_HYPERCALL_MSR 0x40000000 + +#define EVTCHNSTAT_ipi 5 + +struct evtchn_send { + uint32_t port; +}; + +static void guest_code(void) +{ + struct evtchn_send s = { .port = EVTCHN_PORT }; + int i; + + for (i = 0; i < TEST_ITERATIONS; i++) { + /* + * The delivery may legitimately fail while the shared_info + * slot is mid-churn (between DELETE and re-CREATE there is + * no slot at all, which is -EFAULT, not a transient). The + * failure mode under test is the vCPU never coming back at + * all (deadlock -> timeout), not the hypercall rc. + */ + __xen_hypercall(__HYPERVISOR_event_channel_op, EVTCHNOP_send, &s); + GUEST_SYNC(i); + } + + GUEST_DONE(); +} + +static atomic_bool churn_stop; + +static void *memslot_churn(void *arg) +{ + struct kvm_vm *vm = arg; + + while (!atomic_load(&churn_stop)) { + vm_mem_region_delete(vm, SHINFO_REGION_SLOT); + vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, + SHINFO_REGION_GPA, + SHINFO_REGION_SLOT, 2, 0); + } + return NULL; +} + +int main(int argc, char *argv[]) +{ + struct kvm_vcpu *vcpu; + struct kvm_vm *vm; + pthread_t thread; + struct ucall uc; + int done = 0; + int r; + + int xen_caps = kvm_check_cap(KVM_CAP_XEN_HVM); + + TEST_REQUIRE(xen_caps & KVM_XEN_HVM_CONFIG_SHARED_INFO); + TEST_REQUIRE(xen_caps & KVM_XEN_HVM_CONFIG_EVTCHN_SEND); + + vm = vm_create_with_one_vcpu(&vcpu, guest_code); + + vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, + SHINFO_REGION_GPA, SHINFO_REGION_SLOT, 2, 0); + + struct kvm_xen_hvm_config hvmc = { + .flags = KVM_XEN_HVM_CONFIG_INTERCEPT_HCALL | + KVM_XEN_HVM_CONFIG_EVTCHN_SEND, + .msr = XEN_HYPERCALL_MSR, + }; + vm_ioctl(vm, KVM_XEN_HVM_CONFIG, &hvmc); + + struct kvm_xen_hvm_attr lm = { + .type = KVM_XEN_ATTR_TYPE_LONG_MODE, + .u.long_mode = 1, + }; + vm_ioctl(vm, KVM_XEN_HVM_SET_ATTR, &lm); + + struct kvm_xen_hvm_attr ha = { + .type = KVM_XEN_ATTR_TYPE_SHARED_INFO, + .u.shared_info.gfn = SHINFO_REGION_GPA / PAGE_SIZE, + }; + vm_ioctl(vm, KVM_XEN_HVM_SET_ATTR, &ha); + + struct kvm_xen_vcpu_attr vi = { + .type = KVM_XEN_VCPU_ATTR_TYPE_VCPU_INFO, + .u.gpa = SHINFO_REGION_GPA + 0x40, + }; + vcpu_ioctl(vcpu, KVM_XEN_VCPU_SET_ATTR, &vi); + + /* Loop the port back to this vCPU as an IPI. */ + struct kvm_xen_hvm_attr evt = { + .type = KVM_XEN_ATTR_TYPE_EVTCHN, + .u.evtchn.send_port = EVTCHN_PORT, + .u.evtchn.type = EVTCHNSTAT_ipi, + .u.evtchn.flags = 0, + .u.evtchn.deliver.port.port = EVTCHN_PORT, + .u.evtchn.deliver.port.vcpu = vcpu->id, + .u.evtchn.deliver.port.priority = KVM_IRQ_ROUTING_XEN_EVTCHN_PRIO_2LEVEL, + }; + vm_ioctl(vm, KVM_XEN_HVM_SET_ATTR, &evt); + + r = pthread_create(&thread, NULL, memslot_churn, vm); + TEST_ASSERT(!r, "pthread_create() failed: %d", r); + + while (!done) { + vcpu_run(vcpu); + + /* + * In the window where the shared_info slot is fully deleted + * (between the churn thread's DELETE and re-CREATE), the + * delivery fails with -EFAULT and the hypercall bounces to + * userspace as KVM_EXIT_XEN. Complete it with an error + * result and keep going; the failure mode under test is + * the vCPU never coming back (deadlock -> timeout), not + * the hypercall rc. + */ + if (vcpu->run->exit_reason == KVM_EXIT_XEN && + vcpu->run->xen.type == KVM_EXIT_XEN_HCALL) { + vcpu->run->xen.u.hcall.result = -EFAULT; + continue; + } + + TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO); + + switch (get_ucall(vcpu, &uc)) { + case UCALL_SYNC: + break; + case UCALL_DONE: + done = 1; + break; + case UCALL_ABORT: + REPORT_GUEST_ASSERT(uc); + default: + TEST_FAIL("Unexpected ucall %lu", uc.cmd); + } + } + + atomic_store(&churn_stop, true); + pthread_join(thread, NULL); + + kvm_vm_free(vm); + return 0; +} -- 2.43.0

