From: Sean Christopherson <seanjc@google.com>
To: sashiko-reviews@lists.linux.dev
Cc: kvm@vger.kernel.org
Subject: Re: [PATCH 3/4] KVM: x86: Recompute EFER.LMA instead of inheriting the current value on EFER writes
Date: Mon, 26 Oct 2026 10:51:59 -0700 [thread overview]
Message-ID: <at-TP7Il6rWcd5Xh@google.com> (raw)
In-Reply-To: <sashiko-outbox-182134@kernel.org>
On Thu, Oct 22, 2026, sashiko-bot@kernel.org wrote:
> > diff --git a/arch/x86/kvm/msrs.c b/arch/x86/kvm/msrs.c
> > index c3cb1575fe66e..41f3ec6eb2866 100644
> > --- a/arch/x86/kvm/msrs.c
> > +++ b/arch/x86/kvm/msrs.c
> > @@ -638,8 +638,18 @@ static int set_efer(struct kvm_vcpu *vcpu, u64 efer)
> > (vcpu->arch.efer & EFER_SVME) && !(efer & EFER_SVME))
> > return 1;
> >
> > - efer &= ~EFER_LMA;
> > - efer |= vcpu->arch.efer & EFER_LMA;
> > + if ((efer & EFER_LME) && is_paging(vcpu) && is_pae(vcpu))
> > + efer |= EFER_LMA;
> > + else
> > + efer &= ~EFER_LMA;
> > +
> > + /*
> > + * Toggling EFER.LMA on a bare write to EFER should be impossible, as
> > + * it means KVM allowed architecturally invalid state at some point in
> > + * the past, i.e. allowed EFER.LMA=1 && (!CR0.PG || !CR4.PAE).
> > + */
> > + if (WARN_ON_ONCE((efer & EFER_LMA) != (vcpu->arch.efer & EFER_LMA)))
> > + return 1;
>
> [Severity: High]
> Can a guest or host userspace trigger this WARN_ON_ONCE() and get a spurious
> #GP (or failed RSM / KVM_SET_MSRS) via SVM paths that still leave EFER_LMA
> in vcpu->arch.efer out of sync with (EFER_LME && CR0.PG && CR4.PAE)?
LOL, followed by a freaking disseration.
> Earlier in arch/x86/kvm/msrs.c:set_efer(), KVM rejects changing EFER_LME
> while paging is enabled:
>
> arch/x86/kvm/msrs.c:set_efer() {
> ...
> if (is_paging(vcpu) && (vcpu->arch.efer & EFER_LME) != (efer & EFER_LME))
> return 1;
> ...
> }
>
> Because of that check, the recomputed EFER_LMA in set_efer() is always equal
> to ((vcpu->arch.efer & EFER_LME) && is_paging(vcpu) && is_pae(vcpu)). The new
> WARN_ON_ONCE() therefore triggers whenever a vCPU enters set_efer() with
> vcpu->arch.efer & EFER_LMA out of sync with those bits.
>
> There appear to be three SVM paths where that still happens:
>
> 1. Nested SVM VMRUN and RSM into L2:
>
> Architecturally on SVM, VMRUN ignores EFER.LMA in the VMCB save area and sets
> EFER.LMA from (EFER.LME && CR0.PG) in hardware, so nested_vmcb_check_save()
> does not require save->efer & EFER_LMA to match (EFER_LME && CR0.PG):
>
> arch/x86/kvm/svm/nested.c:nested_vmcb_check_save() {
> ...
> /*
> * These checks are also performed by KVM_SET_SREGS,
> * except that EFER.LMA is not checked by SVM against
> * CR0.PG && EFER.LME.
> */
> if ((save->efer & EFER_LME) && (save->cr0 & X86_CR0_PG)) {
> if (CC(!(save->cr4 & X86_CR4_PAE)) ||
> CC(!(save->cr0 & X86_CR0_PE)) ||
> CC(!kvm_vcpu_is_legal_cr3(vcpu, save->cr3)))
> return false;
> ...
> }
>
> However, nested_vmcb02_prepare_save() only clears EFER_LMA when EFER_LME is
> 0, and relies on svm_set_cr0() to update EFER_LMA:
>
> arch/x86/kvm/svm/nested.c:nested_vmcb02_prepare_save() {
> ...
> /* SVM ignores EFER.LMA if EFER.LME=0 (instead of failing VMRUN). */
> if (!(svm->nested.save.efer & EFER_LME))
> svm->nested.save.efer &= ~EFER_LMA;
>
> svm_set_efer(vcpu, svm->nested.save.efer);
>
> svm_set_cr0(vcpu, svm->nested.save.cr0);
> svm_set_cr4(vcpu, svm->nested.save.cr4);
> ...
> }
>
> And svm_set_cr0() only updates EFER_LMA in vcpu->arch.efer when CR0.PG
> transitions relative to the previous vcpu->arch.cr0:
>
> arch/x86/kvm/svm/svm.c:svm_set_cr0() {
> ...
> if (vcpu->arch.efer & EFER_LME) {
> if (!is_paging(vcpu) && (cr0 & X86_CR0_PG)) {
> vcpu->arch.efer |= EFER_LMA;
> if (!vcpu->arch.guest_state_protected)
> svm->vmcb->save.efer |= EFER_LMA | EFER_LME;
> }
>
> if (is_paging(vcpu) && !(cr0 & X86_CR0_PG)) {
> vcpu->arch.efer &= ~EFER_LMA;
> if (!vcpu->arch.guest_state_protected)
> svm->vmcb->save.efer &= ~(EFER_LMA | EFER_LME);
> }
> }
> ...
> }
>
> Because L1 must have CR0.PG=1 to execute VMRUN, is_paging(vcpu) is already
> true when svm_set_cr0() runs during VMRUN. If vmcb12 has CR0.PG=1, CR4.PAE=1,
> EFER.LME=1, and EFER.LMA=0, svm_set_cr0() never sets EFER_LMA, leaving L2
> running in 64-bit mode with EFER_LMA=0 in vcpu->arch.efer. When L2 executes a
> valid WRMSR(MSR_EFER) (for example setting EFER_NX or EFER_SCE) or userspace
> calls KVM_SET_MSRS, set_efer() computes EFER_LMA=1, triggers WARN_ON_ONCE(),
> and injects a #GP into L2.
This looks like a KVM bug, i.e. we want the WARN and should fix the underlying
bug.
> Conversely, during RSM to L2 via emulator_leave_smm() -> svm_leave_smm() ->
> enter_svm_guest_mode() -> nested_vmcb02_prepare_save(), emulator_leave_smm()
> clears CR0.PG before calling svm_leave_smm(). If vmcb12 has EFER.LME=1,
> EFER.LMA=1, and CR0.PG=0, is_paging(vcpu) is already false when svm_set_cr0()
> runs, leaving EFER_LMA=1 with CR0.PG=0. When rsm_load_state_64() then calls
> __kvm_emulate_msr_write(vcpu, MSR_EFER, smstate->efer & ~EFER_LMA) ->
> set_efer(), set_efer() computes EFER_LMA=0, triggers WARN_ON_ONCE(), and
> aborts RSM with X86EMUL_UNHANDLEABLE.
Same here?
> 2. KVM_SET_NESTED_STATE and svm_leave_smm() followed by #VMEXIT:
>
> In arch/x86/kvm/svm/nested.c:svm_set_nested_state(), L1's saved host state in
> vmcb01->save is only checked for ((save->efer & EFER_LMA) &&
> !(save->efer & EFER_LME)) (and svm_leave_smm() copies vmcb01->save from
> guest MSR_VM_HSAVE_PA memory without checking EFER_LMA):
>
> arch/x86/kvm/svm/nested.c:svm_set_nested_state() {
> ...
> if (!(save->cr0 & X86_CR0_PG) ||
> !(save->cr0 & X86_CR0_PE) ||
> (save->rflags & X86_EFLAGS_VM) ||
> ((save->efer & EFER_LMA) && !(save->efer & EFER_LME)) ||
> !nested_vmcb_check_save(vcpu, &save_cached, false))
> goto out_free;
> ...
> }
>
> If vmcb01->save has EFER.LME=1, EFER.LMA=0, CR0.PG=1, and CR4.PAE=1, then on
> nested_svm_vmexit() from a paged L2 (where is_paging(vcpu) is already true),
> svm_set_efer() and svm_set_cr0() leave L1 with EFER_LMA=0 while EFER_LME=1,
> CR0.PG=1, and CR4.PAE=1, causing the next WRMSR(MSR_EFER) or KVM_SET_MSRS to
> trigger the WARN_ON_ONCE() in set_efer().
And here.
> 3. SEV-ES and SEV-SNP write traps:
>
> In arch/x86/kvm/svm/svm.c:cr_trap(), SVM_EXIT_CR0_WRITE_TRAP and
> SVM_EXIT_CR4_WRITE_TRAP call svm_set_cr0() and svm_set_cr4() directly without
> checking consistency between EFER.LMA, CR0.PG, and CR4.PAE:
>
> arch/x86/kvm/svm/svm.c:cr_trap() {
> ...
> case 0:
> old_value = kvm_read_cr0(vcpu);
> svm_set_cr0(vcpu, new_value);
>
> kvm_post_set_cr0(vcpu, old_value, new_value);
> break;
> case 4:
> old_value = kvm_read_cr4(vcpu);
> svm_set_cr4(vcpu, new_value);
>
> kvm_post_set_cr4(vcpu, old_value, new_value);
> break;
> ...
> }
>
> Because svm_set_cr0() sets EFER_LMA without checking is_pae(vcpu) (for
> example on an SEV-SNP AP created via SVM_VMGEXIT_AP_CREATE where
> sev_snp_init_protected_guest_state() calls kvm_vcpu_reset() and clears
> vcpu->arch.cr4 while the VMSA has CR4.PAE=1), or if a guest clears CR4.PAE
> observed via cr_trap() while EFER_LMA=1, a subsequent efer_trap() ->
> set_efer() sees EFER_LMA=1 with !is_pae(vcpu) and triggers the WARN_ON_ONCE()
> and #GP.
Ugh. I _want_ this to be a KVM bug, e.g. as is already pointed out by the comment:
* Clear EFER_LMSLE for a related reason: EFER writes are *trapped*,
* not intercepted, i.e. hardware has already committed the write by
* the time KVM gains control, and the trap is enabled if and only if
* the guest is SEV-ES, whose EFER lives in the encrypted VMSA and so
* can't be fixed up by KVM. Rejecting EFER.LMSLE=1 would inject a #GP
* *and* leave EFER.LMSLE set in the guest, which is strictly worse
* than honoring a write that hardware itself allowed.
"rejecting" a value and injecting a #GP is completely pointless. The guest already
has the "bad" value in EFER, and a misbehaving guest can simply eat the #GP.
But the issue is really just that KVM simply doesn't have visibility into EFER.LMA
being set. Heh, which technically makes this a bug fix, since IIUC, the current
code will never set EFER.LMA in this case?
I can't think of any way to do the right thing other than to rely on hardware to
not screw up? E.g.
diff --git a/arch/x86/kvm/msrs.c b/arch/x86/kvm/msrs.c
index 41f3ec6eb286..3a6a0ebf1932 100644
--- a/arch/x86/kvm/msrs.c
+++ b/arch/x86/kvm/msrs.c
@@ -648,7 +648,8 @@ static int set_efer(struct kvm_vcpu *vcpu, u64 efer)
* it means KVM allowed architecturally invalid state at some point in
* the past, i.e. allowed EFER.LMA=1 && (!CR0.PG || !CR4.PAE).
*/
- if (WARN_ON_ONCE((efer & EFER_LMA) != (vcpu->arch.efer & EFER_LMA)))
+ if (WARN_ON_ONCE((efer & EFER_LMA) != (vcpu->arch.efer & EFER_LMA) &&
+ !vcpu->kvm->arch.has_protected_state))
return 1;
r = kvm_x86_call(set_efer)(vcpu, efer);
Hmm, though all of these cases make me wonder if we'd better off with just a WARN,
i.e. no #GP, since the goal is to enforce a consistent model, which is what the
new code does.
/*
* Toggling EFER.LMA on a bare write to EFER should be impossible, as
* it means KVM allowed architecturally invalid state at some point in
* the past, i.e. allowed EFER.LMA=1 && (!CR0.PG || !CR4.PAE).
*/
WARN_ON_ONCE((efer & EFER_LMA) != (vcpu->arch.efer & EFER_LMA) &&
!vcpu->kvm->arch.has_protected_state);
next prev parent reply other threads:[~2026-10-26 17:52 UTC|newest]
Thread overview: 14+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 22:30 [PATCH 0/4] KVM: x86: More EFER.LMA vs. CR4.PAE fixes Sean Christopherson
2026-10-22 22:30 ` [PATCH 1/4] Revert "KVM: x86: Skip EFER vs. guest CPUID checks for host-initiated writes" Sean Christopherson
2026-10-22 22:38 ` sashiko-bot
2026-10-22 22:30 ` [PATCH 2/4] KVM: nSVM: Synthesize #GP instead of SHUTDOWN if L2 attempts to clear EFER.SVME Sean Christopherson
2026-10-22 22:41 ` sashiko-bot
2026-10-26 18:17 ` Sean Christopherson
2026-10-22 22:30 ` [PATCH 3/4] KVM: x86: Recompute EFER.LMA instead of inheriting the current value on EFER writes Sean Christopherson
2026-10-22 22:48 ` sashiko-bot
2026-10-26 17:51 ` Sean Christopherson [this message]
2026-10-26 18:09 ` Sean Christopherson
2026-10-23 15:04 ` Yosry Ahmed
2026-10-26 17:37 ` Sean Christopherson
2026-10-22 22:30 ` [PATCH 4/4] KVM: selftests: Update nSVM EFER test to expect #GP when clearing SVME in L2 Sean Christopherson
2026-10-22 22:51 ` sashiko-bot
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=at-TP7Il6rWcd5Xh@google.com \
--to=seanjc@google.com \
--cc=kvm@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/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 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.