From: Sean Christopherson <seanjc@google.com>
To: Yosry Ahmed <yosry.ahmed@linux.dev>
Cc: Paolo Bonzini <pbonzini@redhat.com>,
kvm@vger.kernel.org, linux-kernel@vger.kernel.org,
stable@vger.kernel.org
Subject: Re: [PATCH 1/4] KVM: nSVM: Sync next_rip to cached vmcb12 after VMRUN of L2
Date: Tue, 10 Feb 2026 16:09:11 -0800 [thread overview]
Message-ID: <aYvIpwjsJ50Ns4ho@google.com> (raw)
In-Reply-To: <ck57mmdt5phh64cadoqxylw5q2b72ffmabmlzmpphaf27lbtxw@4kscovf6ahve>
On Tue, Feb 10, 2026, Yosry Ahmed wrote:
> On Tue, Feb 10, 2026 at 11:20:19AM -0800, Sean Christopherson wrote:
> > > > Actually, I take that back, I have no idea how this code works. How does e.g.
> > > > exit_info_1 not get clobbered on save/restore?
> > >
> > > I *think* KVM always sets the error_code and exit_info_* fields before
> > > synthesizing a #VMEXIT to L1, usually right before calling
> > > nested_svm_vmeit(), so no chance for save/restore in between.
> >
> > Ugh, right, KVM generally doesn't recognize signals until after invoking the exit
> > handler. Actually, even that isn't the key, it's that this flaw only affects
> > "in/out" fields, as you note above. Heh, and even that probably isn't entirely
> > precise, as it's really "in/out fields that KVM consumes while running L2 are
> > buggy". E.g. in this case, nested_vmcb02_prepare_control() pulls next_rip for
> > vmcb02 from the cache.
> >
> > if (guest_cpu_cap_has(vcpu, X86_FEATURE_NRIPS))
> > vmcb02->control.next_rip = svm->nested.ctl.next_rip;
> > else if (boot_cpu_has(X86_FEATURE_NRIPS))
> > vmcb02->control.next_rip = vmcb12_rip;
>
> Hmm I thin a pure "out" field would be affected if it's consumed by KVM
> later on, after save+restore is possible, right?
True.
> Do you mean that it just happens that the currently affected fields are
> "in/out" fields? Or is there a reason why pure "out" fields cannot be
> affected? AFAICT, these fields are lost after save+restore.
I was essentially assuming KVM wouldn't ever consume pure out fields from the
cache.
> > > > In other words, AFAICT, nested.ctl.int_ctl is special in that KVM needs it to be
> > > > up-to-date at all times, *and* it needs to copied back to vmcb12 (or userspace).
> > >
> > > Hmm actually looking at nested.ctl.int_ctl, I don't think it's that special.
> > > Most KVM usages are checking "in" bits, i.e. whether some features (e.g.
> > > vGIF) are enabled or not.
> > >
> > > The "out" bits seem to only be consumed by svm_clear_vintr(), and I
> > > think this can be worked around.
> >
> > OMG that code makes my head hurt. Isn't that code just this?
> >
> > /*
> > * Drop int_ctl fields related to VINTR injection. If L2 is active,
> > * restore the virtual IRQ flag and its vector from vmcb12 now that KVM
> > * is done usurping virtual IRQs for its own purposes.
> > */
> > svm->vmcb01.ptr->control.int_ctl &= ~V_IRQ_INJECTION_BITS_MASK;
> >
> > if (is_guest_mode(&svm->vcpu)) {
> > svm->vmcb->control.int_ctl = (svm->vmcb->control.int_ctl & ~V_IRQ_MASK) |
> > (svm->nested.ctl.int_ctl & V_IRQ_MASK);
>
> Also V_INTR_PRIO_MASK, I think?
Ugh, yes. And I think V_IGN_TPR as well? I can't tell if that's a bug or not.
It looks like a bug. AFAICT, svm_set_vintr() uses whatever V_IGN_TPR_MASK value
happens to be in vmcb02. I don't see how that can be desirable.
> But otherwise yeah I think that's what the function is doing
> more-or-less.
>
> > svm->vmcb->control.int_vector = svm->nested.ctl.int_vector;
> > } else {
> > WARN_ON_ONCE(svm->vmcb != svm->vmcb01.ptr);
> > }
> >
> > svm_clr_intercept(svm, INTERCEPT_VINTR);
> > vmcb_mark_dirty(svm->vmcb, VMCB_INTR);
> >
> > > So maybe we don't really need to keep it up-to-date in the cache at all
> > > times.
> >
> > Yeah, IMO that approach is unnecessarily convoluted. The actual logic isn't all
> > that complex, all of the complexity comes from juggling state between the cache
> > and vmcb02 just so that the cache can be authoritative. Given that we failed
> > miserably in actually making the cache authoritative, e.g. see the nested #VMEXIT
> > flow, I think we should kill the entire concept and instead maintain an *exact*
>
> When you say *exact* snapshot, do you mean move all the sanitizing logic
> recently introduced in __nested_copy_vmcb_control_to_cache() (by Kevin
> and myself) to sanitize vmcb02 instead?
Oh, no, not _that_ exact. More "unchanged after emulated VMRUN"
> That would be annoying. For example, for the intercepts, sanitizing
> vmcb02 (but not vmcb12) means that we need to also add checks in the
> exit path (i.e. in nested_svm_intercept() or even
> vmcb12_is_intercept()), as vmcb12 could have illegal intercepts.
>
> > snapshot of vmcb12's controls, and then make vmcb02 authoritative. We'll still
> > need the logic in nested_sync_control_from_vmcb02() to updated int_ctl on save
> > or #VMEXIT if KVM is still intercepting VINTR for its own purposes, but at least
> > the code will be contained.
>
> Yeah. We should leave it out of the vmcb12 cache though.
+1
> > Then to make it all but impossible to re-introduce this mess, do something like
> > this so that someone would have to go way out of their way to try and modify the
> > cache.
> >
> > diff --git a/arch/x86/kvm/svm/svm.h b/arch/x86/kvm/svm/svm.h
> > index ebd7b36b1ceb..2de6305be9ce 100644
> > --- a/arch/x86/kvm/svm/svm.h
> > +++ b/arch/x86/kvm/svm/svm.h
> > @@ -199,14 +199,13 @@ struct svm_nested_state {
> > * we cannot inject a nested vmexit yet. */
> > bool nested_run_pending;
> >
> > - /* cache for control fields of the guest */
> > - struct vmcb_ctrl_area_cached ctl;
> > -
> > /*
> > - * Note: this struct is not kept up-to-date while L2 runs; it is only
> > - * valid within nested_svm_vmrun.
> > + * An opaque, read-only cache of vmcb12 controls, used to query L1's
> > + * controls while running L2, e.g. to route intercepts appropriately.
> > + * All reads are routed through accessors to make it all but impossible
> > + * for KVM to clobber its snapshot of vmcb12.
> > */
> > - struct vmcb_save_area_cached save;
>
> Is dropping the cached save area intentional?
Yes, I think we should try to drop it. Assuming the comment is correct and it
really only
> We can drop it and make it a local vaiable in nested_svm_vmrun(), and
> plumb it all the way down. But it could be too big for the stack.
It's 48 bytes, there's no way that's too big.
> Allocating it every time isn't nice either.
> Do you mean to also make it opaque?
I'd prefer to drop it.
> > + u8 __vmcb12_ctrl[sizeof(struct vmcb_ctrl_area_cached)];
>
> We have a lot of accesses to svm->nested.ctl, so we'll need a lot of
> clutter to cast the field in all of these places.
>
> Maybe we add a read-only accessor that returns a pointer to a constant
> struct?
That's what I said :-D
* All reads are routed through accessors to make it all but impossible
* for KVM to clobber its snapshot of vmcb12.
There might be a lot of helpers, but I bet it's less than nVMX has for vmcs12.
> > bool initialized;
> >
> > > > Part of me wants to remove these two fields entirely:
> > > >
> > > > /* cache for control fields of the guest */
> > > > struct vmcb_ctrl_area_cached ctl;
> > > >
> > > > /*
> > > > * Note: this struct is not kept up-to-date while L2 runs; it is only
> > > > * valid within nested_svm_vmrun.
> > > > */
> > > > struct vmcb_save_area_cached save;
> > > >
> > > > and instead use "full" caches only for the duration of nested_svm_vmrun(). Or
> > > > hell, just copy the entire vmcb12 and throw the cached structures in the garbage.
> > > > But that'll probably end in a game of whack-a-mole as things get moved back in.
> > >
> > > Yeah, KVM needs to keep some of the fields around :/
> >
> > For me, that's totally fine. As above, the problem I see is that there is no
> > single source of truth, i.e. that the authoritative state is spread across vmcb02
> > and the cache.
> >
> > > > So rather than do something totally drastic, I think we should kill
> > > > nested_copy_vmcb_cache_to_control() and replace it with a "save control" flow.
> > > > And then have it share code as much code as possible with nested_svm_vmexit(),
> > > > and fixup nested_svm_vmexit() to not pull from svm->nested.ctl unnecessarily.
> > > > Which, again AFICT, is pretty much limited to int_ctl: either vmcb02 is
> > > > authoritative, or KVM shouldn't be updating vmcb12, and so only the "save control"
> > > > for KVM_GET_NESTED_STATE needs to copy from the cache to the migrated vmcb12.
> > >
> > > I think this works if we draw a clear extinction between "in","out", and
> > > "in/out" fields, which is not great because some fields (like int_ctl)
> > > have different directions for different bits :/
> > >
> > > But if we do draw that distinction, and have helpers that copy fields
> > > based on direction, things become more intuitive:
> > >
> > > During nested VMRUN, we use the "in" and "in/out" fields from cached
> > > vmcb12 to construct vmcb02 through nested_vmcb02_prepare_control().
> > >
> > > During save, we save "in" fields from the cached vmcb12, "out" and
> > > "in/out" fields from vmcb02.
> > >
> > > During restore, we use the "in" and "in/out" fields from the restored
> > > payload to construct vmcb02 through nested_vmcb02_prepare_control(), AND
> > > update the "out" fields as well from the payload.
> >
> > Why the last part? If L2 is active, then the pure "out" fields are guaranteed
> > to be written on nested #VMEXIT. Anything else simply can't work.
>
> I think it just so happens that all pure "out" fields are consumed by
> KVM before save+restore is possible, but it is possible for an "out"
> field to be used by KVM at a later point, or copied from vmcb02 to
> vmcb12 during nested #VMEXIT (e.g. if KVM exits to L1 directly after
> save+restore, before running L2).
Ya.
> next_rip and int_state fall in this bucket, it just happens to also be
> an "in" field.
>
> For example, if support for decode assists is added, there will be cases
> where KVM just copies insn_bytes from vmcb02 to vmcb12 on nested
> #VMEXIT. If insn_bytes is lost on save+restore, and KVM immediately
> exits to L1 after restore, insn_bytes is lost.
>
> So we need to also save+restore pure "out" fields, which we do not do
> today.
Hmm, strictly speaking, no. We'd be fixing a bug that doesn't exist, yet. But
the word yet...
> > > During synthesized #VMEXIT, we save the "out" and "in/out" fields from
> > > vmcb02 (shared part with save/restore).
> >
> > Yeah, that all works. We could also treat save() as an extension of #VMEXIT, but
> > that could make KVM_GET_NESTED_STATE non-idempotent (which might already be the
> > case for VMX?). I.e. we could sync vmcb02 to vmcb12 (cache), and then copy that
> > to userspace.
>
> I think this will require adding more fields to the cache, but wait, we
> already have a lot of "out" fields there but I don't think they are
> being used at all..
>
> Anyway, this may make things simpler. Instead of pulling different
> fields from either cached vmcb12 and vmcb02, we always combine them
> first. I will keep that in mind.
>
> >
> > > The save/restore changes would need a flag to avoid restoring garbage
> > > from an older KVM.
> >
> > I don't follow. I was thinking we'd only change how KVM maintains authoritative
> > state while runnign L2, i.e. not make any changes (other than fixes) to the
> > serialized state for save/restore.
>
> I thought we're not currently saving "out" fields at all, but
> apparently we are, we just do not use them in svm_set_nested_state(). So
> we probably do not need a flag. Even if some fields are not currently
> copied, I assume KVM restoring garbage from an older KVM is no worse
> than having uninitialized garbage :)
Heh, yep.
> I think this will be annoying when new fields are added, like
> insn_bytes. Perhaps at some point we move to just serializing the entire
> combined vmcb02/vmcb12 control area and add a flag for that.
If we do it now, can we avoid the flag?
next prev parent reply other threads:[~2026-02-11 0:09 UTC|newest]
Thread overview: 18+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-10 0:54 [PATCH 0/4] KVM: nSVM: Fix save/restore of next_rip & int_state Yosry Ahmed
2026-02-10 0:54 ` [PATCH 1/4] KVM: nSVM: Sync next_rip to cached vmcb12 after VMRUN of L2 Yosry Ahmed
2026-02-10 1:49 ` Sean Christopherson
2026-02-10 16:25 ` Yosry Ahmed
2026-02-10 19:20 ` Sean Christopherson
2026-02-10 22:19 ` Yosry Ahmed
2026-02-11 0:09 ` Sean Christopherson [this message]
2026-02-11 0:27 ` Yosry Ahmed
2026-02-11 0:39 ` Sean Christopherson
2026-02-11 1:02 ` Yosry Ahmed
2026-02-21 0:03 ` Sean Christopherson
2026-02-21 9:11 ` Yosry Ahmed
2026-02-23 16:59 ` Sean Christopherson
2026-02-23 17:21 ` Yosry Ahmed
2026-02-23 20:23 ` Sean Christopherson
2026-02-10 0:54 ` [PATCH 2/4] KVM: nSVM: Sync int_state " Yosry Ahmed
2026-02-10 0:54 ` [PATCH 3/4] KVM: selftests: Extend state_test to check vGIF Yosry Ahmed
2026-02-10 0:54 ` [PATCH 4/4] KVM: selftests: Extend state_test to check next_rip Yosry Ahmed
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=aYvIpwjsJ50Ns4ho@google.com \
--to=seanjc@google.com \
--cc=kvm@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=pbonzini@redhat.com \
--cc=stable@vger.kernel.org \
--cc=yosry.ahmed@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.