[PATCH v3 10/11] KVM: selftests: Add Xen evtchn vs memslot-update race test
From: David Woodhouse
Date: Fri Oct 16 2026 - 11:07:03 EST
From: David Woodhouse <dwmw@xxxxxxxxxxxx>
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 <dwmw@xxxxxxxxxxxx>
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