* [RFC PATCH 0/5] KVM: x86/mmu: Allocate memslot rmaps on demand
@ 2026-11-04 0:53 Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 1/5] KVM: x86/mmu: Walk memslot rmaps by index Fred Griffoul
` (4 more replies)
0 siblings, 5 replies; 7+ messages in thread
From: Fred Griffoul @ 2026-11-04 0:53 UTC (permalink / raw)
To: Sean Christopherson, Paolo Bonzini
Cc: Mushahid Hussain, James Houghton, nh-open-source, kvm,
linux-kernel
From: Fred Griffoul <fgriffo@amazon.co.uk>
When an L1 guest runs a nested guest, KVM allocates rmaps for all of
L1's memory the first time it creates a shadow root, one 8-byte head
per 4 KiB page. For an L1 with 4 TiB of memory that is 8 GiB, allocated
at once on the first nested VM-Enter, whether L2 maps 1% or all of L1's
memory. Sean pointed at this cost on Mushahid's lpage_info patch [1].
This RFC allocates rmaps in page-sized chunks instead, when the first
SPTE in a chunk's range is added. Each memslot keeps a per-level
directory of chunks, 8 bytes per 2 MiB at the 4 KiB level, so the 4 TiB
L1 above allocates 16 MiB on the first nested VM-Enter. A missing chunk
reads as empty rmaps and slot walks skip it. When L2 maps every gfn,
rmaps cost what they did before plus the directory.
Patches 1 and 2 prepare the slot walk and the debugfs rmap statistics,
with no functional change. Patch 3 does the chunking. Patches 4 and 5
add a selftest in which L2 writes pages around chunk boundaries and
checks the dirty log after the memslot is write-protected, including
eager splitting of nested huge pages.
I would like feedback on:
- The data structure. A directory of page-sized chunks keeps lookups
to one extra load. An xarray would be simpler to grow but adds a
tree walk to every rmap lookup.
- Freeing chunks once they are empty. That would let the shadow MMU
page limit bound rmap memory too, but lockless aging reads chunks
without mmu_lock, so freeing needs an RCU grace period. I left it
out of this RFC.
- Doing the same for gfn_write_track, 2 bytes per 4 KiB page, which
is still allocated for every memslot on the first shadow root.
Limitations:
- Chunks are only freed with the memslot.
- Eager page splitting on dirty-log enable splits L2's huge mappings,
so every 2 MiB that L2 mapped with a huge page then gets a chunk.
The series is based on kvm.git next.
[1] https://lore.kernel.org/all/arr1VgGZmgUmY4hc@google.com/
Fred
Fred Griffoul (5):
KVM: x86/mmu: Walk memslot rmaps by index
KVM: x86: Count rmaps per level in a debugfs helper
KVM: x86/mmu: Allocate memslot rmaps in on-demand chunks
KVM: selftests: Declare __tdp_map() in the x86 processor header
KVM: selftests: Test dirty logging of sparse nested mappings
arch/x86/include/asm/kvm_host.h | 22 +-
arch/x86/kvm/debugfs.c | 46 ++-
arch/x86/kvm/mmu/mmu.c | 126 +++++++-
arch/x86/kvm/x86.c | 22 +-
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/include/x86/processor.h | 1 +
.../kvm/x86/nested_dirty_log_sparse_test.c | 275 ++++++++++++++++++
7 files changed, 460 insertions(+), 33 deletions(-)
create mode 100644 tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c
base-commit: d21eef2084a9af87ac2a783bc7c2b175ba114e4e
--
2.47.3
^ permalink raw reply [flat|nested] 7+ messages in thread
* [RFC PATCH 1/5] KVM: x86/mmu: Walk memslot rmaps by index
2026-11-04 0:53 [RFC PATCH 0/5] KVM: x86/mmu: Allocate memslot rmaps on demand Fred Griffoul
@ 2026-11-04 0:53 ` Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 2/5] KVM: x86: Count rmaps per level in a debugfs helper Fred Griffoul
` (3 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Fred Griffoul @ 2026-11-04 0:53 UTC (permalink / raw)
To: Sean Christopherson, Paolo Bonzini
Cc: Mushahid Hussain, James Houghton, nh-open-source, kvm,
linux-kernel
From: Fred Griffoul <fgriffo@amazon.co.uk>
In preparation for storing memslot rmaps in chunks that are not
contiguous in memory, walk a slot's rmaps by index instead of stepping
a pointer through the array, and look each head up through a new
__idx_to_rmap() helper.
No functional change intended.
Signed-off-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
arch/x86/kvm/mmu/mmu.c | 34 ++++++++++++++++++++++++----------
1 file changed, 24 insertions(+), 10 deletions(-)
diff --git a/arch/x86/kvm/mmu/mmu.c b/arch/x86/kvm/mmu/mmu.c
index f4b12d6aef55..b3fcd6f2a991 100644
--- a/arch/x86/kvm/mmu/mmu.c
+++ b/arch/x86/kvm/mmu/mmu.c
@@ -1236,13 +1236,17 @@ unsigned int pte_list_count(struct kvm_rmap_head *rmap_head)
return desc->tail_count + desc->spte_count;
}
+static struct kvm_rmap_head *__idx_to_rmap(const struct kvm_memory_slot *slot,
+ int level, unsigned long idx)
+{
+ return &slot->arch.rmap[level - PG_LEVEL_4K][idx];
+}
+
static struct kvm_rmap_head *gfn_to_rmap(gfn_t gfn, int level,
const struct kvm_memory_slot *slot)
{
- unsigned long idx;
-
- idx = gfn_to_index(gfn, slot->base_gfn, level);
- return &slot->arch.rmap[level - PG_LEVEL_4K][idx];
+ return __idx_to_rmap(slot, level,
+ gfn_to_index(gfn, slot->base_gfn, level));
}
static void rmap_remove(struct kvm *kvm, u64 *spte)
@@ -1577,17 +1581,21 @@ struct slot_rmap_walk_iterator {
struct kvm_rmap_head *rmap;
int level;
- /* private field. */
- struct kvm_rmap_head *end_rmap;
+ /* private fields. */
+ unsigned long idx;
+ unsigned long end_idx;
};
static void rmap_walk_init_level(struct slot_rmap_walk_iterator *iterator,
int level)
{
+ const struct kvm_memory_slot *slot = iterator->slot;
+
iterator->level = level;
iterator->gfn = iterator->start_gfn;
- iterator->rmap = gfn_to_rmap(iterator->gfn, level, iterator->slot);
- iterator->end_rmap = gfn_to_rmap(iterator->end_gfn, level, iterator->slot);
+ iterator->idx = gfn_to_index(iterator->start_gfn, slot->base_gfn, level);
+ iterator->end_idx = gfn_to_index(iterator->end_gfn, slot->base_gfn, level);
+ iterator->rmap = gfn_to_rmap(iterator->gfn, level, slot);
}
static void slot_rmap_walk_init(struct slot_rmap_walk_iterator *iterator,
@@ -1611,11 +1619,17 @@ static bool slot_rmap_walk_okay(struct slot_rmap_walk_iterator *iterator)
static void slot_rmap_walk_next(struct slot_rmap_walk_iterator *iterator)
{
- while (++iterator->rmap <= iterator->end_rmap) {
+ struct kvm_rmap_head *rmap_head;
+
+ while (++iterator->idx <= iterator->end_idx) {
iterator->gfn += KVM_PAGES_PER_HPAGE(iterator->level);
- if (atomic_long_read(&iterator->rmap->val))
+ rmap_head = __idx_to_rmap(iterator->slot, iterator->level,
+ iterator->idx);
+ if (atomic_long_read(&rmap_head->val)) {
+ iterator->rmap = rmap_head;
return;
+ }
}
if (++iterator->level > iterator->end_level) {
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [RFC PATCH 2/5] KVM: x86: Count rmaps per level in a debugfs helper
2026-11-04 0:53 [RFC PATCH 0/5] KVM: x86/mmu: Allocate memslot rmaps on demand Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 1/5] KVM: x86/mmu: Walk memslot rmaps by index Fred Griffoul
@ 2026-11-04 0:53 ` Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 3/5] KVM: x86/mmu: Allocate memslot rmaps in on-demand chunks Fred Griffoul
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Fred Griffoul @ 2026-11-04 0:53 UTC (permalink / raw)
To: Sean Christopherson, Paolo Bonzini
Cc: Mushahid Hussain, James Houghton, nh-open-source, kvm,
linux-kernel
From: Fred Griffoul <fgriffo@amazon.co.uk>
In preparation for storing memslot rmaps in chunks, move the counting
of one memslot level's rmaps into a helper, so that it can later walk
a level one chunk at a time.
No functional change intended.
Signed-off-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
arch/x86/kvm/debugfs.c | 33 +++++++++++++++++++--------------
1 file changed, 19 insertions(+), 14 deletions(-)
diff --git a/arch/x86/kvm/debugfs.c b/arch/x86/kvm/debugfs.c
index 0074a56e45b4..923424011328 100644
--- a/arch/x86/kvm/debugfs.c
+++ b/arch/x86/kvm/debugfs.c
@@ -86,16 +86,30 @@ void kvm_arch_create_vcpu_debugfs(struct kvm_vcpu *vcpu, struct dentry *debugfs_
static const char *kvm_lpage_str[KVM_NR_PAGE_SIZES] = { "4K", "2M", "1G" };
+static void kvm_mmu_rmaps_stat_level(struct kvm_memory_slot *slot, int level,
+ unsigned int *cur)
+{
+ struct kvm_rmap_head *rmap = slot->arch.rmap[level - PG_LEVEL_4K];
+ unsigned long lpages = kvm_mmu_slot_lpages(slot, level);
+ unsigned int bucket;
+ unsigned long l;
+
+ for (l = 0; l < lpages; l++) {
+ bucket = fls(pte_list_count(&rmap[l]));
+ if (WARN_ON_ONCE(bucket >= RMAP_LOG_SIZE))
+ bucket = RMAP_LOG_SIZE - 1;
+ cur[bucket]++;
+ }
+}
+
static int kvm_mmu_rmaps_stat_show(struct seq_file *m, void *v)
{
- struct kvm_rmap_head *rmap;
struct kvm *kvm = m->private;
struct kvm_memory_slot *slot;
struct kvm_memslots *slots;
- unsigned int lpage_size, index;
/* Still small enough to be on the stack */
unsigned int *log[KVM_NR_PAGE_SIZES], *cur;
- int i, j, k, l, ret;
+ int i, j, k, ret;
if (!kvm_memslots_have_rmaps(kvm))
return 0;
@@ -116,17 +130,8 @@ static int kvm_mmu_rmaps_stat_show(struct seq_file *m, void *v)
slots = __kvm_memslots(kvm, i);
kvm_for_each_memslot(slot, bkt, slots)
- for (k = 0; k < KVM_NR_PAGE_SIZES; k++) {
- rmap = slot->arch.rmap[k];
- lpage_size = kvm_mmu_slot_lpages(slot, k + 1);
- cur = log[k];
- for (l = 0; l < lpage_size; l++) {
- index = fls(pte_list_count(&rmap[l]));
- if (WARN_ON_ONCE(index >= RMAP_LOG_SIZE))
- index = RMAP_LOG_SIZE - 1;
- cur[index]++;
- }
- }
+ for (k = 0; k < KVM_NR_PAGE_SIZES; k++)
+ kvm_mmu_rmaps_stat_level(slot, k + 1, log[k]);
}
write_unlock(&kvm->mmu_lock);
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [RFC PATCH 3/5] KVM: x86/mmu: Allocate memslot rmaps in on-demand chunks
2026-11-04 0:53 [RFC PATCH 0/5] KVM: x86/mmu: Allocate memslot rmaps on demand Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 1/5] KVM: x86/mmu: Walk memslot rmaps by index Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 2/5] KVM: x86: Count rmaps per level in a debugfs helper Fred Griffoul
@ 2026-11-04 0:53 ` Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 4/5] KVM: selftests: Declare __tdp_map() in the x86 processor header Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 5/5] KVM: selftests: Test dirty logging of sparse nested mappings Fred Griffoul
4 siblings, 0 replies; 7+ messages in thread
From: Fred Griffoul @ 2026-11-04 0:53 UTC (permalink / raw)
To: Sean Christopherson, Paolo Bonzini
Cc: Mushahid Hussain, James Houghton, nh-open-source, kvm,
linux-kernel
From: Fred Griffoul <fgriffo@amazon.co.uk>
When L1 runs a nested guest, KVM allocates rmaps for all of L1's
memory the first time it creates a shadow root, which is 8 GiB in one
allocation for a VM with 4 TiB of memory. Only the heads of gfns that
L2 maps are ever used, so an L1 whose L2s map a fraction of its memory
pays mostly for empty rmaps.
Allocate rmaps in page-sized chunks instead, when the first SPTE in a
chunk's range is added, and keep a per-level directory of chunks in
the memslot. A missing chunk reads as empty rmaps. That way, the rmap
cost follows what L2 maps, and when L2 maps every gfn it is what it
was before plus the directory.
Signed-off-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
arch/x86/include/asm/kvm_host.h | 22 ++++++-
arch/x86/kvm/debugfs.c | 31 +++++++---
arch/x86/kvm/mmu/mmu.c | 100 +++++++++++++++++++++++++++++---
arch/x86/kvm/x86.c | 22 +++++--
4 files changed, 153 insertions(+), 22 deletions(-)
diff --git a/arch/x86/include/asm/kvm_host.h b/arch/x86/include/asm/kvm_host.h
index 0378577d9d8b..4cc00dc83aba 100644
--- a/arch/x86/include/asm/kvm_host.h
+++ b/arch/x86/include/asm/kvm_host.h
@@ -431,6 +431,8 @@ struct kvm_rmap_head {
atomic_long_t val;
};
+#define KVM_RMAP_CHUNK_NR_HEADS (PAGE_SIZE / sizeof(struct kvm_rmap_head))
+
struct kvm_pio_request {
unsigned long count;
int in;
@@ -785,6 +787,7 @@ struct kvm_vcpu_arch {
u64 pdptrs[4]; /* pae */
struct kvm_mmu_memory_cache mmu_pte_list_desc_cache;
+ struct kvm_mmu_memory_cache mmu_rmap_chunk_cache;
struct kvm_mmu_memory_cache mmu_shadow_page_cache;
struct kvm_mmu_memory_cache mmu_shadowed_info_cache;
struct kvm_mmu_memory_cache mmu_page_header_cache;
@@ -1044,7 +1047,13 @@ struct kvm_lpage_info {
};
struct kvm_arch_memory_slot {
- struct kvm_rmap_head *rmap[KVM_NR_PAGE_SIZES];
+ /*
+ * Per-level directories of rmap chunks, each chunk holding
+ * KVM_RMAP_CHUNK_NR_HEADS rmap heads. Chunks are allocated when KVM
+ * adds the first SPTE to a head in the chunk's range. A NULL chunk
+ * means all of its heads are empty.
+ */
+ struct kvm_rmap_head **rmap[KVM_NR_PAGE_SIZES];
struct kvm_lpage_info *lpage_info[KVM_NR_PAGE_SIZES - 1];
unsigned short *gfn_write_track;
};
@@ -1418,6 +1427,17 @@ struct kvm_arch {
#define SPLIT_DESC_CACHE_MIN_NR_OBJECTS (SPTE_ENT_PER_PAGE + 1)
struct kvm_mmu_memory_cache split_desc_cache;
+ /*
+ * Memory cache used to allocate rmap chunks while splitting huge
+ * pages. The rmap heads of the lower level leaf SPTEs of one huge
+ * page are contiguous, but span two chunks if the memslot's base gfn
+ * is not aligned to the chunk size.
+ *
+ * Protected by kvm->slots_lock.
+ */
+#define SPLIT_RMAP_CHUNK_CACHE_NR_OBJECTS 2
+ struct kvm_mmu_memory_cache split_rmap_chunk_cache;
+
gfn_t gfn_direct_bits;
int mirror_root_level;
diff --git a/arch/x86/kvm/debugfs.c b/arch/x86/kvm/debugfs.c
index 923424011328..0cdb1ef1b184 100644
--- a/arch/x86/kvm/debugfs.c
+++ b/arch/x86/kvm/debugfs.c
@@ -89,16 +89,29 @@ static const char *kvm_lpage_str[KVM_NR_PAGE_SIZES] = { "4K", "2M", "1G" };
static void kvm_mmu_rmaps_stat_level(struct kvm_memory_slot *slot, int level,
unsigned int *cur)
{
- struct kvm_rmap_head *rmap = slot->arch.rmap[level - PG_LEVEL_4K];
+ struct kvm_rmap_head **rmap = slot->arch.rmap[level - PG_LEVEL_4K];
unsigned long lpages = kvm_mmu_slot_lpages(slot, level);
- unsigned int bucket;
- unsigned long l;
-
- for (l = 0; l < lpages; l++) {
- bucket = fls(pte_list_count(&rmap[l]));
- if (WARN_ON_ONCE(bucket >= RMAP_LOG_SIZE))
- bucket = RMAP_LOG_SIZE - 1;
- cur[bucket]++;
+ struct kvm_rmap_head *chunk;
+ unsigned long l, nr;
+ unsigned int index;
+
+ for (l = 0; l < lpages; l += nr) {
+ nr = min(lpages - l, KVM_RMAP_CHUNK_NR_HEADS);
+ chunk = rmap[l / KVM_RMAP_CHUNK_NR_HEADS];
+
+ /* An unallocated chunk holds only empty rmaps. */
+ if (!chunk) {
+ cur[0] += nr;
+ continue;
+ }
+
+ for (index = 0; index < nr; index++) {
+ unsigned int bucket = fls(pte_list_count(&chunk[index]));
+
+ if (WARN_ON_ONCE(bucket >= RMAP_LOG_SIZE))
+ bucket = RMAP_LOG_SIZE - 1;
+ cur[bucket]++;
+ }
}
}
diff --git a/arch/x86/kvm/mmu/mmu.c b/arch/x86/kvm/mmu/mmu.c
index b3fcd6f2a991..1b958f66764c 100644
--- a/arch/x86/kvm/mmu/mmu.c
+++ b/arch/x86/kvm/mmu/mmu.c
@@ -612,6 +612,17 @@ static int mmu_topup_memory_caches(struct kvm_vcpu *vcpu, bool maybe_indirect)
1 + PT64_ROOT_MAX_LEVEL + PTE_PREFETCH_NUM);
if (r)
return r;
+ /*
+ * 1 rmap chunk for the faulting gfn and 1 per prefetched gfn. Cap the
+ * cache at that, as each object is a page.
+ */
+ if (kvm_memslots_have_rmaps(vcpu->kvm)) {
+ r = __kvm_mmu_topup_memory_cache(&vcpu->arch.mmu_rmap_chunk_cache,
+ 1 + PTE_PREFETCH_NUM,
+ 1 + PTE_PREFETCH_NUM);
+ if (r)
+ return r;
+ }
if (kvm_has_mirrored_tdp(vcpu->kvm)) {
r = kvm_mmu_topup_memory_cache(&vcpu->arch.mmu_external_spt_cache,
PT64_ROOT_MAX_LEVEL);
@@ -635,6 +646,7 @@ static int mmu_topup_memory_caches(struct kvm_vcpu *vcpu, bool maybe_indirect)
static void mmu_free_memory_caches(struct kvm_vcpu *vcpu)
{
kvm_mmu_free_memory_cache(&vcpu->arch.mmu_pte_list_desc_cache);
+ kvm_mmu_free_memory_cache(&vcpu->arch.mmu_rmap_chunk_cache);
kvm_mmu_free_memory_cache(&vcpu->arch.mmu_shadow_page_cache);
kvm_mmu_free_memory_cache(&vcpu->arch.mmu_shadowed_info_cache);
kvm_mmu_free_memory_cache(&vcpu->arch.mmu_external_spt_cache);
@@ -1236,17 +1248,65 @@ unsigned int pte_list_count(struct kvm_rmap_head *rmap_head)
return desc->tail_count + desc->spte_count;
}
+/*
+ * Returned by gfn_to_rmap() for heads in a chunk that hasn't been allocated.
+ * Readers see an empty rmap, and only __rmap_add() writes to a head that may
+ * be empty, after allocating its chunk.
+ */
+static struct kvm_rmap_head kvm_empty_rmap_head;
+
+static struct kvm_rmap_head **
+rmap_chunkp(const struct kvm_memory_slot *slot, int level, unsigned long idx)
+{
+ return &slot->arch.rmap[level - PG_LEVEL_4K][idx / KVM_RMAP_CHUNK_NR_HEADS];
+}
+
+/* Returns NULL if the chunk holding the head hasn't been allocated. */
static struct kvm_rmap_head *__idx_to_rmap(const struct kvm_memory_slot *slot,
int level, unsigned long idx)
{
- return &slot->arch.rmap[level - PG_LEVEL_4K][idx];
+ struct kvm_rmap_head *chunk;
+
+ /*
+ * Lockless walkers can observe a chunk being installed. Pairs with
+ * the smp_store_release() in gfn_to_rmap_alloc().
+ */
+ chunk = READ_ONCE(*rmap_chunkp(slot, level, idx));
+ if (!chunk)
+ return NULL;
+
+ return &chunk[idx % KVM_RMAP_CHUNK_NR_HEADS];
}
static struct kvm_rmap_head *gfn_to_rmap(gfn_t gfn, int level,
const struct kvm_memory_slot *slot)
{
- return __idx_to_rmap(slot, level,
- gfn_to_index(gfn, slot->base_gfn, level));
+ struct kvm_rmap_head *rmap_head;
+
+ rmap_head = __idx_to_rmap(slot, level,
+ gfn_to_index(gfn, slot->base_gfn, level));
+ return rmap_head ? : &kvm_empty_rmap_head;
+}
+
+static struct kvm_rmap_head *gfn_to_rmap_alloc(struct kvm *kvm,
+ struct kvm_mmu_memory_cache *cache,
+ gfn_t gfn, int level,
+ const struct kvm_memory_slot *slot)
+{
+ unsigned long idx = gfn_to_index(gfn, slot->base_gfn, level);
+ struct kvm_rmap_head **chunkp, *chunk;
+
+ lockdep_assert_held_write(&kvm->mmu_lock);
+
+ chunkp = rmap_chunkp(slot, level, idx);
+ chunk = *chunkp;
+ if (!chunk) {
+ chunk = kvm_mmu_memory_cache_alloc(cache);
+ /* Publish the zeroed chunk to lockless walkers. */
+ smp_store_release(chunkp, chunk);
+ }
+
+ return &chunk[idx % KVM_RMAP_CHUNK_NR_HEADS];
}
static void rmap_remove(struct kvm *kvm, u64 *spte)
@@ -1620,12 +1680,22 @@ static bool slot_rmap_walk_okay(struct slot_rmap_walk_iterator *iterator)
static void slot_rmap_walk_next(struct slot_rmap_walk_iterator *iterator)
{
struct kvm_rmap_head *rmap_head;
+ unsigned long skip;
while (++iterator->idx <= iterator->end_idx) {
iterator->gfn += KVM_PAGES_PER_HPAGE(iterator->level);
rmap_head = __idx_to_rmap(iterator->slot, iterator->level,
iterator->idx);
+ if (!rmap_head) {
+ /* Skip the rest of the unallocated chunk. */
+ skip = KVM_RMAP_CHUNK_NR_HEADS - 1 -
+ iterator->idx % KVM_RMAP_CHUNK_NR_HEADS;
+ iterator->idx += skip;
+ iterator->gfn += skip * KVM_PAGES_PER_HPAGE(iterator->level);
+ continue;
+ }
+
if (atomic_long_read(&rmap_head->val)) {
iterator->rmap = rmap_head;
return;
@@ -1748,6 +1818,7 @@ bool kvm_unmap_gfn_range(struct kvm *kvm, struct kvm_gfn_range *range)
static void __rmap_add(struct kvm *kvm,
struct kvm_mmu_memory_cache *cache,
+ struct kvm_mmu_memory_cache *chunk_cache,
const struct kvm_memory_slot *slot,
u64 *spte, gfn_t gfn, unsigned int access)
{
@@ -1759,7 +1830,7 @@ static void __rmap_add(struct kvm *kvm,
kvm_mmu_page_set_translation(sp, spte_index(spte), gfn, access);
kvm_update_page_stats(kvm, sp->role.level, 1);
- rmap_head = gfn_to_rmap(gfn, sp->role.level, slot);
+ rmap_head = gfn_to_rmap_alloc(kvm, chunk_cache, gfn, sp->role.level, slot);
rmap_count = pte_list_add(kvm, cache, spte, rmap_head);
if (rmap_count > kvm->stat.max_mmu_rmap_size)
@@ -1773,9 +1844,8 @@ static void __rmap_add(struct kvm *kvm,
static void rmap_add(struct kvm_vcpu *vcpu, const struct kvm_memory_slot *slot,
u64 *spte, gfn_t gfn, unsigned int access)
{
- struct kvm_mmu_memory_cache *cache = &vcpu->arch.mmu_pte_list_desc_cache;
-
- __rmap_add(vcpu->kvm, cache, slot, spte, gfn, access);
+ __rmap_add(vcpu->kvm, &vcpu->arch.mmu_pte_list_desc_cache,
+ &vcpu->arch.mmu_rmap_chunk_cache, slot, spte, gfn, access);
}
static bool kvm_rmap_age_gfn_range(struct kvm *kvm,
@@ -6977,6 +7047,8 @@ int kvm_mmu_create(struct kvm_vcpu *vcpu)
vcpu->arch.mmu_pte_list_desc_cache.kmem_cache = pte_list_desc_cache;
vcpu->arch.mmu_pte_list_desc_cache.gfp_zero = __GFP_ZERO;
+ vcpu->arch.mmu_rmap_chunk_cache.gfp_zero = __GFP_ZERO;
+
vcpu->arch.mmu_page_header_cache.kmem_cache = mmu_page_header_cache;
vcpu->arch.mmu_page_header_cache.gfp_zero = __GFP_ZERO;
@@ -7164,12 +7236,15 @@ int kvm_mmu_init_vm(struct kvm *kvm)
kvm->arch.split_desc_cache.kmem_cache = pte_list_desc_cache;
kvm->arch.split_desc_cache.gfp_zero = __GFP_ZERO;
+
+ kvm->arch.split_rmap_chunk_cache.gfp_zero = __GFP_ZERO;
return 0;
}
static void mmu_free_vm_memory_caches(struct kvm *kvm)
{
kvm_mmu_free_memory_cache(&kvm->arch.split_desc_cache);
+ kvm_mmu_free_memory_cache(&kvm->arch.split_rmap_chunk_cache);
kvm_mmu_free_memory_cache(&kvm->arch.split_page_header_cache);
kvm_mmu_free_memory_cache(&kvm->arch.split_shadow_page_cache);
}
@@ -7288,6 +7363,8 @@ static bool need_topup_split_caches_or_resched(struct kvm *kvm)
* is possible but not worth the complexity.
*/
return need_topup(&kvm->arch.split_desc_cache, SPLIT_DESC_CACHE_MIN_NR_OBJECTS) ||
+ need_topup(&kvm->arch.split_rmap_chunk_cache,
+ SPLIT_RMAP_CHUNK_CACHE_NR_OBJECTS) ||
need_topup(&kvm->arch.split_page_header_cache, 1) ||
need_topup(&kvm->arch.split_shadow_page_cache, 1);
}
@@ -7318,6 +7395,12 @@ static int topup_split_caches(struct kvm *kvm)
if (r)
return r;
+ r = __kvm_mmu_topup_memory_cache(&kvm->arch.split_rmap_chunk_cache,
+ SPLIT_RMAP_CHUNK_CACHE_NR_OBJECTS,
+ SPLIT_RMAP_CHUNK_CACHE_NR_OBJECTS);
+ if (r)
+ return r;
+
r = kvm_mmu_topup_memory_cache(&kvm->arch.split_page_header_cache, 1);
if (r)
return r;
@@ -7392,7 +7475,8 @@ static void shadow_mmu_split_huge_page(struct kvm *kvm,
spte = make_small_spte(kvm, huge_spte, sp->role, index);
mmu_spte_set(sptep, spte);
- __rmap_add(kvm, cache, slot, sptep, gfn, sp->role.access);
+ __rmap_add(kvm, cache, &kvm->arch.split_rmap_chunk_cache, slot,
+ sptep, gfn, sp->role.access);
}
__link_shadow_page(kvm, cache, huge_sptep, sp, flush);
diff --git a/arch/x86/kvm/x86.c b/arch/x86/kvm/x86.c
index 26e7beaff8a0..6231c9649c95 100644
--- a/arch/x86/kvm/x86.c
+++ b/arch/x86/kvm/x86.c
@@ -10141,12 +10141,27 @@ void kvm_arch_destroy_vm(struct kvm *kvm)
kvm_x86_call(vm_destroy)(kvm);
}
+static unsigned long memslot_rmap_nr_chunks(struct kvm_memory_slot *slot,
+ unsigned long npages, int level)
+{
+ return DIV_ROUND_UP(__kvm_mmu_slot_lpages(slot, npages, level),
+ KVM_RMAP_CHUNK_NR_HEADS);
+}
+
static void memslot_rmap_free(struct kvm_memory_slot *slot)
{
+ unsigned long j, nr_chunks;
int i;
for (i = 0; i < KVM_NR_PAGE_SIZES; ++i) {
- vfree(slot->arch.rmap[i]);
+ if (!slot->arch.rmap[i])
+ continue;
+
+ nr_chunks = memslot_rmap_nr_chunks(slot, slot->npages, i + 1);
+ for (j = 0; j < nr_chunks; j++)
+ free_page((unsigned long)slot->arch.rmap[i][j]);
+
+ kvfree(slot->arch.rmap[i]);
slot->arch.rmap[i] = NULL;
}
}
@@ -10171,13 +10186,12 @@ int memslot_rmap_alloc(struct kvm_memory_slot *slot, unsigned long npages)
int i;
for (i = 0; i < KVM_NR_PAGE_SIZES; ++i) {
- int level = i + 1;
- int lpages = __kvm_mmu_slot_lpages(slot, npages, level);
+ unsigned long nr_chunks = memslot_rmap_nr_chunks(slot, npages, i + 1);
if (slot->arch.rmap[i])
continue;
- slot->arch.rmap[i] = __vcalloc(lpages, sz, GFP_KERNEL_ACCOUNT);
+ slot->arch.rmap[i] = kvcalloc(nr_chunks, sz, GFP_KERNEL_ACCOUNT);
if (!slot->arch.rmap[i]) {
memslot_rmap_free(slot);
return -ENOMEM;
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [RFC PATCH 4/5] KVM: selftests: Declare __tdp_map() in the x86 processor header
2026-11-04 0:53 [RFC PATCH 0/5] KVM: x86/mmu: Allocate memslot rmaps on demand Fred Griffoul
` (2 preceding siblings ...)
2026-11-04 0:53 ` [RFC PATCH 3/5] KVM: x86/mmu: Allocate memslot rmaps in on-demand chunks Fred Griffoul
@ 2026-11-04 0:53 ` Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 5/5] KVM: selftests: Test dirty logging of sparse nested mappings Fred Griffoul
4 siblings, 0 replies; 7+ messages in thread
From: Fred Griffoul @ 2026-11-04 0:53 UTC (permalink / raw)
To: Sean Christopherson, Paolo Bonzini
Cc: Mushahid Hussain, James Houghton, nh-open-source, kvm,
linux-kernel
From: Fred Griffoul <fgriffo@amazon.co.uk>
Tests can only map L2 memory with 4 KiB or 1 GiB pages in the TDP page
tables L1 uses for L2, because only tdp_map() and tdp_identity_map_1g()
are declared. Declare __tdp_map(), which takes the page size, so that
tests can also use 2 MiB pages.
Signed-off-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
tools/testing/selftests/kvm/include/x86/processor.h | 1 +
1 file changed, 1 insertion(+)
diff --git a/tools/testing/selftests/kvm/include/x86/processor.h b/tools/testing/selftests/kvm/include/x86/processor.h
index 5898a277a2e0..66293e448ab2 100644
--- a/tools/testing/selftests/kvm/include/x86/processor.h
+++ b/tools/testing/selftests/kvm/include/x86/processor.h
@@ -1613,6 +1613,7 @@ void virt_map_level(struct kvm_vm *vm, gva_t gva, gpa_t gpa,
void vm_enable_tdp(struct kvm_vm *vm);
bool kvm_cpu_has_tdp(void);
+void __tdp_map(struct kvm_vm *vm, gpa_t l2_gpa, gpa_t gpa, u64 size, int level);
void tdp_map(struct kvm_vm *vm, gpa_t l2_gpa, gpa_t gpa, u64 size);
void tdp_identity_map_default_memslots(struct kvm_vm *vm);
void tdp_identity_map_1g(struct kvm_vm *vm, u64 addr, u64 size);
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [RFC PATCH 5/5] KVM: selftests: Test dirty logging of sparse nested mappings
2026-11-04 0:53 [RFC PATCH 0/5] KVM: x86/mmu: Allocate memslot rmaps on demand Fred Griffoul
` (3 preceding siblings ...)
2026-11-04 0:53 ` [RFC PATCH 4/5] KVM: selftests: Declare __tdp_map() in the x86 processor header Fred Griffoul
@ 2026-11-04 0:53 ` Fred Griffoul
2026-11-04 1:10 ` sashiko-bot
4 siblings, 1 reply; 7+ messages in thread
From: Fred Griffoul @ 2026-11-04 0:53 UTC (permalink / raw)
To: Sean Christopherson, Paolo Bonzini
Cc: Mushahid Hussain, James Houghton, nh-open-source, kvm,
linux-kernel
From: Fred Griffoul <fgriffo@amazon.co.uk>
With memslot rmaps allocated in chunks, slot walks skip unallocated
chunks. A walk that skips too far leaves a mapped page writable after
dirty logging write-protects the memslot, and L2's next write to that
page is missing from the dirty log.
Add a test in which L2 writes pages at chunk boundaries and in a chunk
surrounded by unallocated ones, and checks that exactly the pages it
writes after dirty logging is enabled are reported dirty. A second
pass maps L2 with 2 MiB pages and checks that eager page splitting
splits them and logs the resulting 4 KiB SPTEs. It needs four free
2 MiB hugepages and eager page splitting, and is skipped otherwise.
Signed-off-by: Fred Griffoul <fgriffo@amazon.co.uk>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../kvm/x86/nested_dirty_log_sparse_test.c | 275 ++++++++++++++++++
2 files changed, 276 insertions(+)
create mode 100644 tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 17a4256e9081..61d88e0dd0ca 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -99,6 +99,7 @@ TEST_GEN_PROGS_x86 += x86/monitor_mwait_test
TEST_GEN_PROGS_x86 += x86/msrs_test
TEST_GEN_PROGS_x86 += x86/nested_close_kvm_test
TEST_GEN_PROGS_x86 += x86/nested_dirty_log_test
+TEST_GEN_PROGS_x86 += x86/nested_dirty_log_sparse_test
TEST_GEN_PROGS_x86 += x86/nested_emulation_test
TEST_GEN_PROGS_x86 += x86/nested_exceptions_test
TEST_GEN_PROGS_x86 += x86/nested_invalid_cr3_test
diff --git a/tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c b/tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c
new file mode 100644
index 000000000000..bc1c6b8760c9
--- /dev/null
+++ b/tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Dirty logging of memory that L2 maps sparsely.
+ *
+ * KVM allocates shadow MMU rmaps in chunks that cover 2 MiB of guest memory
+ * at the 4 KiB level, and only for chunks that hold a mapped gfn. L2 writes
+ * pages at both ends of a chunk, in the next chunk, and in chunks separated
+ * by unallocated ones. Enabling dirty logging write-protects the memslot by
+ * walking its rmaps, so a mapped page that the walk misses stays writable,
+ * and L2's next write to it is missing from the dirty log.
+ *
+ * A second test maps the memory with 2 MiB pages in L1's TDP page tables and
+ * verifies that eager page splitting splits the nested huge pages when
+ * dirty logging is enabled, and that the 4 KiB SPTEs it creates are logged.
+ */
+#include <stdio.h>
+#include <stdlib.h>
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/sizes.h>
+
+#include "test_util.h"
+#include "kvm_util.h"
+#include "processor.h"
+#include "svm_util.h"
+#include "vmx.h"
+
+#define TEST_MEM_SLOT 1
+#define TEST_MEM_BASE 0xc0000000ULL
+
+/* 64 MiB, i.e. 32 rmap chunks at the 4 KiB level. */
+#define SPARSE_MEM_PAGES (SZ_64M / PAGE_SIZE)
+
+/* 4 huge pages for the eager page splitting test. */
+#define HUGE_MEM_PAGES (4 * SZ_2M / PAGE_SIZE)
+#define HUGE_PAGES_PER_2M (SZ_2M / PAGE_SIZE)
+
+/*
+ * Page indices L2 writes in the sparse test: both ends of the first two
+ * chunks, both ends of a chunk surrounded by unallocated chunks, and the
+ * last page of the memslot.
+ */
+static const u64 sparse_pages[] = { 0, 511, 512, 1023, 8192, 8703,
+ SPARSE_MEM_PAGES - 1 };
+
+#define NR_SPARSE_PAGES ARRAY_SIZE(sparse_pages)
+
+enum test_stage {
+ STAGE_POPULATE = 1, /* L2 maps the pages, dirty logging is off */
+ STAGE_ALL, /* L2 rewrites every page */
+ STAGE_SUBSET, /* L2 rewrites every other page */
+};
+
+static bool huge_test;
+
+static void l2_write(u64 page, u64 val)
+{
+ WRITE_ONCE(*(u64 *)(TEST_MEM_BASE + page * PAGE_SIZE), val);
+}
+
+static u64 nr_test_pages(void)
+{
+ return huge_test ? HUGE_MEM_PAGES / HUGE_PAGES_PER_2M : NR_SPARSE_PAGES;
+}
+
+/* One page in each huge page in the huge test, the sparse pages otherwise. */
+static u64 test_page(u64 i)
+{
+ return huge_test ? i * HUGE_PAGES_PER_2M + 1 : sparse_pages[i];
+}
+
+static void l2_guest_code(void)
+{
+ u64 i;
+
+ for (i = 0; i < nr_test_pages(); i++)
+ l2_write(test_page(i), STAGE_POPULATE);
+ GUEST_SYNC(STAGE_POPULATE);
+
+ for (i = 0; i < nr_test_pages(); i++)
+ l2_write(test_page(i), STAGE_ALL);
+ GUEST_SYNC(STAGE_ALL);
+
+ for (i = 0; i < nr_test_pages(); i += 2)
+ l2_write(test_page(i), STAGE_SUBSET);
+ GUEST_SYNC(STAGE_SUBSET);
+
+ vmcall();
+}
+
+static void l1_vmx_code(struct vmx_pages *vmx)
+{
+ prepare_for_vmx_operation(vmx);
+ load_vmcs(vmx);
+ prepare_vmcs(vmx, l2_guest_code);
+
+ vmlaunch();
+ GUEST_ASSERT_EQ(vmread(VM_EXIT_REASON), EXIT_REASON_VMCALL);
+ GUEST_DONE();
+}
+
+static void l1_svm_code(struct svm_test_data *svm)
+{
+ generic_svm_setup(svm, l2_guest_code);
+
+ run_guest(svm->vmcb, svm->vmcb_gpa);
+ GUEST_ASSERT_EQ(svm->vmcb->control.exit_code, SVM_EXIT_VMMCALL);
+ GUEST_DONE();
+}
+
+static void l1_guest_code(void *data)
+{
+ if (this_cpu_has(X86_FEATURE_VMX))
+ l1_vmx_code(data);
+ else
+ l1_svm_code(data);
+}
+
+/* Returns true if L2 wrote @page in @stage. */
+static bool page_written(u64 page, enum test_stage stage)
+{
+ u64 i;
+
+ for (i = 0; i < nr_test_pages(); i++)
+ if (test_page(i) == page)
+ return stage != STAGE_SUBSET || !(i % 2);
+
+ return false;
+}
+
+static void check_dirty_log(struct kvm_vm *vm, u64 npages,
+ enum test_stage stage, unsigned long *bmap)
+{
+ u64 page;
+
+ kvm_vm_get_dirty_log(vm, TEST_MEM_SLOT, bmap);
+
+ for (page = 0; page < npages; page++) {
+ bool written = page_written(page, stage);
+
+ TEST_ASSERT(test_bit(page, bmap) == written,
+ "Stage %d: page %lu incorrectly reported %s",
+ stage, page, written ? "clean" : "dirty");
+ if (written)
+ TEST_ASSERT_EQ(*(u64 *)addr_gpa2hva(vm, TEST_MEM_BASE + page * PAGE_SIZE),
+ stage);
+ }
+}
+
+static void test_dirty_log(bool nested_tdp, bool huge)
+{
+ u64 npages = huge ? HUGE_MEM_PAGES : SPARSE_MEM_PAGES;
+ enum vm_mem_backing_src_type src;
+ u64 pages_2m = 0, pages_4k = 0;
+ gva_t nested_gva = 0;
+ struct kvm_vcpu *vcpu;
+ unsigned long *bmap;
+ struct kvm_vm *vm;
+ struct ucall uc;
+
+ pr_info("Nested TDP: %s, L1 TDP page size: %s\n",
+ nested_tdp ? "enabled" : "disabled", huge ? "2M" : "4K");
+
+ /* Room for L1's page tables and TDP page tables of the test memory. */
+ vm = __vm_create_with_one_vcpu(&vcpu, 2 * (npages / 512 + 16),
+ l1_guest_code);
+ if (nested_tdp)
+ vm_enable_tdp(vm);
+
+ if (kvm_cpu_has(X86_FEATURE_VMX))
+ vcpu_alloc_vmx(vm, &nested_gva);
+ else
+ vcpu_alloc_svm(vm, &nested_gva);
+ vcpu_args_set(vcpu, 1, nested_gva);
+
+ src = huge ? VM_MEM_SRC_ANONYMOUS_HUGETLB : VM_MEM_SRC_ANONYMOUS;
+ vm_userspace_mem_region_add(vm, src, TEST_MEM_BASE, TEST_MEM_SLOT,
+ npages, 0);
+ virt_map(vm, TEST_MEM_BASE, TEST_MEM_BASE, npages);
+
+ if (nested_tdp) {
+ tdp_identity_map_default_memslots(vm);
+ __tdp_map(vm, TEST_MEM_BASE, TEST_MEM_BASE, npages * PAGE_SIZE,
+ huge ? PG_LEVEL_2M : PG_LEVEL_4K);
+ }
+
+ huge_test = huge;
+ sync_global_to_guest(vm, huge_test);
+
+ bmap = bitmap_zalloc(npages);
+
+ for (;;) {
+ vcpu_run(vcpu);
+ TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_IO);
+
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ /* NOT REACHED */
+ case UCALL_SYNC:
+ break;
+ case UCALL_DONE:
+ goto done;
+ default:
+ TEST_FAIL("Unknown ucall %lu", uc.cmd);
+ }
+
+ if (uc.args[1] != STAGE_POPULATE) {
+ check_dirty_log(vm, npages, uc.args[1], bmap);
+ continue;
+ }
+
+ if (huge) {
+ pages_2m = vm_get_stat(vm, pages_2m);
+ pages_4k = vm_get_stat(vm, pages_4k);
+ }
+
+ /* Write-protect the memslot by walking its rmaps. */
+ vm_mem_region_set_flags(vm, TEST_MEM_SLOT, KVM_MEM_LOG_DIRTY_PAGES);
+
+ /*
+ * Only L2 maps the test memslot, so eager page splitting must
+ * have split exactly the nested huge pages L2 populated.
+ */
+ if (huge) {
+ u64 nr_huge = npages / HUGE_PAGES_PER_2M;
+ u64 split = pages_2m - vm_get_stat(vm, pages_2m);
+
+ TEST_ASSERT(split == nr_huge,
+ "Split %lu nested 2M pages, expected %lu",
+ split, nr_huge);
+ TEST_ASSERT_EQ(vm_get_stat(vm, pages_4k),
+ pages_4k + nr_huge * HUGE_PAGES_PER_2M);
+ }
+ }
+
+done:
+ free(bmap);
+ kvm_vm_free(vm);
+}
+
+static u64 free_2m_hugepages(void)
+{
+ u64 nr = 0;
+ FILE *f;
+
+ f = fopen("/sys/kernel/mm/hugepages/hugepages-2048kB/free_hugepages", "r");
+ if (!f)
+ return 0;
+ if (fscanf(f, "%lu", &nr) != 1)
+ nr = 0;
+ fclose(f);
+
+ return nr;
+}
+
+int main(int argc, char *argv[])
+{
+ TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_VMX) || kvm_cpu_has(X86_FEATURE_SVM));
+
+ test_dirty_log(/*nested_tdp=*/false, /*huge=*/false);
+
+ if (!kvm_cpu_has_tdp())
+ return 0;
+
+ test_dirty_log(/*nested_tdp=*/true, /*huge=*/false);
+
+ if (free_2m_hugepages() >= HUGE_MEM_PAGES / HUGE_PAGES_PER_2M &&
+ get_kvm_param_bool("tdp_mmu") && get_kvm_param_bool("eager_page_split"))
+ test_dirty_log(/*nested_tdp=*/true, /*huge=*/true);
+ else
+ pr_info("Skipping eager page splitting test, needs 4 free 2M hugepages, tdp_mmu and eager_page_split\n");
+
+ return 0;
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [RFC PATCH 5/5] KVM: selftests: Test dirty logging of sparse nested mappings
2026-11-04 0:53 ` [RFC PATCH 5/5] KVM: selftests: Test dirty logging of sparse nested mappings Fred Griffoul
@ 2026-11-04 1:10 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-04 1:10 UTC (permalink / raw)
To: Fred Griffoul; +Cc: kvm
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] KVM: selftests: test_dirty_log() uses default HugeTLB pool instead of 2M
- [Low] KVM: selftests: free_2m_hugepages() duplicates get_free_hugepages()
- [Low] KVM: selftests: test_dirty_log() pre-divides extra_mem_pages by 512
--
commit f746099353da91d06474f4e326f2f464849f22eb
Author: Fred Griffoul <fgriffo@amazon.co.uk>
KVM: selftests: Test dirty logging of sparse nested mappings
This commit adds an x86 KVM selftest that verifies dirty logging of sparsely
mapped L2 memory across shadow MMU rmap chunk boundaries, as well as eager
page splitting of 2 MiB nested TDP mappings when dirty logging is enabled.
> diff --git a/tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c b/tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c
> new file mode 100644
> index 0000000000000..bc1c6b8760c96
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/x86/nested_dirty_log_sparse_test.c
> @@ -0,0 +1,275 @@
[ ... ]
> +static void test_dirty_log(bool nested_tdp, bool huge)
> +{
> + u64 npages = huge ? HUGE_MEM_PAGES : SPARSE_MEM_PAGES;
> + enum vm_mem_backing_src_type src;
> + u64 pages_2m = 0, pages_4k = 0;
> + gva_t nested_gva = 0;
> + struct kvm_vcpu *vcpu;
> + unsigned long *bmap;
> + struct kvm_vm *vm;
> + struct ucall uc;
> +
> + pr_info("Nested TDP: %s, L1 TDP page size: %s\n",
> + nested_tdp ? "enabled" : "disabled", huge ? "2M" : "4K");
> +
> + /* Room for L1's page tables and TDP page tables of the test memory. */
> + vm = __vm_create_with_one_vcpu(&vcpu, 2 * (npages / 512 + 16),
> + l1_guest_code);
[Severity: Low]
Does passing 2 * (npages / 512 + 16) as extra_mem_pages divide npages by 512
twice?
__vm_create_with_one_vcpu() passes extra_mem_pages to vm_nr_pages_required(),
which already divides extra_mem_pages by PTES_PER_MIN_PAGE (512):
tools/testing/selftests/kvm/lib/kvm_util.c:vm_nr_pages_required() {
...
nr_pages += (nr_pages + extra_mem_pages) / PTES_PER_MIN_PAGE * 2;
...
}
Because npages is pre-divided by 512 here, extra_mem_pages is 96 (or 40 when
huge is true), so (nr_pages + extra_mem_pages) / 512 evaluates to 1 and adds
no extra page-table pages beyond the default. Should this pass 2 * npages
instead?
> + if (nested_tdp)
> + vm_enable_tdp(vm);
> +
> + if (kvm_cpu_has(X86_FEATURE_VMX))
> + vcpu_alloc_vmx(vm, &nested_gva);
> + else
> + vcpu_alloc_svm(vm, &nested_gva);
> + vcpu_args_set(vcpu, 1, nested_gva);
> +
> + src = huge ? VM_MEM_SRC_ANONYMOUS_HUGETLB : VM_MEM_SRC_ANONYMOUS;
> + vm_userspace_mem_region_add(vm, src, TEST_MEM_BASE, TEST_MEM_SLOT,
> + npages, 0);
[Severity: Medium]
Should src use VM_MEM_SRC_ANONYMOUS_HUGETLB_2MB instead of
VM_MEM_SRC_ANONYMOUS_HUGETLB when huge is true?
In main(), the test checks specifically for four free 2 MiB hugepages in
/sys/kernel/mm/hugepages/hugepages-2048kB/free_hugepages and sizes the region
to 8 MiB (HUGE_MEM_PAGES).
However, VM_MEM_SRC_ANONYMOUS_HUGETLB uses the system default HugeTLB page
size without MAP_HUGE_2MB:
tools/testing/selftests/kvm/lib/test_util.c:get_backing_src_pagesz() {
switch (i) {
...
case VM_MEM_SRC_ANONYMOUS_HUGETLB:
case VM_MEM_SRC_SHARED_HUGETLB:
return get_def_hugetlb_pagesz();
default:
return MAP_HUGE_PAGE_SIZE(flag);
}
}
On a host booted with default_hugepagesz=1G that has 2 MiB hugepages allocated
in the 2048kB pool, get_def_hugetlb_pagesz() will check the 1 GiB pool in
/proc/meminfo and either skip the test if HugePages_Total is 0, or return 1
GiB and cause __kvm_mmap() in vm_mem_add() to fail with EINVAL because the 8
MiB region is not 1 GiB aligned.
[ ... ]
> +static u64 free_2m_hugepages(void)
> +{
> + u64 nr = 0;
> + FILE *f;
> +
> + f = fopen("/sys/kernel/mm/hugepages/hugepages-2048kB/free_hugepages", "r");
> + if (!f)
> + return 0;
> + if (fscanf(f, "%lu", &nr) != 1)
> + nr = 0;
> + fclose(f);
> +
> + return nr;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> + TEST_REQUIRE(kvm_cpu_has(X86_FEATURE_VMX) || kvm_cpu_has(X86_FEATURE_SVM));
> +
> + test_dirty_log(/*nested_tdp=*/false, /*huge=*/false);
> +
> + if (!kvm_cpu_has_tdp())
> + return 0;
> +
> + test_dirty_log(/*nested_tdp=*/true, /*huge=*/false);
> +
> + if (free_2m_hugepages() >= HUGE_MEM_PAGES / HUGE_PAGES_PER_2M &&
> + get_kvm_param_bool("tdp_mmu") && get_kvm_param_bool("eager_page_split"))
> + test_dirty_log(/*nested_tdp=*/true, /*huge=*/true);
[Severity: Low]
This isn't a bug, but test_util.h already provides get_free_hugepages() in
tools/testing/selftests/kvm/lib/test_util.c, which reads the same sysfs file
and returns the free bytes:
tools/testing/selftests/kvm/lib/test_util.c:get_free_hugepages() {
char path[128];
size_t free;
snprintf(path, sizeof(path),
"/sys/kernel/mm/hugepages/hugepages-%zukB/free_hugepages",
page_size >> 10);
if (!test_sysfs_path(path))
return 0;
free = get_sysfs_val(path);
return free * page_size;
}
Could main() use get_free_hugepages(SZ_2M) >= HUGE_MEM_PAGES * PAGE_SIZE
instead of defining free_2m_hugepages()?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261104005331.50600-1-griffoul@gmail.com?part=5
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-11-04 1:10 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-04 0:53 [RFC PATCH 0/5] KVM: x86/mmu: Allocate memslot rmaps on demand Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 1/5] KVM: x86/mmu: Walk memslot rmaps by index Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 2/5] KVM: x86: Count rmaps per level in a debugfs helper Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 3/5] KVM: x86/mmu: Allocate memslot rmaps in on-demand chunks Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 4/5] KVM: selftests: Declare __tdp_map() in the x86 processor header Fred Griffoul
2026-11-04 0:53 ` [RFC PATCH 5/5] KVM: selftests: Test dirty logging of sparse nested mappings Fred Griffoul
2026-11-04 1:10 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox