[PATCH] KVM: nVMX: Re-arm the vmcs12 pages request if mapping the pages fails

From: Jinwoo Lee

Date: Thu Aug 13 2026 - 00:39:56 EST


Re-arm KVM_REQ_GET_NESTED_STATE_PAGES when nested_get_vmcs12_pages()
fails, so that KVM retries the mapping on the next KVM_RUN instead of
resuming L2 with a stale vmcs02.

On failure KVM exits to userspace with KVM_EXIT_INTERNAL_ERROR but
leaves the vCPU in guest mode with vmcs02 loaded. The request has
already been consumed by kvm_check_request() in vcpu_enter_guest(), and
nothing re-arms it, so a subsequent KVM_RUN goes straight to VM-Enter.

vmcs02's APIC_ACCESS_ADDR, VIRTUAL_APIC_PAGE_ADDR and
POSTED_INTR_DESC_ADDR still hold the host physical addresses that were
mapped for the previous nested VM-Enter. Those pages have already been
unmapped and unpinned by nested_put_vmcs12_pages(), which runs after
vmx_switch_vmcs() to vmcs01 and therefore cannot clear the vmcs02
fields, and prepare_vmcs02_early() re-arms the controls that consume
them without rewriting the address fields. Hardware then accesses
pages that KVM no longer holds a reference to.

The SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES branch makes this worse by
returning before the CPU_BASED_TPR_SHADOW and posted interrupt
fallbacks, which would otherwise write INVALID_GPA to
VIRTUAL_APIC_PAGE_ADDR and clear PIN_BASED_POSTED_INTR.

Commit 671ddc700fd0 ("KVM: nVMX: Don't leak L1 MMIO regions to L2")
replaced the "clear the control" fallback with an error return. The
intent is right, but it left vmcs02 in a usable state. Re-arming the
request closes that; if the mapping keeps failing KVM keeps exiting to
userspace, which is noisy but safe.

Note this relies on KVM_REQ_GET_NESTED_STATE_PAGES being cleared on
nested VM-Exit, which "KVM: nVMX: Ensure KVM_REQ_GET_NESTED_STATE_PAGES
is cleared on VM-Exit" makes unconditional.

Fixes: 671ddc700fd0 ("KVM: nVMX: Don't leak L1 MMIO regions to L2")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Jinwoo Lee <rkskek9254@xxxxxxxxx>
---
Notes for reviewers, not intended for the commit log.

Affected versions: v5.4-rc5 (671ddc700fd0) through v7.2-rc6. Verified that
nested_get_vmcs12_pages() and the KVM_REQ_GET_NESTED_STATE_PAGES consumer in
vcpu_enter_guest() are unchanged in kvm-x86/next as of 2026-08-13.

Disclosure: this was found with AI-assisted code review, so per
Documentation/process/security-bugs.rst it is being reported publicly rather
than to security@xxxxxxxxxx.

What I verified empirically, on the RSM path with the load_pdptrs() abort:

- KVM consumes KVM_REQ_GET_NESTED_STATE_PAGES, the mapping fails, KVM_RUN
returns 0 with run->exit_reason left at KVM_EXIT_UNKNOWN, and the vCPU is
still in guest mode.
- The request is not re-armed, and the next KVM_RUN VM-Enters L2, which then
executes with vmcs02 still naming the previously mapped pages. Confirmed
deterministically (4/4) with a selftest, plus a control run showing that
the same sequence without the poison maps successfully.
- With the patch applied the code compiles clean, but I have not been able to
boot a patched kernel, so the fix itself is not runtime tested. The
selftest fails on an unpatched kernel as expected.

What I did not verify:

- The APIC-access branch end to end. That is the interesting one, because it
is reachable by L1 alone (point vmcs12->apic_access_addr at an unbacked
GPA) and it returns before the CPU_BASED_TPR_SHADOW and posted interrupt
fallbacks. This host does not expose
SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES or PIN_BASED_POSTED_INTR to L1, so
I could only reach the load_pdptrs() abort, which needs userspace to poison
the PDPTEs through a KVM_GUESTDBG_SINGLESTEP window and is therefore not
guest-triggerable on its own.
- Whether a stale page is actually reused by the host. The selftest keeps
every page allocated as its own guest RAM for the whole run.

On whether userspace resumes: QEMU's kvm_cpu_exec() treats
KVM_INTERNAL_ERROR_EMULATION as recoverable and returns EXCP_INTERRUPT when
kvm_arch_stop_on_emulation_error() is false, which for x86 is the case when the
guest is in protected mode at CPL 3 (target/i386/kvm/kvm.c). It does not
re-push nested state on that path. This is from reading qemu.git at
055952c0aa91; I have not run it.

A selftest is available. I have not included it here per the reproducer
guidance in security-bugs.rst; happy to post it if you want it.

arch/x86/kvm/vmx/nested.c | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/arch/x86/kvm/vmx/nested.c b/arch/x86/kvm/vmx/nested.c
index ddf6df7bee93..9e9bd6c541ba 100644
--- a/arch/x86/kvm/vmx/nested.c
+++ b/arch/x86/kvm/vmx/nested.c
@@ -3453,7 +3453,7 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)
* state which can lead to a load of wrong PDPTRs.
*/
if (CC(!load_pdptrs(vcpu, vcpu->arch.cr3)))
- return false;
+ goto fail;
}


@@ -3469,7 +3469,7 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)
vcpu->run->internal.suberror =
KVM_INTERNAL_ERROR_EMULATION;
vcpu->run->internal.ndata = 0;
- return false;
+ goto fail;
}
}

@@ -3525,6 +3525,20 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)
exec_controls_clearbit(vmx, CPU_BASED_USE_MSR_BITMAPS);

return true;
+
+fail:
+ /*
+ * Re-arm the request so that KVM retries the mapping instead of running
+ * L2 with a stale vmcs02. Bailing here leaves the vCPU in guest mode
+ * with vmcs02 loaded and its APIC-access, virtual-APIC and posted
+ * interrupt descriptor addresses still pointing at the host pages that
+ * were mapped for the *previous* nested VM-Enter, which have since been
+ * unmapped and unpinned by nested_put_vmcs12_pages(). KVM returns to
+ * userspace without leaving guest mode, so if userspace resumes the
+ * vCPU, VM-Enter succeeds and hardware accesses those stale HPAs.
+ */
+ kvm_make_request(KVM_REQ_GET_NESTED_STATE_PAGES, vcpu);
+ return false;
}

static bool vmx_get_nested_state_pages(struct kvm_vcpu *vcpu)
--
2.43.0