From: Maximilian Dittgen mdittgen@amazon.de
At the moment, all MSIs injected from userspace using KVM_SIGNAL_MSI are processed as LPIs in software with a hypervisor trap and exit. To properly test GICv4 direct vLPI injection from KVM selftests, we write a KVM_DEBUG_GIC_MSI_SETUP ioctl that manually creates an IRQ routing table entry for the specified MSI, and populates ITS structures (device, collection, and interrupt translation table entries) to map the MSI to a vLPI. We then call GICv4 kvm_vgic_v4_set_forwarding to let the vLPI bypass hypervisor traps and inject directly to the vCPU.
To demonstrate the use of this ioctl, we implement a -D flag to the vgic_lpi_stress.c selftest that runs the stress test using direct vLPI injection rather than software-emulated LPI handling.
Signed-off-by: Maximilian Dittgen mdittgen@amazon.de --- arch/arm64/kvm/arm.c | 37 +++++ arch/arm64/kvm/vgic/vgic-its.c | 133 ++++++++++++++++++ arch/arm64/kvm/vgic/vgic.h | 2 + include/linux/irqchip/arm-gic-v3.h | 1 + include/uapi/linux/kvm.h | 15 ++ .../selftests/kvm/arm64/vgic_lpi_stress.c | 52 ++++++- 6 files changed, 238 insertions(+), 2 deletions(-)
diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c index 5bf101c869c9..e18f5ff68274 100644 --- a/arch/arm64/kvm/arm.c +++ b/arch/arm64/kvm/arm.c @@ -46,6 +46,8 @@ #include <kvm/arm_pmu.h> #include <kvm/arm_psci.h>
+#include <vgic/vgic.h> + #include "sys_regs.h"
static enum kvm_mode kvm_mode = KVM_MODE_DEFAULT; @@ -1927,6 +1929,41 @@ int kvm_arch_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg) return -EFAULT; return kvm_vm_ioctl_get_reg_writable_masks(kvm, &range); } + case KVM_DEBUG_GIC_MSI_SETUP: { + /* Define interrupt ID boundaries for input validation */ + #define GIC_LPI_OFFSET 8192 + #define GIC_LPI_MAX 65535 + #define SPI_INTID_MIN 32 + #define SPI_INTID_MAX 1019 + + struct kvm_debug_gic_msi_setup params; + struct kvm_vcpu *vcpu; + + if (copy_from_user(¶ms, argp, sizeof(params))) + return -EFAULT; + + /* validate vcpu_id is in range and exists */ + if (params.vcpu_id >= atomic_read(&kvm->online_vcpus)) + return -EINVAL; + + vcpu = kvm_get_vcpu(kvm, params.vcpu_id); + if (!vcpu) + return -EINVAL; + + /* validate vintid is in LPI range */ + if (params.vintid < GIC_LPI_OFFSET || params.vintid > GIC_LPI_MAX) + return -EINVAL; + + /* + * Validate host_irq is in safe range -- we use SPI range since + * selftests guests will have no shared peripheral devices + */ + if (params.host_irq < SPI_INTID_MIN || params.host_irq > SPI_INTID_MAX) + return -EINVAL; + + /* Mock single MSI for testing */ + return debug_gic_msi_setup_mock_msi(kvm, ¶ms); + } default: return -EINVAL; } diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c index 7368c13f16b7..46153ef5efcb 100644 --- a/arch/arm64/kvm/vgic/vgic-its.c +++ b/arch/arm64/kvm/vgic/vgic-its.c @@ -2816,3 +2816,136 @@ int kvm_vgic_register_its_device(void) return kvm_register_device_ops(&kvm_arm_vgic_its_ops, KVM_DEV_TYPE_ARM_VGIC_ITS); } + +static struct vgic_its *vgic_get_its(struct kvm *kvm, + struct kvm_kernel_irq_routing_entry *irq_entry) +{ + struct kvm_msi msi = (struct kvm_msi) { + .address_lo = irq_entry->msi.address_lo, + .address_hi = irq_entry->msi.address_hi, + .data = irq_entry->msi.data, + .flags = irq_entry->msi.flags, + .devid = irq_entry->msi.devid, + }; + + return vgic_msi_to_its(kvm, &msi); +} + +/* + * debug_gic_msi_setup_mock_msi - manually set up vLPI direct injection infrastructure + * for an MSI upon userspace request. Used for testing vLPIs from selftests. + * + * Creates an IRQ routing entry mapping the specified MSI signature to a mock + * host IRQ, then populates ITS structures (device, collection, ITE) to establish + * the DevID/EventID to LPI translation. Finally enables GICv4 vLPI forwarding + * to bypass software emulation and inject interrupts directly to the vCPU. + * + * This function is intended solely for KVM selftests via KVM_DEBUG_GIC_MSI_SETUP. + * It uses mock host IRQs in the SPI range assuming no real hardware devices are + * present on a selftest guest. Using this interface in production will corrupt the + * IRQ routing table. + */ +int debug_gic_msi_setup_mock_msi(struct kvm *kvm, struct kvm_debug_gic_msi_setup *params) +{ + struct kvm_irq_routing_entry user_entry; + struct kvm_kernel_irq_routing_entry entry; + struct vgic_its *its; + struct its_device *device; + struct its_collection *collection; + struct its_ite *ite; + struct vgic_irq *irq; + struct kvm_vcpu *vcpu; + u64 doorbell_addr = GITS_BASE_GPA + GITS_TRANSLATER; + u32 device_id = params->device_id; + u32 event_id = params->event_id; + u32 coll_id = params->vcpu_id; + u32 lpi_nr = params->vintid; + gpa_t itt_addr = params->itt_addr; + int ret; + int host_irq = params->host_irq; + + // Unmap any existing vLPI on the mock host IRQ (remnants from prior mocks) + kvm_vgic_v4_unset_forwarding(kvm, host_irq); + + /* Create mock user IRQ routing entry using kvm_set_routing_entry function */ + memset(&user_entry, 0, sizeof(user_entry)); + user_entry.gsi = host_irq; + user_entry.type = KVM_IRQ_ROUTING_MSI; + user_entry.u.msi.address_lo = doorbell_addr & 0xFFFFFFFF; + user_entry.u.msi.address_hi = doorbell_addr >> 32; + user_entry.u.msi.data = event_id; + user_entry.u.msi.devid = device_id; + user_entry.flags = KVM_MSI_VALID_DEVID; + + /* Initialize kernel routing entry */ + memset(&entry, 0, sizeof(entry)); + + /* Use vgic-irqfd.c function to create entry */ + ret = kvm_set_routing_entry(kvm, &entry, &user_entry); + if (ret) + return ret; + + /* Now that we created an MSI -> ITS mapping, we can populate the ITS for this MSI */ + + /* Get ITS instance */ + its = vgic_get_its(kvm, &entry); + if (IS_ERR(its)) + return PTR_ERR(its); + + /* Enable ITS manually for testing, normally done by guest writing to GITS_CTLR register */ + its->enabled = true; + + /* Get target vCPU */ + vcpu = kvm_get_vcpu(kvm, params->vcpu_id); + if (!vcpu) + return -EINVAL; + + /* + * Enable this vLPIs for this vCPU manually for testing, normally + * done by guest writing GICR_CTLR + */ + atomic_set(&vcpu->arch.vgic_cpu.ctlr, GICR_CTLR_ENABLE_LPIS); + + mutex_lock(&its->its_lock); + + /* Create ITS device */ + device = vgic_its_alloc_device(its, device_id, itt_addr, 8); + if (IS_ERR(device)) { + ret = PTR_ERR(device); + goto unlock; + } + + /* Create collection mapped to inputted vcpu */ + ret = vgic_its_alloc_collection(its, &collection, coll_id); + if (ret) + goto unlock; + + collection->target_addr = params->vcpu_id; // Map to specified vcpu + + /* Create ITE */ + ite = vgic_its_alloc_ite(device, collection, event_id); + if (IS_ERR(ite)) { + ret = PTR_ERR(ite); + vgic_its_free_collection(its, coll_id); + goto unlock; + } + + /* Create LPI */ + irq = vgic_add_lpi(kvm, lpi_nr, vcpu); + if (IS_ERR(irq)) { + ret = PTR_ERR(irq); + its_free_ite(kvm, ite); + vgic_its_free_collection(its, coll_id); + goto unlock; + } + + ite->irq = irq; + mutex_unlock(&its->its_lock); + + /* Now that routing entry is initialized, call v4 forwarding setup */ + return kvm_vgic_v4_set_forwarding(kvm, host_irq, &entry); + +unlock: + mutex_unlock(&its->its_lock); + return ret; +} diff --git a/arch/arm64/kvm/vgic/vgic.h b/arch/arm64/kvm/vgic/vgic.h index de1c1d3261c3..8c8f1e963884 100644 --- a/arch/arm64/kvm/vgic/vgic.h +++ b/arch/arm64/kvm/vgic/vgic.h @@ -432,4 +432,6 @@ static inline bool vgic_is_v3(struct kvm *kvm) int vgic_its_debug_init(struct kvm_device *dev); void vgic_its_debug_destroy(struct kvm_device *dev);
+int debug_gic_msi_setup_mock_msi(struct kvm *kvm, struct kvm_debug_gic_msi_setup *params); + #endif diff --git a/include/linux/irqchip/arm-gic-v3.h b/include/linux/irqchip/arm-gic-v3.h index 70c0948f978e..76beac55cb69 100644 --- a/include/linux/irqchip/arm-gic-v3.h +++ b/include/linux/irqchip/arm-gic-v3.h @@ -378,6 +378,7 @@ #define GITS_CIDR3 0xfffc
#define GITS_TRANSLATER 0x10040 +#define GITS_BASE_GPA 0x8000000ULL
#define GITS_SGIR 0x20020
diff --git a/include/uapi/linux/kvm.h b/include/uapi/linux/kvm.h index f0f0d49d2544..a655bbb70e99 100644 --- a/include/uapi/linux/kvm.h +++ b/include/uapi/linux/kvm.h @@ -1440,6 +1440,21 @@ struct kvm_enc_region { #define KVM_GET_SREGS2 _IOR(KVMIO, 0xcc, struct kvm_sregs2) #define KVM_SET_SREGS2 _IOW(KVMIO, 0xcd, struct kvm_sregs2)
+/* + * Generate an IRQ routing entry and vLPI tables for userspace-sourced + * MSI, enabling direct vLPI injection testing from selftests + */ +#define KVM_DEBUG_GIC_MSI_SETUP _IOW(KVMIO, 0xf0, struct kvm_debug_gic_msi_setup) + +struct kvm_debug_gic_msi_setup { + __u32 device_id; + __u32 event_id; + __u32 vcpu_id; + __u32 vintid; + __u32 host_irq; + __u64 itt_addr; +}; + #define KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE (1 << 0) #define KVM_DIRTY_LOG_INITIALLY_SET (1 << 1)
diff --git a/tools/testing/selftests/kvm/arm64/vgic_lpi_stress.c b/tools/testing/selftests/kvm/arm64/vgic_lpi_stress.c index fc4fe52fb6f8..8350665d9bdc 100644 --- a/tools/testing/selftests/kvm/arm64/vgic_lpi_stress.c +++ b/tools/testing/selftests/kvm/arm64/vgic_lpi_stress.c @@ -18,10 +18,14 @@ #include "ucall.h" #include "vgic.h"
+#define KVM_DEBUG_GIC_MSI_SETUP _IOW(KVMIO, 0xf0, struct kvm_debug_gic_msi_setup) + #define TEST_MEMSLOT_INDEX 1
#define GIC_LPI_OFFSET 8192
+static bool vlpi_enabled; + static size_t nr_iterations = 1000; static vm_paddr_t gpa_base;
@@ -220,6 +224,21 @@ static void setup_gic(void) its_fd = vgic_its_setup(vm); }
+static int enable_msi_vlpi_injection(u32 device_id, u32 event_id, + u32 vcpu_id, u32 vintid, u32 host_irq) +{ + struct kvm_debug_gic_msi_setup params = { + .device_id = device_id, + .event_id = event_id, + .vcpu_id = vcpu_id, + .vintid = vintid, + .host_irq = host_irq, + .itt_addr = test_data.itt_tables + (device_id * SZ_64K) + }; + + return __vm_ioctl(vm, KVM_DEBUG_GIC_MSI_SETUP, ¶ms); +} + static void signal_lpi(u32 device_id, u32 event_id) { vm_paddr_t db_addr = GITS_BASE_GPA + GITS_TRANSLATER; @@ -267,6 +286,30 @@ static void *vcpu_worker_thread(void *data)
switch (get_ucall(vcpu, &uc)) { case UCALL_SYNC: + /* if flag is set, set direct injection mappings for MSIs */ + if (vlpi_enabled) { + u32 intid = GIC_LPI_OFFSET; + + for (u32 device_id = 0; device_id < test_data.nr_devices; + device_id++) { + for (u32 event_id = 0; event_id < test_data.nr_event_ids; + event_id++) { + + /* we mock host_irqs in the SPI interrupt range of + * 100-1020 since selftest guests have no hardware + * devices + */ + int ret = enable_msi_vlpi_injection(device_id, + event_id, vcpu->id, intid, + intid - GIC_LPI_OFFSET + 100); + TEST_ASSERT(ret == 0, "KVM_DEBUG_GIC_MSI_SETUP failed: %d", + ret); + + intid++; + } + } + } + pthread_barrier_wait(&test_setup_barrier); continue; case UCALL_DONE: @@ -362,7 +405,9 @@ static void destroy_vm(void)
static void pr_usage(const char *name) { - pr_info("%s [-v NR_VCPUS] [-d NR_DEVICES] [-e NR_EVENTS] [-i ITERS] -h\n", name); + pr_info("%s -D [-v NR_VCPUS] [-d NR_DEVICES] [-e NR_EVENTS] [-i ITERS] -h\n", name); + pr_info(" -D:\tenable direct vLPI injection (default: %s)\n", + vlpi_enabled ? "true" : "false"); pr_info(" -v:\tnumber of vCPUs (default: %u)\n", test_data.nr_cpus); pr_info(" -d:\tnumber of devices (default: %u)\n", test_data.nr_devices); pr_info(" -e:\tnumber of event IDs per device (default: %u)\n", test_data.nr_event_ids); @@ -374,8 +419,11 @@ int main(int argc, char **argv) u32 nr_threads; int c;
- while ((c = getopt(argc, argv, "hv:d:e:i:")) != -1) { + while ((c = getopt(argc, argv, "hDv:d:e:i:")) != -1) { switch (c) { + case 'D': + vlpi_enabled = true; + break; case 'v': test_data.nr_cpus = atoi(optarg); break;
On Thu, 25 Sep 2025 10:01:16 +0100, Maximilian Dittgen mdittgen@amazon.de wrote:
From: Maximilian Dittgen mdittgen@amazon.de
At the moment, all MSIs injected from userspace using KVM_SIGNAL_MSI are processed as LPIs in software with a hypervisor trap and exit.
Not really. Injecting an interrupt preempts the guest injecting a host IPI, but there is no trap.
To properly test GICv4 direct vLPI injection from KVM selftests, we write a KVM_DEBUG_GIC_MSI_SETUP ioctl that manually creates an IRQ routing table entry for the specified MSI, and populates ITS structures (device, collection, and interrupt translation table entries) to map the MSI to a vLPI. We then call GICv4 kvm_vgic_v4_set_forwarding to let the vLPI bypass hypervisor traps and inject directly to the vCPU.
I think that's totally overkill, and there is at least two ways to achieve the same thing without adding any additional code to the kernel:
- your test can simulate the restore of a guest with pending interrupts in the in-memory tables, start it, see the expected interrupts in the guest. Additional benefit: you can now test LPI restore.
- you use the interrupt injection mechanism that has been in the core code since 536e2e34bd0022, and let the GIC inject the interrupt for you. In case you wonder why it is there: for the exact purpose you describe.
Thanks,
M.
linux-kselftest-mirror@lists.linaro.org