summaryrefslogtreecommitdiff
path: root/tools/testing/selftests/kvm/irq_test.c
diff options
context:
space:
mode:
Diffstat (limited to 'tools/testing/selftests/kvm/irq_test.c')
-rw-r--r--tools/testing/selftests/kvm/irq_test.c362
1 files changed, 362 insertions, 0 deletions
diff --git a/tools/testing/selftests/kvm/irq_test.c b/tools/testing/selftests/kvm/irq_test.c
new file mode 100644
index 000000000000..240f6f0fdbe4
--- /dev/null
+++ b/tools/testing/selftests/kvm/irq_test.c
@@ -0,0 +1,362 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "kvm_util.h"
+#include "test_util.h"
+#include "apic.h"
+#include "processor.h"
+#include "proc_util.h"
+
+#include <libvfio.h>
+#include <linux/sizes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <sys/eventfd.h>
+#include <sys/sysinfo.h>
+
+static u64 timeout_ns = 2ULL * 1000 * 1000 * 1000;
+static bool guest_ready_for_irqs[KVM_MAX_VCPUS];
+static bool guest_received_irq[KVM_MAX_VCPUS];
+static bool guest_received_nmi[KVM_MAX_VCPUS];
+static bool x2apic = true;
+static bool irq_affinity;
+static bool done;
+
+#define GUEST_RECEIVED_IRQ(__vcpu) \
+ SYNC_FROM_GUEST_AND_READ((__vcpu)->vm, guest_received_irq[(__vcpu)->id])
+#define GUEST_RECEIVED_NMI(__vcpu) \
+ SYNC_FROM_GUEST_AND_READ((__vcpu)->vm, guest_received_nmi[(__vcpu)->id])
+
+#define GUEST_RECEIVED_INTERRUPT(__vcpu, __nmi) \
+ ((__nmi) ? GUEST_RECEIVED_NMI(__vcpu) : GUEST_RECEIVED_IRQ(__vcpu))
+
+static u32 guest_get_vcpu_id(void)
+{
+ if (x2apic)
+ return x2apic_read_reg(APIC_ID);
+ else
+ return xapic_read_reg(APIC_ID) >> 24;
+}
+
+static void guest_irq_handler(struct ex_regs *regs)
+{
+ WRITE_ONCE(guest_received_irq[guest_get_vcpu_id()], true);
+
+ if (x2apic)
+ x2apic_write_reg(APIC_EOI, 0);
+ else
+ xapic_write_reg(APIC_EOI, 0);
+}
+
+static void guest_nmi_handler(struct ex_regs *regs)
+{
+ WRITE_ONCE(guest_received_nmi[guest_get_vcpu_id()], true);
+}
+
+static void guest_code(void)
+{
+ if (x2apic)
+ x2apic_enable();
+ else
+ xapic_enable();
+
+ sti_nop();
+
+ WRITE_ONCE(guest_ready_for_irqs[guest_get_vcpu_id()], true);
+
+ while (!READ_ONCE(done))
+ cpu_relax();
+
+ GUEST_DONE();
+}
+
+static void *vcpu_thread_main(void *arg)
+{
+ struct kvm_vcpu *vcpu = arg;
+ struct ucall uc;
+
+ vcpu_run(vcpu);
+ TEST_ASSERT_EQ(UCALL_DONE, get_ucall(vcpu, &uc));
+
+ return NULL;
+}
+
+static int vfio_setup_msi(struct vfio_pci_device *device)
+{
+ const int flags = MAP_SHARED | MAP_ANONYMOUS;
+ const int prot = PROT_READ | PROT_WRITE;
+ struct iova_allocator *allocator;
+ struct dma_region *region;
+
+ /* Sanity check that the device+driver can actually send MSIs. */
+ TEST_REQUIRE(device->driver.ops);
+ TEST_REQUIRE(device->driver.ops->send_msi);
+
+ /*
+ * Set up a DMA-able region for the driver to use. Very few devices
+ * provide a way to arbitrarily send interrupts (MSIs), e.g. by writing
+ * an MMIO register. Instead, most devices send MSIs when an action is
+ * completed, and practically all actions involve DMA of some form.
+ */
+ allocator = iova_allocator_init(device->iommu);
+
+ region = &device->driver.region;
+ region->size = SZ_2M;
+ region->iova = iova_allocator_alloc(allocator, region->size);
+ region->vaddr = kvm_mmap(region->size, prot, flags, -1);
+ TEST_ASSERT(region->vaddr != MAP_FAILED, "mmap() failed\n");
+ iommu_map(device->iommu, region);
+
+ iova_allocator_cleanup(allocator);
+
+ vfio_pci_driver_init(device);
+
+ return device->driver.msi;
+}
+
+static void trigger_interrupt(struct vfio_pci_device *device, int eventfd)
+{
+ if (device)
+ vfio_pci_driver_send_msi(device);
+ else
+ eventfd_write(eventfd, 1);
+}
+
+
+static void kvm_route_msi(struct kvm_vm *vm, u32 gsi, struct kvm_vcpu *vcpu,
+ u8 vector, bool use_nmi)
+{
+ struct {
+ struct kvm_irq_routing header;
+ struct kvm_irq_routing_entry entry;
+ } routing = {
+ .header.nr = 1,
+ .entry = {
+ .gsi = gsi,
+ .type = KVM_IRQ_ROUTING_MSI,
+ .u.msi.address_lo = 0xFEE00000 | (vcpu->id & GENMASK(7, 0)) << 12,
+ .u.msi.address_hi = vcpu->id & GENMASK(31, 8),
+ .u.msi.data = use_nmi ? NMI_VECTOR | (4 << 8) : vector,
+ },
+ };
+
+ vm_ioctl(vm, KVM_SET_GSI_ROUTING, &routing.header);
+}
+
+static void kvm_set_empty_gsi_routing(struct kvm_vm *vm)
+{
+ struct kvm_irq_routing routing = {};
+
+ vm_ioctl(vm, KVM_SET_GSI_ROUTING, &routing);
+}
+
+static const char *probe_iommu_type(void)
+{
+ int io_fd;
+
+ io_fd = open("/dev/iommu", O_RDONLY);
+ if (io_fd >= 0) {
+ close(io_fd);
+ return MODE_IOMMUFD;
+ }
+
+ io_fd = __open_path_or_exit("/dev/vfio/vfio", O_RDONLY,
+ "Is VFIO (or IOMMUFD) loaded and enabled?");
+ close(io_fd);
+ return MODE_VFIO_TYPE1_IOMMU;
+}
+
+static void help(const char *name)
+{
+ printf("Usage: %s [-a] [-d <segment:bus:device.function>] [-e] [-h] [-i nr_irqs] [-m] [-n] [-t iommu_type] [-v nr_vcpus] [-x]\n", name);
+ printf("\n");
+ printf("Tests KVM interrupt routing and delivery via irqfd.\n");
+ printf("-a Affine the device's host IRQ to a random physical CPU\n");
+ printf("-d Use a VFIO device to send MSI-X interrupts instead of manually signaling the eventfd\n");
+ printf("-e Set empty GSI routing in-between some interrupts\n");
+ printf("-i The number of IRQs to generate during the test\n");
+ printf("-m Pin target vCPU to random physical CPU before triggering interrupt\n");
+ printf("-n Deliver 50 percent of IRQs as non-maskable interrupts\n");
+ printf("-t Override the IOMMU type to use (vfio_type1_iommu or iommufd)\n");
+ printf("-v Number of vCPUS to run\n");
+ printf("-x Use xAPIC mode instead of x2APIC mode in the guest\n");
+ printf("\n");
+ exit(KSFT_FAIL);
+}
+
+int main(int argc, char **argv)
+{
+ /*
+ * Pick a random vector and a random GSI to use for device IRQ.
+ *
+ * Pick an IRQ vector in range [32, UINT8_MAX]. Min value is 32 because
+ * Linux/x86 reserves vectors 0-31 for exceptions and architecture
+ * defined NMIs and interrupts.
+ *
+ * Pick a GSI in range [24, KVM_MAX_IRQ_ROUTES - 1]. The min value is 24
+ * because KVM reserves GSIs 0-15 for legacy ISA IRQs and 16-23 only go
+ * to the IOAPIC. The max is KVM_MAX_IRQ_ROUTES - 1, because
+ * KVM_MAX_IRQ_ROUTES is exclusive.
+ */
+ u32 gsi = kvm_random_u64_in_range(&kvm_rng, 24, KVM_MAX_IRQ_ROUTES - 1);
+ u8 vector = kvm_random_u64_in_range(&kvm_rng, 32, UINT8_MAX);
+
+ pthread_t vcpu_threads[KVM_MAX_VCPUS];
+ struct kvm_vcpu *vcpus[KVM_MAX_VCPUS];
+ struct vfio_pci_device *device = NULL;
+ int nr_irqs = 1000, nr_vcpus = 1;
+ bool set_empty_routing = false;
+ const char *device_bdf = NULL;
+ const char *iommu_type = NULL;
+ int i, j, c, msix, eventfd;
+ bool migrate_vcpus = false;
+ cpu_set_t available_cpus;
+ bool use_nmi = false;
+ struct iommu *iommu;
+ struct kvm_vm *vm;
+ int irq, irq_cpu;
+
+ while ((c = getopt(argc, argv, "ad:ehi:mnt:v:x")) != -1) {
+ switch (c) {
+ case 'a':
+ irq_affinity = true;
+ break;
+ case 'd':
+ device_bdf = optarg;
+ break;
+ case 'e':
+ set_empty_routing = true;
+ break;
+ case 'i':
+ nr_irqs = atoi_positive("Number of IRQs", optarg);
+ break;
+ case 'm':
+ migrate_vcpus = true;
+ break;
+ case 'n':
+ use_nmi = true;
+ break;
+ case 't':
+ iommu_type = optarg;
+ break;
+ case 'v':
+ nr_vcpus = atoi_positive("Number of vCPUS", optarg);
+ TEST_ASSERT(nr_vcpus <= KVM_MAX_VCPUS,
+ "KVM selftests support at most %u vCPUs", KVM_MAX_VCPUS);
+ break;
+ case 'x':
+ x2apic = false;
+ break;
+ case 'h':
+ default:
+ help(argv[0]);
+ }
+ }
+
+ TEST_REQUIRE(kvm_arch_has_default_irqchip());
+
+ vm = vm_create_with_vcpus(nr_vcpus, guest_code, vcpus);
+ vm_enable_cap(vm, KVM_CAP_X2APIC_API, KVM_X2APIC_API_USE_32BIT_IDS |
+ KVM_X2APIC_API_DISABLE_BROADCAST_QUIRK);
+
+ vm_install_exception_handler(vm, vector, guest_irq_handler);
+ vm_install_exception_handler(vm, NMI_VECTOR, guest_nmi_handler);
+
+ if (!x2apic) {
+ TEST_ASSERT(nr_vcpus < 256, "xAPIC can only target IDs [0-254] (255 vCPUs)");
+ virt_pg_map(vm, APIC_DEFAULT_GPA, APIC_DEFAULT_GPA);
+ }
+
+ if (device_bdf) {
+ if (!iommu_type)
+ iommu_type = probe_iommu_type();
+ iommu = iommu_init(iommu_type);
+ device = vfio_pci_device_init(device_bdf, iommu);
+ msix = vfio_setup_msi(device);
+ irq = vfio_msix_to_host_irq(device_bdf, msix);
+ eventfd = device->msi_eventfds[msix];
+ printf("Using device %s MSI-X[%d] (IRQ-%u)\n", device_bdf, msix,
+ irq);
+ } else {
+ TEST_ASSERT(!irq_affinity,
+ "Setting IRQ affinity (-a) requires a backing device (-d)");
+
+ eventfd = kvm_new_eventfd();
+ irq = -1;
+ }
+
+ pr_info("Injecting interrupts for GSI %d (guest vector 0x%x) %d times\n",
+ gsi, vector, nr_irqs);
+
+ kvm_assign_irqfd(vm, gsi, eventfd);
+
+ sync_global_to_guest(vm, x2apic);
+
+ if (migrate_vcpus)
+ kvm_sched_getaffinity(0, sizeof(available_cpus), &available_cpus);
+
+ for (i = 0; i < nr_vcpus; i++)
+ pthread_create(&vcpu_threads[i], NULL, vcpu_thread_main, vcpus[i]);
+
+ for (i = 0; i < nr_vcpus; i++) {
+ struct kvm_vcpu *vcpu = vcpus[i];
+
+ while (!SYNC_FROM_GUEST_AND_READ(vm, guest_ready_for_irqs[vcpu->id]))
+ continue;
+ }
+
+ irq_cpu = -1;
+
+ for (i = 0; i < nr_irqs; i++) {
+ const bool do_set_empty_routing = set_empty_routing && (i & BIT(3));
+ const bool do_use_nmi = use_nmi && (i & BIT(2));
+ struct kvm_vcpu *vcpu = vcpus[i % nr_vcpus];
+ struct timespec start;
+
+ if (do_set_empty_routing)
+ kvm_set_empty_gsi_routing(vm);
+
+ kvm_route_msi(vm, gsi, vcpu, vector, do_use_nmi);
+
+ if (irq_affinity) {
+ irq_cpu = kvm_random_u64(&kvm_rng) % get_nprocs();
+ proc_irq_set_smp_affinity(irq, irq_cpu);
+ }
+
+ if (migrate_vcpus)
+ pin_task_to_random_cpu(vcpu_threads[i % nr_vcpus], &available_cpus);
+
+ for (j = 0; j < nr_vcpus; j++) {
+ TEST_ASSERT(!GUEST_RECEIVED_IRQ(vcpus[j]),
+ "IRQ flag for vCPU %d not clear prior to test",
+ vcpus[j]->id);
+ TEST_ASSERT(!GUEST_RECEIVED_NMI(vcpus[j]),
+ "NMI flag for vCPU %d not clear prior to test",
+ vcpus[j]->id);
+ }
+
+ trigger_interrupt(device, eventfd);
+
+ clock_gettime(CLOCK_MONOTONIC, &start);
+ while (!GUEST_RECEIVED_INTERRUPT(vcpu, do_use_nmi) &&
+ timespec_to_ns(timespec_elapsed(start)) <= timeout_ns)
+ cpu_relax();
+
+ TEST_ASSERT(GUEST_RECEIVED_INTERRUPT(vcpu, do_use_nmi),
+ "vCPU %d timed out waiting for %s (vector 0x%x) from GSI %d (via CPU %d)\n",
+ vcpu->id, do_use_nmi ? "NMI" : "IRQ",
+ do_use_nmi ? NMI_VECTOR : vector, gsi, irq_cpu);
+
+ if (do_use_nmi)
+ WRITE_AND_SYNC_TO_GUEST(vm, guest_received_nmi[vcpu->id], false);
+ else
+ WRITE_AND_SYNC_TO_GUEST(vm, guest_received_irq[vcpu->id], false);
+ }
+
+ WRITE_AND_SYNC_TO_GUEST(vm, done, true);
+
+ for (i = 0; i < nr_vcpus; i++)
+ pthread_join(vcpu_threads[i], NULL);
+
+ return 0;
+}