[PATCH V2 3/4] KVM: VMX: Don't consult original exit qualification for nested EPT violation injection
From: Kevin Cheng
Date: Tue Feb 24 2026 - 02:21:15 EST
Remove the OR of EPT_VIOLATION_GVA_IS_VALID and
EPT_VIOLATION_GVA_TRANSLATED from the hardware exit qualification when
injecting a synthesized EPT violation to L1. The hardware exit
qualification reflects the original VM exit, which may not be an EPT
violation at all, e.g. if KVM is emulating an I/O instruction and the
memory operand's translation through L1's EPT fails. In that case, bits
7-8 of the exit qualification have completely different semantics (or
are simply zero), and OR'ing them into the injected EPT violation
corrupts the GVA_IS_VALID/GVA_TRANSLATED information.
Even when the original exit is an EPT violation, the hardware bits may
not match the current fault. For example, if an EPT violation happened
while walking L2's page tables, it's possible that the EPT violation
injected by KVM into L1 is for the final address translation, if L1
already had the mappings for L2's page tables in its EPTs but KVM did
not have shadow EPTs for them.
Populate EPT_VIOLATION_GVA_IS_VALID and EPT_VIOLATION_GVA_TRANSLATED
directly in the page table walker at the kvm_translate_gpa() failure
sites, mirroring the existing PFERR_GUEST_PAGE_MASK and
PFERR_GUEST_FINAL_MASK population for NPT.
Signed-off-by: Kevin Cheng <chengkev@xxxxxxxxxx>
---
arch/x86/kvm/mmu/paging_tmpl.h | 16 +++++++++++++++-
arch/x86/kvm/vmx/nested.c | 3 ---
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/arch/x86/kvm/mmu/paging_tmpl.h b/arch/x86/kvm/mmu/paging_tmpl.h
index f148c92b606ba..a084b5e50effc 100644
--- a/arch/x86/kvm/mmu/paging_tmpl.h
+++ b/arch/x86/kvm/mmu/paging_tmpl.h
@@ -386,8 +386,19 @@ static int FNAME(walk_addr_generic)(struct guest_walker *walker,
nested_access, &walker->fault);
if (unlikely(real_gpa == INVALID_GPA)) {
+ /*
+ * Unconditionally set the NPF error_code bits and
+ * EPT exit_qualification bits for nested page
+ * faults. The walker doesn't know whether L1 uses
+ * NPT or EPT, and each injection handler consumes
+ * only the field it cares about (error_code for
+ * NPF, exit_qualification for EPT violations), so
+ * setting both is harmless.
+ */
#if PTTYPE != PTTYPE_EPT
walker->fault.error_code |= PFERR_GUEST_PAGE_MASK;
+ walker->fault.exit_qualification |=
+ EPT_VIOLATION_GVA_IS_VALID;
#endif
return 0;
}
@@ -449,6 +460,9 @@ static int FNAME(walk_addr_generic)(struct guest_walker *walker,
if (real_gpa == INVALID_GPA) {
#if PTTYPE != PTTYPE_EPT
walker->fault.error_code |= PFERR_GUEST_FINAL_MASK;
+ walker->fault.exit_qualification |=
+ EPT_VIOLATION_GVA_IS_VALID |
+ EPT_VIOLATION_GVA_TRANSLATED;
#endif
return 0;
}
@@ -496,7 +510,7 @@ static int FNAME(walk_addr_generic)(struct guest_walker *walker,
* [2:0] - Derive from the access bits. The exit_qualification might be
* out of date if it is serving an EPT misconfiguration.
* [5:3] - Calculated by the page walk of the guest EPT page tables
- * [7:8] - Derived from [7:8] of real exit_qualification
+ * [7:8] - Set at the kvm_translate_gpa() call sites above
*
* The other bits are set to 0.
*/
diff --git a/arch/x86/kvm/vmx/nested.c b/arch/x86/kvm/vmx/nested.c
index 248635da67661..6a167b1d51595 100644
--- a/arch/x86/kvm/vmx/nested.c
+++ b/arch/x86/kvm/vmx/nested.c
@@ -444,9 +444,6 @@ static void nested_ept_inject_page_fault(struct kvm_vcpu *vcpu,
exit_qualification = 0;
} else {
exit_qualification = fault->exit_qualification;
- exit_qualification |= vmx_get_exit_qual(vcpu) &
- (EPT_VIOLATION_GVA_IS_VALID |
- EPT_VIOLATION_GVA_TRANSLATED);
vm_exit_reason = EXIT_REASON_EPT_VIOLATION;
}
--
2.53.0.414.gf7e9f6c205-goog