[PATCH v3 04/10] KVM: nVMX: Implement cache for L1 APIC pages

Fred Griffoul
2025-11-21T11:11:30Z

Replace kvm_host_map usage with gfn_to_pfn_cache for L1 APIC
virtualization pages (APIC access, virtual APIC, and posted interrupt
descriptor pages) to improve performance with unmanaged guest memory.

The conversion involves several changes:

- Page loading in nested_get_vmcs12_pages(): load vmcs02 fields with
pfncache PFNs after each cache has been checked and possibly activated
or refreshed, during OUTSIDE_GUEST_MODE vCPU mode.

- Invalidation window handling: since nested_get_vmcs12_pages() runs in
OUTSIDE_GUEST_MODE, there's a window where caches can be invalidated
by MMU notifications before entering IN_GUEST_MODE. implement
is_nested_state_invalid() callback to monitor cache validity between
OUTSIDE_GUEST_MODE and IN_GUEST_MODE transitions. This triggers
KVM_REQ_GET_NESTED_STATE_PAGES when needed.

- Cache access in event callbacks: the virtual APIC and posted interrupt
descriptor pages are accessed by KVM in has_events() and
check_events() nested_ops callbacks. These use the kernel HVA following
the pfncache pattern of check/refresh, with both callbacks able to sleep
if cache refresh is required.

This eliminates expensive memremap/memunmap cycles for each L2 VM
entry/exit, providing substantial performance improvements when using
unmanaged memory.

Signed-off-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
arch/x86/kvm/vmx/nested.c | 169 +++++++++++++++++++++++++++++---------
arch/x86/kvm/vmx/vmx.h | 8 +-
include/linux/kvm_host.h | 5 ++
3 files changed, 139 insertions(+), 43 deletions(-)

diff --git a/arch/x86/kvm/vmx/nested.c b/arch/x86/kvm/vmx/nested.c
index 627a6c24625d..1f58b380585b 100644
--- a/arch/x86/kvm/vmx/nested.c
+++ b/arch/x86/kvm/vmx/nested.c
@@ -329,8 +329,18 @@ static int nested_gpc_lock(struct gfn_to_pfn_cache *gpc, gpa_t gpa)
if (!kvm_gpc_check(gpc, PAGE_SIZE) || (gpc->gpa != gpa)) {
read_unlock(&gpc->lock);
err = kvm_gpc_activate(gpc, gpa, PAGE_SIZE);
- if (err)
+ if (err) {
+ /*
+ * Deactivate nested state caches to prevent
+ * kvm_gpc_invalid() from returning true in subsequent
+ * is_nested_state_invalid() calls. This prevents an
+ * infinite loop while entering guest mode.
+ */
+ if (gpc->vcpu)
+ kvm_gpc_deactivate(gpc);
+
return err;
+ }

goto retry;
}
@@ -343,14 +353,17 @@ static void nested_gpc_unlock(struct gfn_to_pfn_cache *gpc)
read_unlock(&gpc->lock);
}

-static void nested_put_vmcs12_pages(struct kvm_vcpu *vcpu)
+static int nested_gpc_hpa(struct gfn_to_pfn_cache *gpc, gpa_t gpa, hpa_t *hpa)
{
- struct vcpu_vmx *vmx = to_vmx(vcpu);
+ int err;
+
+ err = nested_gpc_lock(gpc, gpa);
+ if (err)
+ return err;

- kvm_vcpu_unmap(vcpu, &vmx->nested.apic_access_page_map);
- kvm_vcpu_unmap(vcpu, &vmx->nested.virtual_apic_map);
- kvm_vcpu_unmap(vcpu, &vmx->nested.pi_desc_map);
- vmx->nested.pi_desc = NULL;
+ *hpa = pfn_to_hpa(gpc->pfn);
+ nested_gpc_unlock(gpc);
+ return 0;
}

