[RFC PATCH 12/13] KVM: selftests: Check CPUID handling of plane twins

From: David Woodhouse

Date: Fri Nov 13 2026 - 14:20:47 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

Two checks of how plane twins follow the CPUID they share.

A plane vCPU created after KVM_SET_CPUID gets its Hyper-V context in
kvm_arch_vcpu_create(), before its vCPU index is assigned, and used to
latch a VP index of 0xFFFFFFFF. Create two vCPUs, give them a Hyper-V
CPUID, then create their plane-1 twins and check that
HV_X64_MSR_VP_INDEX on each twin reads its vCPU index.

Once a twin has run, KVM_SET_CPUID2 must accept only the CPUID already
set, even on a plane 0 that has never run. Run plane 1 with plane 0
held (a real-mode OUT, then retired), and check that identical CPUID is
accepted and that CPUID with XTILE added to leaf 0xD fails with EINVAL.
Before kvm_set_cpuid() checked every twin, that call went on to the
fpstate code, which failed it with EPERM, or resized the twin that ran
if the process had AMX permission.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
---
tools/testing/selftests/kvm/x86/planes_test.c | 125 +++++++++++++++++-
1 file changed, 122 insertions(+), 3 deletions(-)

diff --git a/tools/testing/selftests/kvm/x86/planes_test.c b/tools/testing/selftests/kvm/x86/planes_test.c
index 968f1b099b8e..dd3c251f0b7a 100644
--- a/tools/testing/selftests/kvm/x86/planes_test.c
+++ b/tools/testing/selftests/kvm/x86/planes_test.c
@@ -1,16 +1,19 @@
// 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.
+ * creation bounds, plane vCPU creation, the independence of
+ * per-plane vCPU register state, the Hyper-V VP index of a plane
+ * vCPU created after KVM_SET_CPUID, and KVM_SET_CPUID2 once a twin
+ * has run.
*/
#include <errno.h>
#include <string.h>

#include "kvm_util.h"
#include "processor.h"
+#include "hyperv.h"

-/* Where the run-state test puts plane 0's one instruction. */
+/* Where the tests that enter a plane put its one instruction. */
#define CODE_GPA 0x1000

