* [PATCH 0/2] KVM: nVMX: Don't let L2's LMSW modify CR0 bits owned by L1 @ 2026-10-29 23:50 ma.mandourr 2026-10-29 23:50 ` [PATCH 1/2] " ma.mandourr 2026-10-29 23:50 ` [PATCH 2/2] KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone ma.mandourr 0 siblings, 2 replies; 4+ messages in thread From: ma.mandourr @ 2026-10-29 23:50 UTC (permalink / raw) To: Sean Christopherson, Paolo Bonzini; +Cc: kvm, linux-kernel, Mahmoud Abumandour From: Mahmoud Abumandour <ma.mandourr@gmail.com> When L2's LMSW exits to L0 but not to L1, KVM emulates it with kvm_lmsw(), which ignores vmcs12's CR0 guest/host mask, so L2 can change CR0 bits that L1 owns. Mahmoud Abumandour (2): KVM: nVMX: Don't let L2's LMSW modify CR0 bits owned by L1 KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone arch/x86/kvm/vmx/vmx.c | 15 ++++++++ .../kvm/x86/nested_exceptions_test.c | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+) base-commit: dea4f6380f4ec96e8f3b0aaf2343764bd0d25ef4 -- 2.25.1 ^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH 1/2] KVM: nVMX: Don't let L2's LMSW modify CR0 bits owned by L1 2026-10-29 23:50 [PATCH 0/2] KVM: nVMX: Don't let L2's LMSW modify CR0 bits owned by L1 ma.mandourr @ 2026-10-29 23:50 ` ma.mandourr 2026-10-29 23:50 ` [PATCH 2/2] KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone ma.mandourr 1 sibling, 0 replies; 4+ messages in thread From: ma.mandourr @ 2026-10-29 23:50 UTC (permalink / raw) To: Sean Christopherson, Paolo Bonzini; +Cc: kvm, linux-kernel, Mahmoud Abumandour From: Mahmoud Abumandour <ma.mandourr@gmail.com> When L2's LMSW exits to L0 but not to L1, emulate it through handle_set_cr0(), as is done for MOV to CR0. kvm_lmsw() ignores vmcs12's CR0 guest/host mask, so L2 could change CR0 bits that L1 owns, e.g. clear TS, and read their effective values instead of the read shadow. Fixes: eeadf9e7558c ("KVM: nVMX: Handling of CR0 and CR4 modifying instructions") Signed-off-by: Mahmoud Abumandour <ma.mandourr@gmail.com> --- arch/x86/kvm/vmx/vmx.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/arch/x86/kvm/vmx/vmx.c b/arch/x86/kvm/vmx/vmx.c index b341af4c60..3633496299 100644 --- a/arch/x86/kvm/vmx/vmx.c +++ b/arch/x86/kvm/vmx/vmx.c @@ -5748,6 +5748,21 @@ static int handle_cr(struct kvm_vcpu *vcpu) case 3: /* lmsw */ val = (exit_qualification >> LMSW_SOURCE_DATA_SHIFT) & 0x0f; trace_kvm_cr_write(0, (kvm_read_cr0_bits(vcpu, ~0xful) | val)); + if (is_guest_mode(vcpu)) { + struct vmcs12 *vmcs12 = get_vmcs12(vcpu); + unsigned long mask = vmcs12->cr0_guest_host_mask; + unsigned long cr0; + + /* + * LMSW leaves the bits L1 owns unmodified, as does a MOV + * to CR0 that writes their read shadow values. Emulate + * it as that MOV: L2's view of CR0 with LMSW's bits. + */ + cr0 = (kvm_read_cr0(vcpu) & ~mask) | + (vmcs12->cr0_read_shadow & mask); + err = handle_set_cr0(vcpu, (cr0 & ~0x0eul) | val); + return kvm_complete_insn_gp(vcpu, err); + } kvm_lmsw(vcpu, val); return kvm_skip_emulated_instruction(vcpu); -- 2.25.1 ^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH 2/2] KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone 2026-10-29 23:50 [PATCH 0/2] KVM: nVMX: Don't let L2's LMSW modify CR0 bits owned by L1 ma.mandourr 2026-10-29 23:50 ` [PATCH 1/2] " ma.mandourr @ 2026-10-29 23:50 ` ma.mandourr 2026-10-30 0:04 ` sashiko-bot 1 sibling, 1 reply; 4+ messages in thread From: ma.mandourr @ 2026-10-29 23:50 UTC (permalink / raw) To: Sean Christopherson, Paolo Bonzini; +Cc: kvm, linux-kernel, Mahmoud Abumandour From: Mahmoud Abumandour <ma.mandourr@gmail.com> Verify that an LMSW in L2 that exits to L0 only leaves an L1-owned CR0.TS set, and that L2 reads an L1-owned CR0.NE from the read shadow. Assisted-by: LLM Signed-off-by: Mahmoud Abumandour <ma.mandourr@gmail.com> --- .../kvm/x86/nested_exceptions_test.c | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tools/testing/selftests/kvm/x86/nested_exceptions_test.c b/tools/testing/selftests/kvm/x86/nested_exceptions_test.c index f47432526e..7113018ee6 100644 --- a/tools/testing/selftests/kvm/x86/nested_exceptions_test.c +++ b/tools/testing/selftests/kvm/x86/nested_exceptions_test.c @@ -109,6 +109,40 @@ static void l1_svm_code(struct svm_test_data *svm) GUEST_DONE(); } +static u64 l2_cr0; + +static void l2_lmsw_test(void) +{ + /* Toggle CR0.MP, which KVM intercepts and L1 doesn't. */ + asm volatile("lmsw %w0" : : "r" (get_cr0() ^ X86_CR0_MP)); + l2_cr0 = get_cr0(); + asm volatile("fnop"); + GUEST_FAIL("L2's FNOP didn't take #NM"); +} + +/* + * L1 owns CR0.TS (set) and CR0.NE, both with a read shadow of 0. L2's LMSW + * must leave both alone: FNOP takes #NM and L2 reads NE as 0. + */ +static void vmx_run_l2_lmsw(void) +{ + u64 cr0 = vmread(GUEST_CR0); + + vmwrite(CR0_GUEST_HOST_MASK, X86_CR0_TS | X86_CR0_NE); + vmwrite(CR0_READ_SHADOW, cr0 & ~(X86_CR0_TS | X86_CR0_NE)); + vmwrite(GUEST_CR0, cr0 | X86_CR0_TS); + vmwrite(EXCEPTION_BITMAP, BIT(NM_VECTOR)); + vmwrite(GUEST_RIP, (u64)l2_lmsw_test); + vmresume(); + + GUEST_ASSERT_EQ(vmread(VM_EXIT_REASON), EXIT_REASON_EXCEPTION_NMI); + GUEST_ASSERT_EQ(vmread(VM_EXIT_INTR_INFO) & 0xff, NM_VECTOR); + GUEST_ASSERT(!(l2_cr0 & X86_CR0_NE)); + + vmwrite(CR0_GUEST_HOST_MASK, 0); + vmwrite(GUEST_CR0, cr0); +} + static void vmx_run_l2(void *l2_code, int vector, u32 error_code) { vmwrite(GUEST_RIP, (u64)l2_code); @@ -149,6 +183,8 @@ static void l1_vmx_code(struct vmx_pages *vmx) vmwrite(EXCEPTION_BITMAP, INTERCEPT_SS_DF); vmx_run_l2(l2_ss_injected_df_test, DF_VECTOR, DF_ERROR_CODE); + vmx_run_l2_lmsw(); + vmwrite(EXCEPTION_BITMAP, INTERCEPT_SS); vmx_run_l2(l2_ss_injected_tf_test, FAKE_TRIPLE_FAULT_VECTOR, 0); GUEST_ASSERT_EQ(vmread(VM_EXIT_REASON), EXIT_REASON_TRIPLE_FAULT); -- 2.25.1 ^ permalink raw reply related [flat|nested] 4+ messages in thread
* Re: [PATCH 2/2] KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone 2026-10-29 23:50 ` [PATCH 2/2] KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone ma.mandourr @ 2026-10-30 0:04 ` sashiko-bot 0 siblings, 0 replies; 4+ messages in thread From: sashiko-bot @ 2026-10-30 0:04 UTC (permalink / raw) To: ma.mandourr; +Cc: kvm Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] KVM: selftests: unordered l2_cr0 store and zero check in l2_lmsw_test() -- commit 81df08b65f8d95b00451c4c4a33f93d0001cf528 Author: Mahmoud Abumandour <ma.mandourr@gmail.com> KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone This commit adds a nested VMX test case to nested_exceptions_test to verify that an LMSW in L2 that exits to L0 preserves L1-owned CR0.TS and reads an L1-owned CR0.NE from the CR0 read shadow. > diff --git a/tools/testing/selftests/kvm/x86/nested_exceptions_test.c b/tools/testing/selftests/kvm/x86/nested_exceptions_test.c > index f47432526ea16..7113018ee62a9 100644 > --- a/tools/testing/selftests/kvm/x86/nested_exceptions_test.c > +++ b/tools/testing/selftests/kvm/x86/nested_exceptions_test.c > @@ -109,6 +109,40 @@ static void l1_svm_code(struct svm_test_data *svm) > GUEST_DONE(); > } > > +static u64 l2_cr0; > + > +static void l2_lmsw_test(void) > +{ > + /* Toggle CR0.MP, which KVM intercepts and L1 doesn't. */ > + asm volatile("lmsw %w0" : : "r" (get_cr0() ^ X86_CR0_MP)); > + l2_cr0 = get_cr0(); > + asm volatile("fnop"); > + GUEST_FAIL("L2's FNOP didn't take #NM"); > +} > + > +/* > + * L1 owns CR0.TS (set) and CR0.NE, both with a read shadow of 0. L2's LMSW > + * must leave both alone: FNOP takes #NM and L2 reads NE as 0. > + */ > +static void vmx_run_l2_lmsw(void) > +{ > + u64 cr0 = vmread(GUEST_CR0); > + > + vmwrite(CR0_GUEST_HOST_MASK, X86_CR0_TS | X86_CR0_NE); > + vmwrite(CR0_READ_SHADOW, cr0 & ~(X86_CR0_TS | X86_CR0_NE)); > + vmwrite(GUEST_CR0, cr0 | X86_CR0_TS); > + vmwrite(EXCEPTION_BITMAP, BIT(NM_VECTOR)); > + vmwrite(GUEST_RIP, (u64)l2_lmsw_test); > + vmresume(); > + > + GUEST_ASSERT_EQ(vmread(VM_EXIT_REASON), EXIT_REASON_EXCEPTION_NMI); > + GUEST_ASSERT_EQ(vmread(VM_EXIT_INTR_INFO) & 0xff, NM_VECTOR); > + GUEST_ASSERT(!(l2_cr0 & X86_CR0_NE)); [Severity: Medium] Can the store to l2_cr0 in l2_lmsw_test() be reordered past the fnop instruction, causing the assertion in vmx_run_l2_lmsw() to pass vacuously? In tools/testing/selftests/kvm/include/x86/processor.h:get_cr0(), the inline assembly reads CR0 into a register, and l2_lmsw_test() then stores that register to static u64 l2_cr0, which is zero-initialized in .bss and not marked volatile (nor written via WRITE_ONCE()). Because asm volatile("fnop") does not have a memory clobber, the compiler is only required to order the volatile asm inside get_cr0() before fnop, while the plain C store to l2_cr0 can be scheduled after fnop (before the function call in GUEST_FAIL()). When fnop triggers #NM and exits to L1, any store scheduled after fnop never executes, leaving l2_cr0 at 0. In vmx_run_l2_lmsw(), GUEST_ASSERT(!(l2_cr0 & X86_CR0_NE)) only tests that X86_CR0_NE is clear, which succeeds when l2_cr0 is still 0. Should l2_cr0 be written with WRITE_ONCE() (or given a memory clobber on fnop) and checked against the full expected CR0 value (cr0 & ~(X86_CR0_TS | X86_CR0_NE)) ^ X86_CR0_MP via GUEST_ASSERT_EQ()? -- Sashiko AI review · https://sashiko.dev/#/patchset/20261029235040.12013-1-ma.mandourr@gmail.com?part=2 ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-30 0:04 UTC | newest] Thread overview: 4+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-10-29 23:50 [PATCH 0/2] KVM: nVMX: Don't let L2's LMSW modify CR0 bits owned by L1 ma.mandourr 2026-10-29 23:50 ` [PATCH 1/2] " ma.mandourr 2026-10-29 23:50 ` [PATCH 2/2] KVM: selftests: Verify that L2's LMSW leaves L1-owned CR0 bits alone ma.mandourr 2026-10-30 0:04 ` sashiko-bot
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.