/*
@@ -373,6 +386,9 @@ static void free_nested(struct kvm_vcpu *vcpu)
vmx->nested.smm.vmxon = false;
vmx->nested.vmxon_ptr = INVALID_GPA;

+ kvm_gpc_deactivate(&vmx->nested.pi_desc_cache);
+ kvm_gpc_deactivate(&vmx->nested.virtual_apic_cache);
+ kvm_gpc_deactivate(&vmx->nested.apic_access_page_cache);
kvm_gpc_deactivate(&vmx->nested.msr_bitmap_cache);

free_vpid(vmx->nested.vpid02);
@@ -389,8 +405,6 @@ static void free_nested(struct kvm_vcpu *vcpu)
kfree(vmx->nested.cached_shadow_vmcs12);
vmx->nested.cached_shadow_vmcs12 = NULL;

- nested_put_vmcs12_pages(vcpu);
-
kvm_mmu_free_roots(vcpu->kvm, &vcpu->arch.guest_mmu, KVM_MMU_ROOTS_ALL);

nested_release_evmcs(vcpu);
@@ -3477,7 +3491,8 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)
{
struct vmcs12 *vmcs12 = get_vmcs12(vcpu);
struct vcpu_vmx *vmx = to_vmx(vcpu);
- struct kvm_host_map *map;
+ struct gfn_to_pfn_cache *gpc;
+ hpa_t hpa;

if (!vcpu->arch.pdptrs_from_userspace &&
!nested_cpu_has_ept(vmcs12) && is_pae_paging(vcpu)) {
@@ -3492,10 +3507,10 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)

if (nested_cpu_has2(vmcs12, SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES)) {
- map = &vmx->nested.apic_access_page_map;
+ gpc = &vmx->nested.apic_access_page_cache;

- if (!kvm_vcpu_map(vcpu, gpa_to_gfn(vmcs12->apic_access_addr), map)) {
- vmcs_write64(APIC_ACCESS_ADDR, pfn_to_hpa(map->pfn));
+ if (!nested_gpc_hpa(gpc, vmcs12->apic_access_addr, &hpa)) {
+ vmcs_write64(APIC_ACCESS_ADDR, hpa);
} else {
pr_debug_ratelimited("%s: no backing for APIC-access address in vmcs12\n",
__func__);
@@ -3508,10 +3523,10 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)
}

if (nested_cpu_has(vmcs12, CPU_BASED_TPR_SHADOW)) {
- map = &vmx->nested.virtual_apic_map;
+ gpc = &vmx->nested.virtual_apic_cache;

- if (!kvm_vcpu_map(vcpu, gpa_to_gfn(vmcs12->virtual_apic_page_addr), map)) {
- vmcs_write64(VIRTUAL_APIC_PAGE_ADDR, pfn_to_hpa(map->pfn));
+ if (!nested_gpc_hpa(gpc, vmcs12->virtual_apic_page_addr, &hpa)) {
+ vmcs_write64(VIRTUAL_APIC_PAGE_ADDR, hpa);
} else if (nested_cpu_has(vmcs12, CPU_BASED_CR8_LOAD_EXITING) &&
nested_cpu_has(vmcs12, CPU_BASED_CR8_STORE_EXITING) &&
!nested_cpu_has2(vmcs12, SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES)) {
@@ -3534,14 +3549,12 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)
}

if (nested_cpu_has_posted_intr(vmcs12)) {
- map = &vmx->nested.pi_desc_map;
+ gpc = &vmx->nested.pi_desc_cache;

- if (!kvm_vcpu_map(vcpu, gpa_to_gfn(vmcs12->posted_intr_desc_addr), map)) {
- vmx->nested.pi_desc =
- (struct pi_desc *)(((void *)map->hva) +
- offset_in_page(vmcs12->posted_intr_desc_addr));
+ if (!nested_gpc_hpa(gpc, vmcs12->posted_intr_desc_addr & PAGE_MASK, &hpa)) {
+ vmx->nested.pi_desc_offset = offset_in_page(vmcs12->posted_intr_desc_addr);
vmcs_write64(POSTED_INTR_DESC_ADDR,
- pfn_to_hpa(map->pfn) + offset_in_page(vmcs12->posted_intr_desc_addr));
+ hpa + offset_in_page(vmcs12->posted_intr_desc_addr));
} else {
/*
* Defer the KVM_INTERNAL_EXIT until KVM tries to
@@ -3549,7 +3562,6 @@ static bool nested_get_vmcs12_pages(struct kvm_vcpu *vcpu)
* descriptor. (Note that KVM may do this when it
* should not, per the architectural specification.)
*/
- vmx->nested.pi_desc = NULL;
pin_controls_clearbit(vmx, PIN_BASED_POSTED_INTR);
}
}
@@ -3590,7 +3602,16 @@ static bool vmx_get_nested_state_pages(struct kvm_vcpu *vcpu)