/*
@@ -317,6 +320,120 @@ static void test_plane_lifecycle(void)
kvm_vm_free(vm);
}

+/*
+ * A plane vCPU created after KVM_SET_CPUID gets its Hyper-V context at
+ * creation, and its default VP index must be its vCPU index like any
+ * other vCPU's, not the -1 the vCPU carries until it is published.
+ */
+static void test_late_twin_vp_index(void)
+{
+ struct kvm_plane_vcpu *twin;
+ struct kvm_vcpu *vcpu[2];
+ struct kvm_plane *plane;
+ struct kvm_vm *vm;
+ int i;
+
+ if (!kvm_has_cap(KVM_CAP_SYS_HYPERV_CPUID)) {
+ print_skip("KVM_CAP_SYS_HYPERV_CPUID not supported");
+ return;
+ }
+
+ vm = vm_create_barebones_irqchip(true);
+ vm_enable_cap(vm, KVM_CAP_PLANES, 2);
+ plane = vm_plane_add(vm, 1);
+
+ for (i = 0; i < 2; i++) {
+ vcpu[i] = __vm_vcpu_add(vm, i);
+ vcpu_set_hv_cpuid(vcpu[i]);
+ }
+
+ for (i = 0; i < 2; i++) {
+ twin = __vm_plane_vcpu_add(vcpu[i], plane);
+ TEST_ASSERT_EQ(plane_vcpu_get_msr(twin, HV_X64_MSR_VP_INDEX), i);
+ }
+
+ kvm_vm_free(vm);
+}
+
+/*
+ * CPUID is shared between plane twins, so once any twin has run,
+ * KVM_SET_CPUID2 on plane 0 only accepts the CPUID already set, even
+ * while plane 0 itself has never run. Without that, an enlarged
+ * dynamic xfeature would resize the fpstate of the twin that ran.
+ * Plane 1 runs here with plane 0 held: a real-mode OUT at CODE_GPA,
+ * which is then retired.
+ */
+static void test_cpuid_after_twin_ran(void)
+{
+ static const u8 out_insn[] = { 0xe6, 0x10 }; /* out %al, $0x10 */
+ u64 hold = KVM_PLANE_RUN_STATE_USER_HOLD, none = 0;
+ struct kvm_cpuid_entry2 *leaf_d = NULL;
+ struct kvm_plane_vcpu *vcpu1;
+ struct kvm_cpuid2 *enlarged;
+ struct kvm_plane *plane;
+ struct kvm_sregs sregs;
+ struct kvm_vcpu *vcpu0;
+ struct kvm_regs regs;
+ struct kvm_vm *vm;
+ u32 i;
+
+ vm = vm_create_barebones_irqchip(true);
+ vm_enable_cap(vm, KVM_CAP_PLANES, 2);
+ plane = vm_plane_add(vm, 1);
+ vcpu0 = __vm_vcpu_add(vm, 0);
+ vcpu1 = __vm_plane_vcpu_add(vcpu0, plane);
+ vcpu_init_cpuid(vcpu0, kvm_get_supported_cpuid());
+
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, CODE_GPA, 1, 1, 0);
+ memcpy(addr_gpa2hva(vm, CODE_GPA), out_insn, sizeof(out_insn));
+
+ plane_vcpu_ioctl(vcpu1, KVM_GET_SREGS, &sregs);
+ sregs.cs.selector = 0;
+ sregs.cs.base = 0;
+ plane_vcpu_ioctl(vcpu1, KVM_SET_SREGS, &sregs);
+ plane_vcpu_ioctl(vcpu1, KVM_GET_REGS, &regs);
+ regs.rip = CODE_GPA;
+ plane_vcpu_ioctl(vcpu1, KVM_SET_REGS, &regs);
+
+ vcpu_ioctl(vcpu0, KVM_SET_PLANE_RUN_STATE, &hold);
+ plane_vcpu_ioctl(vcpu1, KVM_SET_PLANE_RUN_STATE, &none);
+ TEST_ASSERT_EQ(__vcpu_run(vcpu0), 0);
+ TEST_ASSERT_EQ(vcpu0->run->exit_reason, KVM_EXIT_IO);
+ TEST_ASSERT_EQ(vcpu0->run->io.port, 0x10);
+
+ /* Retire the OUT without entering again. */
+ vcpu0->run->immediate_exit = 1;
+ TEST_ASSERT_EQ(__vcpu_run(vcpu0), -1);
+ TEST_ASSERT_EQ(errno, EINTR);
+ vcpu0->run->immediate_exit = 0;
+
+ /* Identical CPUID is still accepted. */
+ TEST_ASSERT_EQ(__vcpu_ioctl(vcpu0, KVM_SET_CPUID2, vcpu0->cpuid), 0);
+
+ /*
+ * An enlarged dynamic xfeature is refused by the has-run check
+ * (EINVAL), before the fpstate code could fail it for lack of
+ * permission (EPERM).
+ */
+ enlarged = allocate_kvm_cpuid2(vcpu0->cpuid->nent);
+ memcpy(enlarged, vcpu0->cpuid, kvm_cpuid2_size(vcpu0->cpuid->nent));
+ for (i = 0; i < enlarged->nent; i++) {
+ if (enlarged->entries[i].function == 0xd &&
+ enlarged->entries[i].index == 0)
+ leaf_d = &enlarged->entries[i];
+ }
+ if (leaf_d) {
+ leaf_d->eax |= XFEATURE_MASK_XTILE;
+ TEST_ASSERT_EQ(__vcpu_ioctl(vcpu0, KVM_SET_CPUID2, enlarged), -1);
+ TEST_ASSERT_EQ(errno, EINVAL);
+ } else {
+ print_skip("no CPUID leaf 0xD, enlarged-xfeature check");
+ }
+ free(enlarged);
+
+ kvm_vm_free(vm);
+}
+
int main(int argc, char *argv[])
{
TEST_REQUIRE(kvm_has_cap(KVM_CAP_PLANES));
@@ -326,6 +443,8 @@ int main(int argc, char *argv[])
test_enable_cap_bounds();
test_enable_cap_too_late();
test_plane_lifecycle();
+ test_late_twin_vp_index();
+ test_cpuid_after_twin_ran();

return 0;
}
--
2.50.1