[RFC PATCH 13/13] KVM: arm64: selftests: Add test for per-vCPU vLPI control API
Maximilian Dittgen
mdittgen at amazon.de
Thu Nov 20 06:03:02 PST 2025
Add a selftest for KVM API ioctls for enabling, disabling, and
querying direct vLPI injection capability on a per-vCPU basis.
Ensure that ITS data structures remain correct, vPEIDs can
be reused by different vCPUs, and ioctl behavior works as
intended in corner cases (idempotent behavior, vGIC uninitialized).
Signed-off-by: Maximilian Dittgen <mdittgen at amazon.de>
---
arch/arm64/kvm/arm.c | 4 +
drivers/irqchip/irq-gic-v3-its.c | 6 +
include/linux/irqchip/arm-gic-v4.h | 1 +
include/uapi/linux/kvm.h | 1 +
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/per_vcpu_vlpi.c | 274 ++++++++++++++++++
6 files changed, 287 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/per_vcpu_vlpi.c
diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c
index ecc3c87889db..eea0d77508a2 100644
--- a/arch/arm64/kvm/arm.c
+++ b/arch/arm64/kvm/arm.c
@@ -21,6 +21,7 @@
#include <linux/sched/stat.h>
#include <linux/psci.h>
#include <trace/events/kvm.h>
+#include <linux/irqchip/arm-gic-v4.h>
#define CREATE_TRACE_POINTS
#include "trace_arm.h"
@@ -429,6 +430,9 @@ int kvm_vm_ioctl_check_extension(struct kvm *kvm, long ext)
case KVM_CAP_ARM_PER_VCPU_VLPI:
r = kvm_per_vcpu_vlpi_supported();
break;
+ case KVM_CAP_ARM_MAX_VPEID:
+ r = its_get_max_vpeid();
+ break;
default:
r = 0;
diff --git a/drivers/irqchip/irq-gic-v3-its.c b/drivers/irqchip/irq-gic-v3-its.c
index 0e0778d61df2..078a9cafaf17 100644
--- a/drivers/irqchip/irq-gic-v3-its.c
+++ b/drivers/irqchip/irq-gic-v3-its.c
@@ -4546,6 +4546,12 @@ static const struct irq_domain_ops its_sgi_domain_ops = {
.deactivate = its_sgi_irq_domain_deactivate,
};
+int its_get_max_vpeid(void)
+{
+ return ITS_MAX_VPEID;
+}
+EXPORT_SYMBOL_GPL(its_get_max_vpeid);
+
static int its_vpe_id_alloc(void)
{
return ida_alloc_max(&its_vpeid_ida, ITS_MAX_VPEID - 1, GFP_KERNEL);
diff --git a/include/linux/irqchip/arm-gic-v4.h b/include/linux/irqchip/arm-gic-v4.h
index bd3e8de35147..3a42cccb72af 100644
--- a/include/linux/irqchip/arm-gic-v4.h
+++ b/include/linux/irqchip/arm-gic-v4.h
@@ -147,6 +147,7 @@ int its_alloc_vcpu_irqs(struct its_vm *vm);
int its_alloc_vcpu_irq(struct kvm_vcpu *vcpu);
void its_free_vcpu_irqs(struct its_vm *vm);
void its_free_vcpu_irq(struct kvm_vcpu *vcpu);
+int its_get_max_vpeid(void);
int its_make_vpe_resident(struct its_vpe *vpe, bool g0en, bool g1en);
int its_make_vpe_non_resident(struct its_vpe *vpe, bool db);
int its_commit_vpe(struct its_vpe *vpe);
diff --git a/include/uapi/linux/kvm.h b/include/uapi/linux/kvm.h
index 057eb9e61ac8..9f0ae2096e58 100644
--- a/include/uapi/linux/kvm.h
+++ b/include/uapi/linux/kvm.h
@@ -974,6 +974,7 @@ struct kvm_enable_cap {
#define KVM_CAP_GUEST_MEMFD_FLAGS 244
#define KVM_CAP_ARM_SEA_TO_USER 245
#define KVM_CAP_ARM_PER_VCPU_VLPI 246
+#define KVM_CAP_ARM_MAX_VPEID 247
struct kvm_irq_routing_irqchip {
__u32 irqchip;
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 02a7663c097b..71a929ef7e5d 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -162,6 +162,7 @@ TEST_GEN_PROGS_arm64 += arm64/host_sve
TEST_GEN_PROGS_arm64 += arm64/hypercalls
TEST_GEN_PROGS_arm64 += arm64/external_aborts
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
+TEST_GEN_PROGS_arm64 += arm64/per_vcpu_vlpi
TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
TEST_GEN_PROGS_arm64 += arm64/set_id_regs
diff --git a/tools/testing/selftests/kvm/arm64/per_vcpu_vlpi.c b/tools/testing/selftests/kvm/arm64/per_vcpu_vlpi.c
new file mode 100644
index 000000000000..9a5b1b40ff10
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/per_vcpu_vlpi.c
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test per-vCPU vLPI enable/disable/query correctness
+ */
+
+#include <linux/kvm.h>
+#include <pthread.h>
+#include <sys/resource.h>
+#include "test_util.h"
+#include "kvm_util.h"
+#include "processor.h"
+#include "gic.h"
+#include "vgic.h"
+#include "../kselftest_harness.h"
+
+static int MAX_VCPUS;
+static int ITS_MAX_VPEID;
+
+/* Dynamically fetch MAX_VCPUS and ITS_MAX_VPEID values */
+__attribute__((constructor))
+static void init_test_limits(void)
+{
+ int kvm_fd = open("/dev/kvm", O_RDWR);
+ int max_vcpus, max_vpeids;
+
+ if (kvm_fd >= 0) {
+ max_vcpus = ioctl(kvm_fd, KVM_CHECK_EXTENSION, KVM_CAP_MAX_VCPUS);
+ if (max_vcpus > 0)
+ MAX_VCPUS = max_vcpus;
+
+ max_vpeids = ioctl(kvm_fd, KVM_CHECK_EXTENSION, KVM_CAP_ARM_MAX_VPEID);
+ if (max_vpeids > 0)
+ ITS_MAX_VPEID = max_vpeids;
+
+ close(kvm_fd);
+ }
+}
+
+static void guest_code(void)
+{
+ GUEST_SYNC(0);
+ GUEST_DONE();
+}
+
+static void setup_vm_with_gic(struct kvm_vm **vm, struct kvm_vcpu **vcpu, int nr_vcpus)
+{
+ struct kvm_vcpu **vcpus;
+
+ TEST_REQUIRE(kvm_supports_vgic_v3());
+
+ if (nr_vcpus == 1) {
+ *vm = vm_create_with_one_vcpu(vcpu, guest_code);
+ } else {
+ vcpus = calloc(nr_vcpus, sizeof(*vcpus));
+ TEST_ASSERT(vcpus, "Failed to allocate vcpu array");
+ *vm = vm_create_with_vcpus(nr_vcpus, guest_code, vcpus);
+ *vcpu = vcpus[0];
+ free(vcpus);
+ }
+}
+
+static void cleanup_vm(struct kvm_vm *vm, int its_fd)
+{
+ if (its_fd >= 0)
+ close(its_fd);
+ kvm_vm_free(vm);
+}
+
+TEST(basic_vlpi_toggle)
+{
+ struct kvm_vm *vm;
+ struct kvm_vcpu *vcpu;
+ int its_fd, ret;
+ int vcpu_id = 0;
+
+ setup_vm_with_gic(&vm, &vcpu, 1);
+ its_fd = vgic_its_setup(vm);
+
+ ret = ioctl(vm->fd, KVM_QUERY_VCPU_VLPI, &vcpu_id);
+ EXPECT_GE(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_ENABLE_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_QUERY_VCPU_VLPI, &vcpu_id);
+ EXPECT_GT(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_DISABLE_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_QUERY_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ cleanup_vm(vm, its_fd);
+}
+
+/* recycle test */
+struct thread_data {
+ struct kvm_vm *vm;
+ int vcpu_id;
+ int ret;
+};
+
+static void *vlpi_thread(void *arg)
+{
+ struct thread_data *data = arg;
+
+ data->ret = ioctl(data->vm->fd, KVM_ENABLE_VCPU_VLPI, &data->vcpu_id);
+ if (data->ret == 0)
+ data->ret = ioctl(data->vm->fd, KVM_DISABLE_VCPU_VLPI, &data->vcpu_id);
+
+ return NULL;
+}
+
+TEST(vpeid_recycling)
+{
+ struct kvm_vm *vm;
+ struct kvm_vcpu *vcpu;
+ int its_fd;
+ int vcpu_id, i;
+ int cycles = (ITS_MAX_VPEID * 2) / MAX_VCPUS;
+ pthread_t threads[MAX_VCPUS];
+ struct thread_data data[MAX_VCPUS];
+
+ setup_vm_with_gic(&vm, &vcpu, MAX_VCPUS);
+ its_fd = vgic_its_setup(vm);
+
+ for (i = 0; i < cycles; i++) {
+ for (vcpu_id = 0; vcpu_id < MAX_VCPUS; vcpu_id++) {
+ data[vcpu_id].vm = vm;
+ data[vcpu_id].vcpu_id = vcpu_id;
+ pthread_create(&threads[vcpu_id], NULL, vlpi_thread, &data[vcpu_id]);
+ }
+
+ for (vcpu_id = 0; vcpu_id < MAX_VCPUS; vcpu_id++) {
+ pthread_join(threads[vcpu_id], NULL);
+ EXPECT_EQ(data[vcpu_id].ret, 0);
+ }
+ }
+
+ cleanup_vm(vm, its_fd);
+}
+
+TEST(double_enable_disable)
+{
+ struct kvm_vm *vm;
+ struct kvm_vcpu *vcpu;
+ int its_fd, ret;
+ int vcpu_id = 0;
+
+ setup_vm_with_gic(&vm, &vcpu, 1);
+ its_fd = vgic_its_setup(vm);
+
+ ret = ioctl(vm->fd, KVM_ENABLE_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_ENABLE_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_QUERY_VCPU_VLPI, &vcpu_id);
+ EXPECT_GT(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_DISABLE_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_DISABLE_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_QUERY_VCPU_VLPI, &vcpu_id);
+ EXPECT_EQ(ret, 0);
+
+ cleanup_vm(vm, its_fd);
+}
+
+TEST(uninitialized_vcpu)
+{
+ struct kvm_vm *vm;
+ struct kvm_vcpu *vcpu;
+ int its_fd, ret;
+ int invalid_vcpu_id = 999;
+
+ setup_vm_with_gic(&vm, &vcpu, 1);
+ its_fd = vgic_its_setup(vm);
+
+ ret = ioctl(vm->fd, KVM_QUERY_VCPU_VLPI, &invalid_vcpu_id);
+ EXPECT_LT(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_ENABLE_VCPU_VLPI, &invalid_vcpu_id);
+ EXPECT_LT(ret, 0);
+
+ ret = ioctl(vm->fd, KVM_DISABLE_VCPU_VLPI, &invalid_vcpu_id);
+ EXPECT_LT(ret, 0);
+
+ cleanup_vm(vm, its_fd);
+}
+
+TEST(vpeid_exhaustion)
+{
+ struct rlimit rlim;
+ struct kvm_vm **vms;
+ struct kvm_vcpu **vcpus;
+ int *its_fds;
+ /* Allocate enough VMs to exhaust vPEs, plus one */
+ int num_vms = ITS_MAX_VPEID / MAX_VCPUS + 1;
+ int remainder_vcpus = ITS_MAX_VPEID % MAX_VCPUS;
+ int vm_idx, vcpu_id, ret;
+ int successful_enables = 0;
+
+ /* Raise fd limit if below vPE limit, as we can't allocate enough vCPUs */
+ if (getrlimit(RLIMIT_NOFILE, &rlim) == 0) {
+ struct rlimit new_rlim = rlim;
+ /*
+ * Require [num_vms * (vcpus_per_vm + VM_fd + ITS_fd) + KVM] file
+ * descriptors, tripled for safety.
+ */
+ int required_fds = (num_vms * (MAX_VCPUS + 2) + 1) * 3;
+
+ if (rlim.rlim_cur < required_fds) {
+ new_rlim.rlim_cur = min_t(rlim_t, required_fds, rlim.rlim_max);
+ if (setrlimit(RLIMIT_NOFILE, &new_rlim) != 0) {
+ SKIP(return, "Need %d FDs, have %ld, cannot increase limit",
+ required_fds, rlim.rlim_cur);
+ }
+ }
+ }
+
+ vms = calloc(num_vms, sizeof(*vms));
+ vcpus = calloc(num_vms, sizeof(*vcpus));
+ its_fds = calloc(num_vms, sizeof(*its_fds));
+ TEST_ASSERT(vms && vcpus && its_fds, "Failed to allocate VM arrays");
+
+ /* Create all VMs */
+ for (vm_idx = 0; vm_idx < num_vms; vm_idx++) {
+ setup_vm_with_gic(&vms[vm_idx], &vcpus[vm_idx], MAX_VCPUS);
+ its_fds[vm_idx] = vgic_its_setup(vms[vm_idx]);
+ }
+
+ /* Exhaust all vPEs */
+ for (vm_idx = 0; vm_idx < num_vms - 1; vm_idx++) {
+ for (vcpu_id = 0; vcpu_id < MAX_VCPUS; vcpu_id++) {
+ ret = ioctl(vms[vm_idx]->fd, KVM_ENABLE_VCPU_VLPI, &vcpu_id);
+ if (ret == 0)
+ successful_enables++;
+ }
+ }
+
+ for (vcpu_id = 0; vcpu_id < remainder_vcpus; vcpu_id++) {
+ ret = ioctl(vms[num_vms - 1]->fd, KVM_ENABLE_VCPU_VLPI, &vcpu_id);
+ if (ret == 0)
+ successful_enables++;
+ }
+
+ /* Should have exhausted vPEID limit */
+ TEST_ASSERT(successful_enables == ITS_MAX_VPEID,
+ "Failed to allocate all existing vPEIDs");
+
+ /* Try assigning one more vPEID past exhaustion*/
+ vcpu_id = remainder_vcpus;
+ ret = ioctl(vms[num_vms - 1]->fd, KVM_ENABLE_VCPU_VLPI, &vcpu_id);
+
+ /* Verify failure to allocate additional vPEID */
+ TEST_ASSERT(ret < 0, "Failed to detect vPEID exhaustion");
+
+ /* Cleanup all VMs */
+ for (vm_idx = 0; vm_idx < num_vms; vm_idx++)
+ cleanup_vm(vms[vm_idx], its_fds[vm_idx]);
+
+ free(vms);
+ free(vcpus);
+ free(its_fds);
+ setrlimit(RLIMIT_NOFILE, &rlim); /* Restore fd limit */
+}
+
+TEST_HARNESS_MAIN
--
2.50.1 (Apple Git-155)
Amazon Web Services Development Center Germany GmbH
Tamara-Danz-Str. 13
10243 Berlin
Geschaeftsfuehrung: Christian Schlaeger, Christof Hellmis
Eingetragen am Amtsgericht Charlottenburg unter HRB 257764 B
Sitz: Berlin
Ust-ID: DE 365 538 597
More information about the linux-arm-kernel
mailing list