[PATCH v3 1/5] KVM: nVMX: Preserve L1-owned bits in CR{0,4} read shadows

From: Sean Christopherson

Date: Fri Oct 30 2026 - 17:41:04 EST


When emulating an intercepted MOV CR{0,4} for L2, i.e. when L2 is toggling
CR{0,4} bits that L0 intercepts but L1 does not, explicitly preserve
L1-owned bits in the vmcs02 read shadow instead of subtly relying on the
interception logic to preserve the bits. For straight MOV instructions,
relying on the interception logic works, because KVM would have synthesized
a nested VM-Exit to L1 if L2 were toggling L1-owned bits, i.e. if L2 were
explicitly changing any of the shadowed bits.

However, for LMSW, which preserves bits 31:4, and conditionally preserves
CR0.PE (bit 0), the effective value being set by L2 can diverge from the
read shadow *without* triggering a nested VM-Exit. E.g. if CR0.PE=1 in
"hardware", CR0_READ_SHADOW.PE=0, and CR0_GUEST_HOST_MASK.PE=1, then a LMSW
with CR0.PE=0 won't VM-Exit, but will preserve/set CR0.PE=1 in "hardware".

Explicitly preserving the shadowed bits will allow using handle_set_cr0()
to honor L1's guest/host mask when emulating LMSW.

Signed-off-by: Sean Christopherson <seanjc@xxxxxxxxxx>
---
arch/x86/kvm/vmx/vmx.c | 30 +++++++++++++-----------------
1 file changed, 13 insertions(+), 17 deletions(-)

diff --git a/arch/x86/kvm/vmx/vmx.c b/arch/x86/kvm/vmx/vmx.c
index b341af4c6057..544b7840897c 100644
--- a/arch/x86/kvm/vmx/vmx.c
+++ b/arch/x86/kvm/vmx/vmx.c
@@ -5629,22 +5629,19 @@ static int handle_set_cr0(struct kvm_vcpu *vcpu, unsigned long val)
{
if (is_guest_mode(vcpu)) {
struct vmcs12 *vmcs12 = get_vmcs12(vcpu);
- unsigned long orig_val = val;
+ u64 mask = vmcs12->cr0_guest_host_mask;

/*
- * We get here when L2 changed cr0 in a way that did not change
- * any of L1's shadowed bits (see nested_vmx_exit_handled_cr),
- * but did change L0 shadowed bits. So we first calculate the
- * effective cr0 value that L1 would like to write into the
- * hardware. It consists of the L2-owned bits from the new
- * value combined with the L1-owned bits from L1's guest_cr0.
+ * L2 is writing a CR0 value that isn't modifying any of L1's
+ * shadowed bits (see nested_vmx_exit_handled_cr()), but *is*
+ * modifying L0 shadowed bits. Set the "hardware" CR0 and the
+ * read shadow using the L2-owned bits from the new value,
+ * combined with the L1-owned bits.
*/
- val = (val & ~vmcs12->cr0_guest_host_mask) |
- (vmcs12->guest_cr0 & vmcs12->cr0_guest_host_mask);
-
- if (kvm_set_cr0(vcpu, val))
+ val &= ~mask;
+ if (kvm_set_cr0(vcpu, val | (vmcs12->guest_cr0 & mask)))
return 1;
- vmcs_writel(CR0_READ_SHADOW, orig_val);
+ vmcs_writel(CR0_READ_SHADOW, val | (vmcs12->cr0_read_shadow & mask));
return 0;
} else {
return kvm_set_cr0(vcpu, val);
@@ -5655,14 +5652,13 @@ static int handle_set_cr4(struct kvm_vcpu *vcpu, unsigned long val)
{
if (is_guest_mode(vcpu)) {
struct vmcs12 *vmcs12 = get_vmcs12(vcpu);
- unsigned long orig_val = val;
+ u64 mask = vmcs12->cr4_guest_host_mask;

/* analogously to handle_set_cr0 */
- val = (val & ~vmcs12->cr4_guest_host_mask) |
- (vmcs12->guest_cr4 & vmcs12->cr4_guest_host_mask);
- if (kvm_set_cr4(vcpu, val))
+ val &= ~mask;
+ if (kvm_set_cr4(vcpu, val | (vmcs12->guest_cr4 & mask)))
return 1;
- vmcs_writel(CR4_READ_SHADOW, orig_val);
+ vmcs_writel(CR4_READ_SHADOW, val | (vmcs12->cr4_read_shadow & mask));
return 0;
} else
return kvm_set_cr4(vcpu, val);
--
2.56.0.385.gac671c819a-goog