From: Sean Christopherson <seanjc@google.com>
To: Paolo Bonzini <pbonzini@redhat.com>,
Sean Christopherson <seanjc@google.com>
Cc: kvm@vger.kernel.org, linux-kernel@vger.kernel.org,
Mahmoud Abumandour <ma.mandourr@gmail.com>
Subject: [PATCH v3 5/5] KVM: selftests: Verify KVM handling of L1-owned CR0 bits on writes from L2
Date: Fri, 30 Oct 2026 14:37:47 -0700 [thread overview]
Message-ID: <20261030213747.2611815-6-seanjc@google.com> (raw)
In-Reply-To: <20261030213747.2611815-1-seanjc@google.com>
From: Mahmoud Abumandour <ma.mandourr@gmail.com>
Expand the nested exceptions test to verify KVM's handling of VMX's CR0
guest/host masks, i.e. of VMX's conditional interception of CR0 writes.
LMSW in particular is nasty, as it preserves bits 31:4 as well as CR0.PE
if it was already set.
Verify that MOV CR0 and LMSW in L2 that exits to L0, i.e. isn't intercepted
by L1, doesn't modify an L1-owned bits and reads the correct value from the
read shadow. Use CR0.TS to do the bulk of validation, as its value in
hardware can be easily inferred by triggering an #NM in L2. Specifically,
run with CR0.TS=1 in hardware but CR0.TS=0 in the read shadow, and verify
that non-intercepted writes to CR0 from L2 preserve the CR0.TS values in
hardware *and* the shadow.
Note, the downside of using CR0.TS is that any SIMD instructions in L2 will
hit a #NM, which is especially problematic since ucall(), e.g. for guest
prints and asserts, includes a memset(), for which the compiler likes to
generate SSE instructions. To avoid false passes and false failures, have
L2 snapshot the CR0 it sees, but do the actual validation in L1. E.g. if
L2 observed the wrong value, then doing GUEST_ASSERT_EQ() could get a false
pass as memset() would trigger the #NM expected by L1 (when L2 is trying to
report the failure).
Include CR0.NE and CR0.PE in the divergent shadow/hardware values as well
for extra coverage, e.g. CR0.NE is a must-be-1 bit, and CR0.PE is preserved
by LMSW, but if and only if it's already '1'.
Signed-off-by: Mahmoud Abumandour <ma.mandourr@gmail.com>
Co-developed-by: Sean Christopherson <seanjc@google.com>
Signed-off-by: Sean Christopherson <seanjc@google.com>
---
.../selftests/kvm/include/x86/processor.h | 5 ++
.../kvm/x86/nested_exceptions_test.c | 73 +++++++++++++++++++
2 files changed, 78 insertions(+)
diff --git a/tools/testing/selftests/kvm/include/x86/processor.h b/tools/testing/selftests/kvm/include/x86/processor.h
index 5898a277a2e0..79c4b502b00c 100644
--- a/tools/testing/selftests/kvm/include/x86/processor.h
+++ b/tools/testing/selftests/kvm/include/x86/processor.h
@@ -617,6 +617,11 @@ static inline void set_cr0(u64 val)
__asm__ __volatile__("mov %0, %%cr0" : : "r" (val) : "memory");
}
+static inline void lmsw(u8 val)
+{
+ __asm__ __volatile__("lmsw %w0" : : "r" (val) : "memory");
+}
+
static inline u64 get_cr2(void)
{
u64 cr2;
diff --git a/tools/testing/selftests/kvm/x86/nested_exceptions_test.c b/tools/testing/selftests/kvm/x86/nested_exceptions_test.c
index f47432526ea1..e83c1f41703d 100644
--- a/tools/testing/selftests/kvm/x86/nested_exceptions_test.c
+++ b/tools/testing/selftests/kvm/x86/nested_exceptions_test.c
@@ -109,6 +109,51 @@ static void l1_svm_code(struct svm_test_data *svm)
GUEST_DONE();
}
+static u64 l2_cr0;
+
+static void l2_vmx_cr0_test(bool use_lmsw)
+{
+ /*
+ * DANGER!!! DO NOT add anything that might cause the compiler to emit
+ * a "string" operation, e.g. memset(). Compilers often generate SIMD
+ * instructions for memset() and friends, and so will hit an #NM due to
+ * CR0.TS being set in hardware. Note, all ucalls trigger memset()!
+ */
+ u64 cr0 = (get_cr0() ^ X86_CR0_MP);
+
+ if (use_lmsw)
+ lmsw(cr0);
+ else
+ set_cr0(cr0);
+
+ /*
+ * Snapshot CR0 and let L1 check the value; as above, attempting to use
+ * GUEST_ASSERT_EQ() => ucall will likely trigger a premature #NM and
+ * result in a false pass (L1 will see the #NM and think all is well).
+ */
+ l2_cr0 = get_cr0();
+
+ /* FNOP should #NM as CR0.TS is set vmcs12.CR0 and thus vmcs02. */
+ asm volatile("fnop" ::: "memory");
+
+ /*
+ * Signal failure with a #UD, again to avoid ucall/memset(). If FNOP
+ * somehow succeeds but then #NM is triggered by a SIMD instruction in
+ * memset(), the test will get a false pass.
+ */
+ asm volatile("ud2");
+}
+
+static void l2_mov_cr0_test(void)
+{
+ l2_vmx_cr0_test(false);
+}
+
+static void l2_lmsw_test(void)
+{
+ l2_vmx_cr0_test(true);
+}
+
static void vmx_run_l2(void *l2_code, int vector, u32 error_code)
{
vmwrite(GUEST_RIP, (u64)l2_code);
@@ -129,6 +174,8 @@ static void vmx_run_l2(void *l2_code, int vector, u32 error_code)
static void l1_vmx_code(struct vmx_pages *vmx)
{
+ unsigned long cr0;
+
prepare_for_vmx_operation(vmx);
load_vmcs(vmx);
@@ -153,6 +200,32 @@ static void l1_vmx_code(struct vmx_pages *vmx)
vmx_run_l2(l2_ss_injected_tf_test, FAKE_TRIPLE_FAULT_VECTOR, 0);
GUEST_ASSERT_EQ(vmread(VM_EXIT_REASON), EXIT_REASON_TRIPLE_FAULT);
+ /*
+ * Validate KVM's CR0 mask+shadow emulation, for both MOV CR and LMSW.
+ * Set TS and NE in the value loaded in hardware, to verify that doing
+ * e.g. FNOP generates an #NM in L2, but clear the bits in L2's read
+ * shadow and verify that L2 always sees '0' and can't set those bits.
+ */
+ cr0 = vmread(GUEST_CR0) | X86_CR0_TS | X86_CR0_NE;
+ vmwrite(CR0_GUEST_HOST_MASK, X86_CR0_TS | X86_CR0_NE | X86_CR0_PE);
+ vmwrite(CR0_READ_SHADOW, cr0 & ~(X86_CR0_TS | X86_CR0_NE));
+ vmwrite(GUEST_CR0, cr0);
+ vmwrite(EXCEPTION_BITMAP, BIT_ULL(NM_VECTOR));
+
+ vmx_run_l2(l2_mov_cr0_test, NM_VECTOR, 0);
+ GUEST_ASSERT_EQ(l2_cr0, (cr0 & ~(X86_CR0_TS | X86_CR0_NE)) ^ X86_CR0_MP);
+ GUEST_ASSERT_EQ(vmread(GUEST_CR0), cr0 ^ X86_CR0_MP);
+
+ /*
+ * LMSW preserves CR0.PE *if* CR0.PE=1. Clear the bit in the shadow to
+ * validate that KVM preserves CR0.PE in "hardware" even if L2 attempts
+ * to clear CR0.PE via LMSW.
+ */
+ vmwrite(CR0_READ_SHADOW, cr0 & ~(X86_CR0_TS | X86_CR0_NE | X86_CR0_PE));
+ vmx_run_l2(l2_lmsw_test, NM_VECTOR, 0);
+ GUEST_ASSERT_EQ(l2_cr0, cr0 & ~(X86_CR0_TS | X86_CR0_NE | X86_CR0_PE));
+ GUEST_ASSERT_EQ(vmread(GUEST_CR0), cr0);
+
GUEST_DONE();
}
--
2.56.0.385.gac671c819a-goog
prev parent reply other threads:[~2026-10-30 21:37 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-30 21:37 [PATCH v3 0/5] KVM: nVMX: Honor L1-owned CR0 bits for LMSW Sean Christopherson
2026-10-30 21:37 ` [PATCH v3 1/5] KVM: nVMX: Preserve L1-owned bits in CR{0,4} read shadows Sean Christopherson
2026-10-30 21:37 ` [PATCH v3 2/5] KVM: nVMX: Honor L1-owned CR0 bits when emulating intercepted LMSW for L2 Sean Christopherson
2026-10-30 21:37 ` [PATCH v3 3/5] KVM: nVMX: Synthesize #GP if L2 writes a "bad" CR0 value via LMSW Sean Christopherson
2026-10-30 21:37 ` [PATCH v3 4/5] KVM: nVMX: Take full ownership of the post-VMXON CR{0,4} must-be-1 bits Sean Christopherson
2026-10-30 21:37 ` Sean Christopherson [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261030213747.2611815-6-seanjc@google.com \
--to=seanjc@google.com \
--cc=kvm@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=ma.mandourr@gmail.com \
--cc=pbonzini@redhat.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®