static bool vmx_is_nested_state_invalid(struct kvm_vcpu *vcpu)
{
- return false;
+ struct vcpu_vmx *vmx = to_vmx(vcpu);
+
+ /*
+ * @vcpu is in IN_GUEST_MODE, eliminating the need for individual gpc
+ * locks. Since kvm_gpc_invalid() doesn't verify gpc memslot
+ * generation, we can also skip acquiring the srcu lock.
+ */
+ return kvm_gpc_invalid(&vmx->nested.apic_access_page_cache) ||
+ kvm_gpc_invalid(&vmx->nested.virtual_apic_cache) ||
+ kvm_gpc_invalid(&vmx->nested.pi_desc_cache);
}

static int nested_vmx_write_pml_buffer(struct kvm_vcpu *vcpu, gpa_t gpa)
@@ -4091,9 +4112,55 @@ void nested_mark_vmcs12_pages_dirty(struct kvm_vcpu *vcpu)
}
}

+static void *nested_gpc_lock_if_active(struct gfn_to_pfn_cache *gpc)
+{
+retry:
+ read_lock(&gpc->lock);
+ if (!gpc->active) {
+ read_unlock(&gpc->lock);
+ return NULL;
+ }
+
+ if (!kvm_gpc_check(gpc, PAGE_SIZE)) {
+ read_unlock(&gpc->lock);
+ if (kvm_gpc_refresh(gpc, PAGE_SIZE))
+ return NULL;
+ goto retry;
+ }
+
+ return gpc->khva;
+}
+
+static struct pi_desc *nested_lock_pi_desc(struct vcpu_vmx *vmx)
+{
+ u8 *pi_desc_page;
+
+ pi_desc_page = nested_gpc_lock_if_active(&vmx->nested.pi_desc_cache);
+ if (!pi_desc_page)
+ return NULL;
+
+ return (struct pi_desc *)(pi_desc_page + vmx->nested.pi_desc_offset);
+}
+
+static void nested_unlock_pi_desc(struct vcpu_vmx *vmx)
+{
+ nested_gpc_unlock(&vmx->nested.pi_desc_cache);
+}
+
+static void *nested_lock_vapic(struct vcpu_vmx *vmx)
+{
+ return nested_gpc_lock_if_active(&vmx->nested.virtual_apic_cache);
+}
+
+static void nested_unlock_vapic(struct vcpu_vmx *vmx)
+{
+ nested_gpc_unlock(&vmx->nested.virtual_apic_cache);
+}
+
static int vmx_complete_nested_posted_interrupt(struct kvm_vcpu *vcpu)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
+ struct pi_desc *pi_desc;
int max_irr;
void *vapic_page;
u16 status;
@@ -4101,22 +4168,29 @@ static int vmx_complete_nested_posted_interrupt(struct kvm_vcpu *vcpu)
if (!vmx->nested.pi_pending)
return 0;

- if (!vmx->nested.pi_desc)
+ pi_desc = nested_lock_pi_desc(vmx);
+ if (!pi_desc)
goto mmio_needed;

vmx->nested.pi_pending = false;

- if (!pi_test_and_clear_on(vmx->nested.pi_desc))
+ if (!pi_test_and_clear_on(pi_desc)) {
+ nested_unlock_pi_desc(vmx);
return 0;
+ }

- max_irr = pi_find_highest_vector(vmx->nested.pi_desc);
+ max_irr = pi_find_highest_vector(pi_desc);
if (max_irr > 0) {
- vapic_page = vmx->nested.virtual_apic_map.hva;
- if (!vapic_page)
+ vapic_page = nested_lock_vapic(vmx);
+ if (!vapic_page) {
+ nested_unlock_pi_desc(vmx);
goto mmio_needed;
+ }
+
+ __kvm_apic_update_irr(pi_desc->pir, vapic_page, &max_irr);
+
+ nested_unlock_vapic(vmx);

- __kvm_apic_update_irr(vmx->nested.pi_desc->pir,
- vapic_page, &max_irr);
status = vmcs_read16(GUEST_INTR_STATUS);
if ((u8)max_irr > ((u8)status & 0xff)) {
status &= ~0xff;
@@ -4125,6 +4199,7 @@ static int vmx_complete_nested_posted_interrupt(struct kvm_vcpu *vcpu)
}
}

+ nested_unlock_pi_desc(vmx);
nested_mark_vmcs12_pages_dirty(vcpu);
return 0;

@@ -4244,8 +4319,10 @@ static bool nested_vmx_preemption_timer_pending(struct kvm_vcpu *vcpu)
static bool vmx_has_nested_events(struct kvm_vcpu *vcpu, bool for_injection)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
- void *vapic = vmx->nested.virtual_apic_map.hva;
+ struct pi_desc *pi_desc;
int max_irr, vppr;
+ void *vapic;
+ bool res = false;

