[PATCH 16/17] KVM: nSVM: Cache L1's IO permissions map pages
From: David Woodhouse
Date: Sun Sep 20 2026 - 17:21:02 EST
From: David Woodhouse <dwmw@xxxxxxxxxxxx>
The per-intercept IOPM check on every trapped L2 IO instruction reads
L1's IO permissions map with kvm_vcpu_read_guest() — a
memremap/memunmap cycle per access with unmanaged guest memory. Cache
the three IOPM pages in gfn_to_pfn_caches keyed on iopm_base_pa, as
for the MSR permissions map.
Unlike the MSRPM accesses, an IOPM read is not naturally aligned: a
two-byte read for a port range straddling a page boundary (e.g. ports
0x7ff8-0x7fff at bytes 0xfff-0x1000) spans two pages, so read byte by
byte, each from its own page's cache.
Failure semantics are preserved: an unreadable IOPM reflects the
intercept to L1 (NESTED_EXIT_DONE).
Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
Assisted-by: Claude:claude-mythos-5
---
arch/x86/kvm/svm/nested.c | 33 +++++++++++++++++++++++++++++----
arch/x86/kvm/svm/svm.h | 6 ++++++
2 files changed, 35 insertions(+), 4 deletions(-)
diff --git a/arch/x86/kvm/svm/nested.c b/arch/x86/kvm/svm/nested.c
index 07609e4802f9..0fa3183a509c 100644
--- a/arch/x86/kvm/svm/nested.c
+++ b/arch/x86/kvm/svm/nested.c
@@ -1532,6 +1532,8 @@ int svm_allocate_nested(struct vcpu_svm *svm)
kvm_gpc_init(&svm->nested.vmcb12_cache, svm->vcpu.kvm);
for (i = 0; i < ARRAY_SIZE(svm->nested.msrpm12_cache); i++)
kvm_gpc_init(&svm->nested.msrpm12_cache[i], svm->vcpu.kvm);
+ for (i = 0; i < ARRAY_SIZE(svm->nested.iopm12_cache); i++)
+ kvm_gpc_init(&svm->nested.iopm12_cache[i], svm->vcpu.kvm);
svm->nested.initialized = true;
return 0;
@@ -1557,6 +1559,8 @@ void svm_free_nested(struct vcpu_svm *svm)
kvm_gpc_deactivate(&svm->nested.vmcb12_cache);
for (i = 0; i < ARRAY_SIZE(svm->nested.msrpm12_cache); i++)
kvm_gpc_deactivate(&svm->nested.msrpm12_cache[i]);
+ for (i = 0; i < ARRAY_SIZE(svm->nested.iopm12_cache); i++)
+ kvm_gpc_deactivate(&svm->nested.iopm12_cache[i]);
__free_page(virt_to_page(svm->nested.vmcb02.ptr));
svm->nested.vmcb02.ptr = NULL;
@@ -1635,7 +1639,7 @@ static int nested_svm_intercept_ioio(struct vcpu_svm *svm)
unsigned port, size, iopm_len;
u16 val, mask;
u8 start_bit;
- u64 gpa;
+ int i;
if (!(vmcb12_is_intercept(&svm->nested.ctl, INTERCEPT_IOIO_PROT)))
return NESTED_EXIT_HOST;
@@ -1643,14 +1647,35 @@ static int nested_svm_intercept_ioio(struct vcpu_svm *svm)
port = svm->vmcb->control.exit_info_1 >> 16;
size = (svm->vmcb->control.exit_info_1 & SVM_IOIO_SIZE_MASK) >>
SVM_IOIO_SIZE_SHIFT;
- gpa = svm->nested.ctl.iopm_base_pa + (port / 8);
start_bit = port % 8;
iopm_len = (start_bit + size > 8) ? 2 : 1;
mask = (0xf >> (4 - size)) << start_bit;
val = 0;
- if (kvm_vcpu_read_guest(&svm->vcpu, gpa, &val, iopm_len))
- return NESTED_EXIT_DONE;
+ /*
+ * Read byte by byte: a two-byte read at the end of a page (e.g.
+ * ports 0x7ff8-0x7fff spanning bytes 0xfff-0x1000) crosses into
+ * the next page of the IOPM, i.e. the next cache.
+ */
+ for (i = 0; i < iopm_len; i++) {
+ unsigned long offset = port / 8 + i;
+ struct gfn_to_pfn_cache *gpc;
+ gpa_t gpa;
+ int idx;
+
+ if (WARN_ON_ONCE(offset >= IOPM_SIZE))
+ return NESTED_EXIT_DONE;
+
+ gpc = &svm->nested.iopm12_cache[offset >> PAGE_SHIFT];
+ gpa = svm->nested.ctl.iopm_base_pa + (offset & PAGE_MASK);
+
+ idx = kvm_gpc_lock_page(gpc, gpa);
+ if (idx < 0)
+ return NESTED_EXIT_DONE;
+
+ val |= *(u8 *)(gpc->khva + offset_in_page(offset)) << (i * 8);
+ kvm_gpc_unlock(gpc, idx);
+ }
return (val & mask) ? NESTED_EXIT_DONE : NESTED_EXIT_HOST;
}
diff --git a/arch/x86/kvm/svm/svm.h b/arch/x86/kvm/svm/svm.h
index ac2f9731bf5d..8ccd5a2d942e 100644
--- a/arch/x86/kvm/svm/svm.h
+++ b/arch/x86/kvm/svm/svm.h
@@ -228,6 +228,12 @@ struct svm_nested_state {
*/
struct gfn_to_pfn_cache msrpm12_cache[MSRPM_SIZE / PAGE_SIZE];
+ /*
+ * Cached mappings of the three pages of L1's IO permissions map,
+ * keyed on ctl.iopm_base_pa, for the per-intercept checks.
+ */
+ struct gfn_to_pfn_cache iopm12_cache[IOPM_SIZE / PAGE_SIZE];
+
/*
* The MSR permissions map used for vmcb02, which is the merge result
* of vmcb01 and vmcb12
--
2.55.0