[RFC PATCH 10/13] KVM: planes: Let plane twins share XCR0
From: David Woodhouse
Date: Fri Nov 13 2026 - 14:12:35 EST
From: David Woodhouse <dwmw@xxxxxxxxxxxx>
XCR0/XFEM is shared between VTLs (TLFS 15.4). The planes series keeps
arch.xcr0 per vCPU, so an XSETBV in one VTL is invisible in the other,
and VTL1 reads a stale XFEM: a feature VTL0 enabled (AVX, for the
Windows secure kernel) is not enabled in VTL1's XCR0. The register
contents themselves are a separate matter, carried by the code that
switches VTLs.
Add KVM_PLANE_SHARE_XCR0 to the KVM_CAP_PLANES share mask. With it set,
propagate the validated value to every plane twin in __kvm_set_xcr(),
covering both guest XSETBV and the KVM_SET_XCRS ioctl path, and let a
twin created later inherit plane 0's current value. The CPUID
dynamic-bits dirty marker lives in the shared common state, so the
existing single set covers the twins. Update the planes section of
api.rst and the kvm_cpuid_apply_runtime() comment, which still listed
XCR0 among the per-plane registers.
SEV-SNP VMPLs bank XCR0 per VMSA, and sharing it there would be wrong
in two ways: sev_es_sync_from_ghcb() feeds a VMPL's GHCB XCR0 to
__kvm_set_xcr(), so a less-privileged VMPL would overwrite its siblings'
values (and the MSR-protocol CPUID leaf 0xD sizes would then follow the
wrong VMPL); and a pre-launch KVM_SET_XCRS on one twin would change
plane 0's VMSA, and with it the launch measurement. An SNP VM cannot
set the share mask, since KVM_CAP_PLANES refuses its VM type, so its
VMPLs keep XCR0 of their own by construction.
The twins' fpstate sizing does not depend on this bit: the planes share
CPUID, so any twin's own XSETBV can enable a dynamic xfeature whether or
not XCR0 is shared.
Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
---
Documentation/virt/kvm/api.rst | 11 ++++++++---
arch/x86/include/uapi/asm/kvm.h | 3 +++
arch/x86/kvm/cpuid.c | 4 +++-
arch/x86/kvm/x86.c | 34 +++++++++++++++++++++++++++++++++
arch/x86/kvm/x86.h | 2 +-
5 files changed, 49 insertions(+), 5 deletions(-)
diff --git a/Documentation/virt/kvm/api.rst b/Documentation/virt/kvm/api.rst
index a6008cb33e99..22dc3c793bc4 100644
--- a/Documentation/virt/kvm/api.rst
+++ b/Documentation/virt/kvm/api.rst
@@ -374,8 +374,9 @@ The planes of a vCPU share its CPUID model, set through the plane 0 fd with
KVM_SET_CPUID2. The CPUID bits that follow a vCPU's own registers (OSXSAVE
from CR4.OSXSAVE, OSPKE from CR4.PKE, the APIC enable, MWAIT from
MISC_ENABLE, and the XSAVE area sizes from XCR0 and XSS) reflect the plane
-that executes CPUID, since every plane has these registers of its own.
-KVM_GET_CPUID2 returns them as plane 0 sees them.
+that executes CPUID, since every plane has these registers of its own,
+unless the VM asked the planes to share XCR0 with KVM_PLANE_SHARE_XCR0 (see
+:ref:`KVM_CAP_PLANES`). KVM_GET_CPUID2 returns them as plane 0 sees them.
4.8 KVM_GET_DIRTY_LOG
---------------------
@@ -9347,7 +9348,11 @@ through one plane is seen by all of them. KVM_CHECK_EXTENSION
(KVM_CAP_PLANES_SHARE) returns the bits KVM supports on the system fd, and
the bits the VM enabled on a VM fd. Setting an unsupported bit, or a
non-zero args[2] or args[3], fails with -EINVAL. Like the count, the mask
-is set once.
+is set once. The bits are:
+
+- KVM_PLANE_SHARE_XCR0: XCR0, whether written by the guest with XSETBV or
+ with KVM_SET_XCRS on any plane's vCPU fd. A plane vCPU created later
+ starts with plane 0's value.
8. Other capabilities.
======================
diff --git a/arch/x86/include/uapi/asm/kvm.h b/arch/x86/include/uapi/asm/kvm.h
index 00ce265391e1..edec00979acf 100644
--- a/arch/x86/include/uapi/asm/kvm.h
+++ b/arch/x86/include/uapi/asm/kvm.h
@@ -922,6 +922,9 @@ struct kvm_sev_snp_launch_finish {
#define KVM_X2APIC_ENABLE_SUPPRESS_EOI_BROADCAST _BITULL(2)
#define KVM_X2APIC_DISABLE_SUPPRESS_EOI_BROADCAST _BITULL(3)
+/* KVM_CAP_PLANES args[1]: state the planes of a vCPU share */
+#define KVM_PLANE_SHARE_XCR0 _BITULL(0)
+
struct kvm_hyperv_eventfd {
__u32 conn_id;
__s32 fd;
diff --git a/arch/x86/kvm/cpuid.c b/arch/x86/kvm/cpuid.c
index 971ebd781c6e..aa89d1b6f6c4 100644
--- a/arch/x86/kvm/cpuid.c
+++ b/arch/x86/kvm/cpuid.c
@@ -2155,7 +2155,9 @@ get_out_of_range_cpuid_entry(struct kvm_vcpu *vcpu, u32 *fn_ptr, u32 index)
/*
* The planes share the CPUID table but not CR4, the APIC enable, MISC_ENABLE
- * or XCR0/XSS: the bits that follow those come from the executing plane.
+ * or XSS, nor XCR0 unless userspace asked for KVM_PLANE_SHARE_XCR0, in which
+ * case __kvm_set_xcr() keeps it equal across the twins: the bits that follow
+ * those come from the executing plane.
*/
static void kvm_cpuid_apply_runtime(struct kvm_vcpu *vcpu, u32 function,
u32 index, u32 *eax, u32 *ebx, u32 *ecx,
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 4d70e111eeb0..ceec10d116a9 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -799,6 +799,31 @@ int __kvm_set_xcr(struct kvm_vcpu *vcpu, u32 index, u64 xcr)
vcpu->arch.xcr0 = xcr0;
+ /*
+ * XCR0/XFEM is shared across VTLs (TLFS 15.4): propagate the new
+ * value to every plane twin so the other VTLs observe it. This
+ * covers both guest XSETBV and the KVM_SET_XCRS ioctl path. The
+ * CPUID dirty marker below lives in the shared common state, so one
+ * set covers the twins too.
+ *
+ * Only planes that userspace asked to share XCR0 do so. SEV-SNP
+ * VMPLs bank it per VMSA, and an SNP VM cannot set the share mask
+ * (KVM_CAP_PLANES refuses its VM type), so they never fan out.
+ *
+ * Skip the fan-out until this vCPU is published in
+ * common->vcpus[]: the architectural reset of a twin still being
+ * created must not clobber a live sibling's XCR0 (the new twin
+ * inherits the shared value instead, in kvm_arch_vcpu_create()).
+ */
+ if (kvm_plane_shares(vcpu->kvm, KVM_PLANE_SHARE_XCR0) && vcpu->common &&
+ vcpu->common->vcpus[vcpu->plane_level] == vcpu) {
+ struct kvm_vcpu *twin;
+ int i;
+
+ vcpu_for_each_plane(vcpu->common, i, twin)
+ twin->arch.xcr0 = xcr0;
+ }
+
if ((xcr0 ^ old_xcr0) & XFEATURE_MASK_EXTEND)
cpuid_set_dirty(vcpu);
return 0;
@@ -9820,6 +9845,15 @@ int kvm_arch_vcpu_create(struct kvm_vcpu *vcpu)
kvm_vcpu_after_set_cpuid(vcpu);
kvm_set_tsc_khz(vcpu, vcpu->kvm->arch.default_tsc_khz);
kvm_vcpu_reset(vcpu, false);
+ /*
+ * With XCR0 shared, a twin created after its plane-0 sibling has run
+ * (Hyper-V VSM enables VTL1 at runtime) inherits the current shared
+ * value; the architectural reset value only stands when no sibling
+ * exists yet, or when the planes keep XCR0 of their own.
+ */
+ if (kvm_plane_shares(vcpu->kvm, KVM_PLANE_SHARE_XCR0) &&
+ vcpu->plane_level && vcpu->common->vcpus[0])
+ vcpu->arch.xcr0 = vcpu->common->vcpus[0]->arch.xcr0;
kvm_init_mmu(vcpu);
vcpu_put(vcpu);
return 0;
diff --git a/arch/x86/kvm/x86.h b/arch/x86/kvm/x86.h
index 2f51b476dbd1..aa8abd219507 100644
--- a/arch/x86/kvm/x86.h
+++ b/arch/x86/kvm/x86.h
@@ -927,7 +927,7 @@ unsigned kvm_x86_default_max_planes(struct kvm *kvm);
* The per-vCPU state that userspace may ask the planes of a vCPU to share,
* in args[1] of KVM_ENABLE_CAP(KVM_CAP_PLANES).
*/
-#define KVM_PLANE_SHARE_VALID 0
+#define KVM_PLANE_SHARE_VALID KVM_PLANE_SHARE_XCR0
static inline bool kvm_plane_shares(struct kvm *kvm, u64 state)
{
--
2.50.1