[RFC PATCH 04/55] KVM: selftests: Add x86 planes lifecycle test

From: David Woodhouse

Date: Sat Oct 17 2026 - 08:53:22 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

Cover KVM_CAP_PLANES argument validation and ordering, plane creation
bounds, plane vCPU creation, and per-plane isolation of vCPU register
state.

The tools copy of linux/kvm.h is synced to pick up the VM planes
definitions the test builds against.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
Assisted-by: LLM
---
tools/include/uapi/linux/kvm.h | 40 +++++-
tools/testing/selftests/kvm/Makefile.kvm | 1 +
tools/testing/selftests/kvm/x86/planes_test.c | 120 ++++++++++++++++++
3 files changed, 156 insertions(+), 5 deletions(-)
create mode 100644 tools/testing/selftests/kvm/x86/planes_test.c

diff --git a/tools/include/uapi/linux/kvm.h b/tools/include/uapi/linux/kvm.h
index fe2f500c932d..dcd12f58186e 100644
--- a/tools/include/uapi/linux/kvm.h
+++ b/tools/include/uapi/linux/kvm.h
@@ -11,6 +11,7 @@
#include <linux/const.h>
#include <linux/types.h>
#include <linux/compiler.h>
+#include <linux/stddef.h>
#include <linux/ioctl.h>
#include <asm/kvm.h>

@@ -145,6 +146,13 @@ struct kvm_exit_snp_req_certs {
__u64 ret;
};

+struct kvm_plane_event_exit {
+#define KVM_PLANE_EVENT_CREATE_VCPU 1
+ __u32 cause;
+ __u32 plane;
+ __u64 extra[8];
+};
+
#define KVM_S390_GET_SKEYS_NONE 1
#define KVM_S390_SKEYS_MAX 1048576