if (nested_vmx_preemption_timer_pending(vcpu) ||
vmx->nested.mtf_pending)
@@ -4264,23 +4341,33 @@ static bool vmx_has_nested_events(struct kvm_vcpu *vcpu, bool for_injection)
__vmx_interrupt_blocked(vcpu))
return false;

+ vapic = nested_lock_vapic(vmx);
if (!vapic)
return false;

vppr = *((u32 *)(vapic + APIC_PROCPRI));

+ nested_unlock_vapic(vmx);
+
max_irr = vmx_get_rvi();
if ((max_irr & 0xf0) > (vppr & 0xf0))
return true;

- if (vmx->nested.pi_pending && vmx->nested.pi_desc &&
- pi_test_on(vmx->nested.pi_desc)) {
- max_irr = pi_find_highest_vector(vmx->nested.pi_desc);
- if (max_irr > 0 && (max_irr & 0xf0) > (vppr & 0xf0))
- return true;
+ if (vmx->nested.pi_pending) {
+ pi_desc = nested_lock_pi_desc(vmx);
+ if (!pi_desc)
+ return false;
+
+ if (pi_test_on(pi_desc)) {
+ max_irr = pi_find_highest_vector(pi_desc);
+ if (max_irr > 0 && (max_irr & 0xf0) > (vppr & 0xf0))
+ res = true;
+ }
+
+ nested_unlock_pi_desc(vmx);
}

- return false;
+ return res;
}

/*
@@ -5244,7 +5331,7 @@ void __nested_vmx_vmexit(struct kvm_vcpu *vcpu, u32 vm_exit_reason,
vmx_update_cpu_dirty_logging(vcpu);
}

- nested_put_vmcs12_pages(vcpu);
+ nested_mark_vmcs12_pages_dirty(vcpu);

if (vmx->nested.reload_vmcs01_apic_access_page) {
vmx->nested.reload_vmcs01_apic_access_page = false;
@@ -5529,6 +5616,10 @@ static int enter_vmx_operation(struct kvm_vcpu *vcpu)

kvm_gpc_init(&vmx->nested.msr_bitmap_cache, vcpu->kvm);

+ kvm_gpc_init_for_vcpu(&vmx->nested.apic_access_page_cache, vcpu);
+ kvm_gpc_init_for_vcpu(&vmx->nested.virtual_apic_cache, vcpu);
+ kvm_gpc_init_for_vcpu(&vmx->nested.pi_desc_cache, vcpu);
+
vmx->nested.vmcs02_initialized = false;
vmx->nested.vmxon = true;

diff --git a/arch/x86/kvm/vmx/vmx.h b/arch/x86/kvm/vmx/vmx.h
index d76621403c28..9a285834ccda 100644
--- a/arch/x86/kvm/vmx/vmx.h
+++ b/arch/x86/kvm/vmx/vmx.h
@@ -158,11 +158,11 @@ struct nested_vmx {
* Guest pages referred to in the vmcs02 with host-physical
* pointers, so we must keep them pinned while L2 runs.
*/
- struct kvm_host_map apic_access_page_map;
- struct kvm_host_map virtual_apic_map;
- struct kvm_host_map pi_desc_map;
+ struct gfn_to_pfn_cache apic_access_page_cache;
+ struct gfn_to_pfn_cache virtual_apic_cache;
+ struct gfn_to_pfn_cache pi_desc_cache;

- struct pi_desc *pi_desc;
+ u64 pi_desc_offset;
bool pi_pending;
u16 posted_intr_nv;

diff --git a/include/linux/kvm_host.h b/include/linux/kvm_host.h
index 6253cf1c38c1..b05aace9e295 100644
--- a/include/linux/kvm_host.h
+++ b/include/linux/kvm_host.h
@@ -1531,6 +1531,11 @@ static inline bool kvm_gpc_is_hva_active(struct gfn_to_pfn_cache *gpc)
return gpc->active && kvm_is_error_gpa(gpc->gpa);
}

+static inline bool kvm_gpc_invalid(struct gfn_to_pfn_cache *gpc)
+{
+ return gpc->active && !gpc->valid;
+}
+
void kvm_sigset_activate(struct kvm_vcpu *vcpu);
void kvm_sigset_deactivate(struct kvm_vcpu *vcpu);

--
2.43.0