@@ -192,6 +200,7 @@ struct kvm_exit_snp_req_certs {
#define KVM_EXIT_ARM_SEA 41
#define KVM_EXIT_ARM_LDST64B 42
#define KVM_EXIT_SNP_REQ_CERTS 43
+#define KVM_EXIT_PLANE_EVENT 44

/* For KVM_EXIT_INTERNAL_ERROR */
/* Emulate instruction failed. */
@@ -456,6 +465,9 @@ struct kvm_run {
} notify;
/* KVM_EXIT_MEMORY_FAULT */
struct {
+#define KVM_MEMORY_EXIT_FLAG_READ (1ULL << 0)
+#define KVM_MEMORY_EXIT_FLAG_WRITE (1ULL << 1)
+#define KVM_MEMORY_EXIT_FLAG_EXEC (1ULL << 2)
#define KVM_MEMORY_EXIT_FLAG_PRIVATE (1ULL << 3)
__u64 flags;
__u64 gpa;
@@ -496,6 +508,8 @@ struct kvm_run {
} arm_sea;
/* KVM_EXIT_SNP_REQ_CERTS */
struct kvm_exit_snp_req_certs snp_req_certs;
+ /* KVM_EXIT_PLANE_EVENT */
+ struct kvm_plane_event_exit plane_event;
/* Fix the size of the union. */
char padding[256];
};
@@ -542,7 +556,7 @@ struct kvm_coalesced_mmio {

struct kvm_coalesced_mmio_ring {
__u32 first, last;
- struct kvm_coalesced_mmio coalesced_mmio[];
+ __DECLARE_FLEX_ARRAY(struct kvm_coalesced_mmio, coalesced_mmio);
};

#define KVM_COALESCED_MMIO_MAX \
@@ -592,7 +606,7 @@ struct kvm_clear_dirty_log {
/* for KVM_SET_SIGNAL_MASK */
struct kvm_signal_mask {
__u32 len;
- __u8 sigset[];
+ __DECLARE_FLEX_ARRAY(__u8, sigset);
};

/* for KVM_TPR_ACCESS_REPORTING */
@@ -703,6 +717,11 @@ struct kvm_enable_cap {
#define KVM_VM_TYPE_ARM_IPA_SIZE_MASK 0xffULL
#define KVM_VM_TYPE_ARM_IPA_SIZE(x) \
((x) & KVM_VM_TYPE_ARM_IPA_SIZE_MASK)
+
+#define KVM_VM_TYPE_ARM_PROTECTED (1UL << 31)
+#define KVM_VM_TYPE_ARM_MASK (KVM_VM_TYPE_ARM_IPA_SIZE_MASK | \
+ KVM_VM_TYPE_ARM_PROTECTED)
+
/*
* ioctls for /dev/kvm fds:
*/
@@ -724,6 +743,11 @@ struct kvm_enable_cap {
#define KVM_GET_EMULATED_CPUID _IOWR(KVMIO, 0x09, struct kvm_cpuid2)
#define KVM_GET_MSR_FEATURE_INDEX_LIST _IOWR(KVMIO, 0x0a, struct kvm_msr_list)

+/*
+ * Maximum number of supported planes
+ */
+#define KVM_MAX_PLANES 16
+
/*
* Extension capability list.
*/
@@ -989,6 +1013,8 @@ struct kvm_enable_cap {
#define KVM_CAP_ARM_SEA_TO_USER 245
#define KVM_CAP_S390_USER_OPEREXEC 246
#define KVM_CAP_S390_KEYOP 247
+#define KVM_CAP_S390_VSIE_ESAMODE 248
+#define KVM_CAP_PLANES 249

struct kvm_irq_routing_irqchip {
__u32 irqchip;
@@ -1051,7 +1077,7 @@ struct kvm_irq_routing_entry {
struct kvm_irq_routing {
__u32 nr;
__u32 flags;
- struct kvm_irq_routing_entry entries[];
+ __DECLARE_FLEX_ARRAY(struct kvm_irq_routing_entry, entries);
};

#define KVM_IRQFD_FLAG_DEASSIGN (1 << 0)
@@ -1142,7 +1168,7 @@ struct kvm_dirty_tlb {

struct kvm_reg_list {
__u64 n; /* number of regs */
- __u64 reg[];
+ __DECLARE_FLEX_ARRAY(__u64, reg);
};

struct kvm_one_reg {
@@ -1224,6 +1250,8 @@ enum kvm_device_type {
#define KVM_DEV_TYPE_LOONGARCH_EIOINTC KVM_DEV_TYPE_LOONGARCH_EIOINTC
KVM_DEV_TYPE_LOONGARCH_PCHPIC,
#define KVM_DEV_TYPE_LOONGARCH_PCHPIC KVM_DEV_TYPE_LOONGARCH_PCHPIC
+ KVM_DEV_TYPE_LOONGARCH_DMSINTC,
+#define KVM_DEV_TYPE_LOONGARCH_DMSINTC KVM_DEV_TYPE_LOONGARCH_DMSINTC
KVM_DEV_TYPE_ARM_VGIC_V5,
#define KVM_DEV_TYPE_ARM_VGIC_V5 KVM_DEV_TYPE_ARM_VGIC_V5

@@ -1340,6 +1368,8 @@ struct kvm_s390_keyop {
#define KVM_GET_DEVICE_ATTR _IOW(KVMIO, 0xe2, struct kvm_device_attr)
#define KVM_HAS_DEVICE_ATTR _IOW(KVMIO, 0xe3, struct kvm_device_attr)

+#define KVM_CREATE_PLANE _IO(KVMIO, 0xe4)
+
/*
* ioctls for vcpu fds
*/
@@ -1610,7 +1640,7 @@ struct kvm_stats_desc {
#ifdef __KERNEL__
char name[KVM_STATS_NAME_SIZE];
#else
- char name[];
+ __DECLARE_FLEX_ARRAY(char, name);
#endif
};

diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 61e767a1e31b..5883359c1a5d 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -104,6 +104,7 @@ TEST_GEN_PROGS_x86 += x86/nested_tsc_scaling_test
TEST_GEN_PROGS_x86 += x86/nested_vmsave_vmload_test
TEST_GEN_PROGS_x86 += x86/platform_info_test
TEST_GEN_PROGS_x86 += x86/plane_x86_test
+TEST_GEN_PROGS_x86 += x86/planes_test
TEST_GEN_PROGS_x86 += x86/pmu_counters_test
TEST_GEN_PROGS_x86 += x86/pmu_event_filter_test
TEST_GEN_PROGS_x86 += x86/private_mem_conversions_test
diff --git a/tools/testing/selftests/kvm/x86/planes_test.c b/tools/testing/selftests/kvm/x86/planes_test.c
new file mode 100644
index 000000000000..3517175580d3
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/planes_test.c
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Test the KVM_CAP_PLANES opt-in and the plane lifecycle uAPI: plane
+ * creation bounds, plane vCPU creation, and the independence of
+ * per-plane vCPU register state.
+ */
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "kvm_util.h"
+
+static int plane_vcpu_fd(int plane_fd, int vcpu_id)
+{
+ return __kvm_ioctl(plane_fd, KVM_CREATE_VCPU, (void *)(unsigned long)vcpu_id);
+}
+
+static void test_enable_cap_bounds(void)
+{
+ struct kvm_vm *vm = vm_create_barebones();
+
+ /* Planes require a split irqchip; the cap itself does not. */
+ TEST_ASSERT_EQ(__vm_enable_cap(vm, KVM_CAP_PLANES, 0), -1);
+ TEST_ASSERT_EQ(errno, EINVAL);
+
+ TEST_ASSERT_EQ(__vm_enable_cap(vm, KVM_CAP_PLANES, KVM_MAX_PLANES + 1), -1);
+ TEST_ASSERT_EQ(errno, EINVAL);
+
+ vm_enable_cap(vm, KVM_CAP_PLANES, KVM_MAX_PLANES);
+
+ kvm_vm_free(vm);
+}
+
+static void test_enable_cap_too_late(void)
+{
+ struct kvm_vm *vm = vm_create_barebones();
+ int vcpu_fd;
+
+ vcpu_fd = __vm_ioctl(vm, KVM_CREATE_VCPU, NULL);
+ TEST_ASSERT(vcpu_fd >= 0, "KVM_CREATE_VCPU failed: errno %d", errno);
+
+ TEST_ASSERT_EQ(__vm_enable_cap(vm, KVM_CAP_PLANES, 2), -1);
+ TEST_ASSERT_EQ(errno, EEXIST);
+
+ close(vcpu_fd);
+ kvm_vm_free(vm);
+}
+
+static void test_plane_lifecycle(void)
+{
+ struct kvm_vm *vm = vm_create_barebones();
+ struct kvm_regs regs0, regs1;
+ int plane_fd, vcpu0_fd, vcpu1_fd;
+ int r;
+
+ vm_enable_cap(vm, KVM_CAP_SPLIT_IRQCHIP, 24);
+
+ /* Without the opt-in a VM has a single plane. */
+ TEST_ASSERT_EQ(vm_check_cap(vm, KVM_CAP_PLANES), 1);
+ TEST_ASSERT_EQ(__vm_ioctl(vm, KVM_CREATE_PLANE, (void *)1UL), -1);
+ TEST_ASSERT_EQ(errno, EINVAL);
+
+ vm_enable_cap(vm, KVM_CAP_PLANES, 2);
+ TEST_ASSERT_EQ(vm_check_cap(vm, KVM_CAP_PLANES), 2);
+
+ /* The configured count is a hard bound, and reconfiguration a hard no. */
+ TEST_ASSERT_EQ(__vm_ioctl(vm, KVM_CREATE_PLANE, (void *)2UL), -1);
+ TEST_ASSERT_EQ(errno, EINVAL);
+
+ plane_fd = __vm_ioctl(vm, KVM_CREATE_PLANE, (void *)1UL);
+ TEST_ASSERT(plane_fd >= 0, "KVM_CREATE_PLANE failed: errno %d", errno);
+
+ TEST_ASSERT_EQ(__vm_enable_cap(vm, KVM_CAP_PLANES, 3), -1);
+ TEST_ASSERT_EQ(errno, EEXIST);
+
+ /*
+ * A plane cannot host planes of its own. Plane fds that do not
+ * implement KVM_CHECK_EXTENSION at all satisfy that trivially.
+ */
+ r = __kvm_ioctl(plane_fd, KVM_CHECK_EXTENSION, (void *)KVM_CAP_PLANES);
+ TEST_ASSERT(r == 0 || (r == -1 && errno == ENOTTY),
+ "plane fd reports nested plane support: %d", r);
+
+ vcpu0_fd = __vm_ioctl(vm, KVM_CREATE_VCPU, NULL);
+ TEST_ASSERT(vcpu0_fd >= 0, "KVM_CREATE_VCPU failed: errno %d", errno);
+
+ vcpu1_fd = plane_vcpu_fd(plane_fd, 0);
+ TEST_ASSERT(vcpu1_fd >= 0,
+ "KVM_CREATE_VCPU on plane 1 failed: errno %d", errno);
+
+ /* Register state is per plane: write plane 1, plane 0 unaffected. */
+ kvm_ioctl(vcpu1_fd, KVM_GET_REGS, &regs1);
+ regs1.rbx = 0x5157ca11; /* arbitrary marker */
+ kvm_ioctl(vcpu1_fd, KVM_SET_REGS, &regs1);
+
+ kvm_ioctl(vcpu0_fd, KVM_GET_REGS, &regs0);
+ TEST_ASSERT(regs0.rbx != 0x5157ca11,
+ "plane 0 RBX changed by plane 1 KVM_SET_REGS");
+
+ memset(&regs1, 0, sizeof(regs1));
+ kvm_ioctl(vcpu1_fd, KVM_GET_REGS, &regs1);
+ TEST_ASSERT_EQ(regs1.rbx, 0x5157ca11);
+
+ close(vcpu1_fd);
+ close(vcpu0_fd);
+ close(plane_fd);
+ kvm_vm_free(vm);
+}
+
+int main(int argc, char *argv[])
+{
+ TEST_REQUIRE(kvm_has_cap(KVM_CAP_PLANES));
+
+ test_enable_cap_bounds();
+ test_enable_cap_too_late();
+ test_plane_lifecycle();
+
+ return 0;
+}
--
2.50.1