* [PATCH] KVM: riscv: selftests: Add G-stage dirty logging transition test
@ 2026-10-12 15:30 SeungJu Cheon
0 siblings, 0 replies; only message in thread
From: SeungJu Cheon @ 2026-10-12 15:30 UTC (permalink / raw)
To: Anup Patel, Atish Patra
Cc: Paolo Bonzini, Sean Christopherson, Shuah Khan, Paul Walmsley,
Palmer Dabbelt, Albert Ou, Alexandre Ghiti, Andrew Jones,
Fuad Tabba, Wang Yechao, Wu Fei, kvm-riscv, kvm, linux-riscv,
linux-kselftest, linux-kernel, SeungJu Cheon
Add a selftest to verify 4K dirty tracking and data integrity across
G-stage hugepage splitting and dirty logging transitions. Since KVM
RISC-V cannot track 4K writes inside a 2M leaf, an exact dirty bitmap
also shows that the huge mapping was split.
Cover single and repeated transitions, manual dirty log protection,
and a regression for commit 0d9dc20842db ("RISC-V: KVM: Fix skip of
valid pages in kvm_riscv_gstage_wp_range") using an unaligned memslot.
Signed-off-by: SeungJu Cheon <suunj1331@gmail.com>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../kvm/riscv/gstage_dirty_log_transitions.c | 412 ++++++++++++++++++
2 files changed, 413 insertions(+)
create mode 100644 tools/testing/selftests/kvm/riscv/gstage_dirty_log_transitions.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 4cfb6385495..4fe50fceb5e 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -225,6 +225,7 @@ TEST_GEN_PROGS_s390 += pre_fault_memory_test
TEST_GEN_PROGS_riscv = $(TEST_GEN_PROGS_COMMON)
TEST_GEN_PROGS_riscv += riscv/sbi_pmu_test
+TEST_GEN_PROGS_riscv += riscv/gstage_dirty_log_transitions
TEST_GEN_PROGS_riscv += riscv/ebreak_test
TEST_GEN_PROGS_riscv += access_tracking_perf_test
TEST_GEN_PROGS_riscv += arch_timer
diff --git a/tools/testing/selftests/kvm/riscv/gstage_dirty_log_transitions.c b/tools/testing/selftests/kvm/riscv/gstage_dirty_log_transitions.c
new file mode 100644
index 00000000000..f518cf3cbde
--- /dev/null
+++ b/tools/testing/selftests/kvm/riscv/gstage_dirty_log_transitions.c
@@ -0,0 +1,412 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * gstage_dirty_log_transitions - Exercise G-stage huge-page/page
+ * granularity changes caused by dirty logging, and verify that
+ * dirty tracking and guest memory contents remain correct.
+ *
+ * With hugetlb backing and a 2M-aligned slot, G-stage faults install
+ * 2M mappings while dirty logging is disabled. Enabling dirty logging
+ * requires 4K-granularity tracking, so KVM splits existing huge
+ * mappings either eagerly when logging is enabled (eager_page_split=1)
+ * or on the first write fault (eager_page_split=0).
+ *
+ * Since KVM RISC-V cannot track individual 4K writes within a 2M
+ * leaf, a dirty bitmap that identifies exactly the written 4K pages
+ * provides evidence that the huge mapping was split into 4K mappings.
+ *
+ * The asserted properties are that the guest completes the sequence,
+ * every KVM_RUN succeeds, the dirty bitmap identifies exactly the
+ * 4K pages written by the guest, and guest data is preserved across
+ * the transitions.
+ *
+ * Scenario 1 - Transitions on aligned blocks:
+ * Fault in NR_BLOCKS hugetlb-backed 2M blocks with dirty logging
+ * disabled, then enable logging and write selected 4K pages in
+ * each block. Verify the dirty bitmap, disable logging, and write
+ * again while checking that guest data remains intact.
+ *
+ * Scenario 2 - Repeated transitions:
+ * Repeat the Scenario 1 sequence over several rounds on the same
+ * blocks, verifying the dirty bitmap and guest data after each
+ * round.
+ *
+ * Scenario 3 - Manual dirty log protect:
+ * Repeat the transitions with KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2
+ * enabled. After each GET_DIRTY_LOG, KVM_CLEAR_DIRTY_LOG clears
+ * the reported dirty bits and re-protects the corresponding pages.
+ * The guest then writes a second set of pages, which must be
+ * reported independently. That set overlaps the first in one page,
+ * so the second bitmap also verifies that the cleared pages were
+ * write-protected again. Skipped if the capability is unavailable.
+ *
+ * Scenario 4 - Unaligned slot with an empty leading block:
+ * Regression test for commit 0d9dc20842db ("RISC-V: KVM: Fix skip of
+ * valid pages in kvm_riscv_gstage_wp_range"). The slot starts 4K
+ * past a 2M boundary and the first block is never touched.
+ * The next block uses 4K anonymous backing to ensure that it is
+ * mapped with 4K leaves.
+ *
+ * Enabling dirty logging walks from the unaligned start, encounters
+ * the empty block, and must resume at the next 2M boundary. The
+ * broken walker advanced by 2M from the unaligned start, skipping
+ * write protection of the first 4K page in the next block. A guest
+ * write to that page then went unreported. This scenario does not
+ * require hugepages.
+ */
+#include <linux/bitmap.h>
+#include <linux/sizes.h>
+#include <sys/mman.h>
+
+#include "kvm_util.h"
+#include "processor.h"
+#include "test_util.h"
+#include "ucall_common.h"
+
+#define DATA_SLOT 1
+#define TEST_GVA 0xc0000000UL
+#define BLOCK_SIZE SZ_2M
+#define PAGES_PER_BLOCK (BLOCK_SIZE / SZ_4K)
+
+/* Exercise dirty-log transitions across multiple 2M blocks. */
+#define NR_BLOCKS 16
+
+/* Two page sets with one shared page to exercise repeated dirty writes. */
+static const unsigned int sel_a[] = { 0, 3, 257, PAGES_PER_BLOCK - 1 };
+static const unsigned int sel_b[] = { 1, 3, 128, 400 };
+#define NR_SEL_A ARRAY_SIZE(sel_a)
+#define NR_SEL_B ARRAY_SIZE(sel_b)
+
+#define PATTERN(blk, pg) (0x1000000000000000ULL | ((u64)(blk) << 32) | (pg))
+#define PATTERN_LOGGED 0x5a5a5a5a5a5a5a5aULL
+#define PATTERN_LOGGED2 0x5b5b5b5b5b5b5b5bULL
+#define PATTERN_AFTER 0xa5a5a5a5a5a5a5a5ULL
+
+/* Guest -> host stages. */
+#define STAGE_BLOCKS_READY 1
+#define STAGE_LOGGED_WRITES 2
+#define STAGE_LOGGED_WRITES2 3
+#define STAGE_ROUND_DONE 4
+
+static u64 *page_addr(u64 base, unsigned int blk, unsigned int pg)
+{
+ return (u64 *)(base + (u64)blk * BLOCK_SIZE + (u64)pg * SZ_4K);
+}
+
+static bool page_in_set(unsigned int pg, const unsigned int *sel, unsigned int nsel)
+{
+ unsigned int i;
+
+ for (i = 0; i < nsel; i++)
+ if (sel[i] == pg)
+ return true;
+ return false;
+}
+
+static void guest_fill_blocks(u64 base, u64 nblocks)
+{
+ unsigned int blk, pg;
+
+ for (blk = 0; blk < nblocks; blk++)
+ for (pg = 0; pg < PAGES_PER_BLOCK; pg++)
+ WRITE_ONCE(*page_addr(base, blk, pg), PATTERN(blk, pg));
+}
+
+static void guest_write_selected(u64 base, u64 nblocks,
+ const unsigned int *sel, unsigned int nsel, u64 val)
+{
+ unsigned int blk, i;
+
+ for (blk = 0; blk < nblocks; blk++)
+ for (i = 0; i < nsel; i++)
+ WRITE_ONCE(*page_addr(base, blk, sel[i]), val);
+}
+
+static void guest_restore_selected(u64 base, u64 nblocks,
+ const unsigned int *sel, unsigned int nsel)
+{
+ unsigned int blk, i;
+
+ for (blk = 0; blk < nblocks; blk++)
+ for (i = 0; i < nsel; i++)
+ WRITE_ONCE(*page_addr(base, blk, sel[i]),
+ PATTERN(blk, sel[i]));
+}
+
+static void guest_check_blocks(u64 base, u64 nblocks, u64 val_a, u64 val_b)
+{
+ unsigned int blk, pg;
+ u64 expect;
+
+ for (blk = 0; blk < nblocks; blk++) {
+ for (pg = 0; pg < PAGES_PER_BLOCK; pg++) {
+ if (page_in_set(pg, sel_b, NR_SEL_B))
+ expect = val_b;
+ else if (page_in_set(pg, sel_a, NR_SEL_A))
+ expect = val_a;
+ else
+ expect = PATTERN(blk, pg);
+ GUEST_ASSERT_EQ(READ_ONCE(*page_addr(base, blk, pg)), expect);
+ }
+ }
+}
+
+static void guest_one_round(u64 base, u64 nblocks)
+{
+ /* Logging on: first page set. */
+ guest_write_selected(base, nblocks, sel_a, NR_SEL_A, PATTERN_LOGGED);
+ GUEST_SYNC(STAGE_LOGGED_WRITES);
+
+ /* Host checked set A and reset dirty tracking for the next writes. */
+ guest_write_selected(base, nblocks, sel_b, NR_SEL_B, PATTERN_LOGGED2);
+ GUEST_SYNC(STAGE_LOGGED_WRITES2);
+
+ /* Host verified set B and disabled logging. */
+ guest_check_blocks(base, nblocks, PATTERN_LOGGED, PATTERN_LOGGED2);
+ guest_write_selected(base, nblocks, sel_a, NR_SEL_A, PATTERN_AFTER);
+ guest_write_selected(base, nblocks, sel_b, NR_SEL_B, PATTERN_AFTER);
+ guest_check_blocks(base, nblocks, PATTERN_AFTER, PATTERN_AFTER);
+
+ /* Restore the base pattern while logging is still off. */
+ guest_restore_selected(base, nblocks, sel_a, NR_SEL_A);
+ guest_restore_selected(base, nblocks, sel_b, NR_SEL_B);
+
+ GUEST_SYNC(STAGE_ROUND_DONE);
+}
+
+static void transitions_guest_code(u64 base, u64 nblocks, u64 nrounds)
+{
+ u64 r;
+
+ guest_fill_blocks(base, nblocks);
+ GUEST_SYNC(STAGE_BLOCKS_READY);
+
+ for (r = 0; r < nrounds; r++)
+ guest_one_round(base, nblocks);
+
+ GUEST_DONE();
+}
+
+static void expect_sync(struct kvm_vcpu *vcpu, u64 stage)
+{
+ struct ucall uc;
+
+ vcpu_run(vcpu);
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_SYNC:
+ TEST_ASSERT(uc.args[1] == stage, "Expected stage %lu, got %lu",
+ stage, uc.args[1]);
+ return;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ default:
+ TEST_FAIL("Unexpected ucall %lu (wanted stage %lu)", uc.cmd, stage);
+ }
+}
+
+static void expect_done(struct kvm_vcpu *vcpu)
+{
+ struct ucall uc;
+
+ vcpu_run(vcpu);
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_DONE:
+ return;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ default:
+ TEST_FAIL("Unexpected ucall %lu (wanted done)", uc.cmd);
+ }
+}
+
+static unsigned int count_dirty(const unsigned long *bmap, unsigned int nbits)
+{
+ unsigned int i, n = 0;
+
+ for (i = 0; i < nbits; i++)
+ if (test_bit(i, bmap))
+ n++;
+ return n;
+}
+
+static void check_selected_dirty(unsigned long *bmap, unsigned int nblocks,
+ const unsigned int *sel, unsigned int nsel)
+{
+ unsigned int blk, i, idx;
+
+ TEST_ASSERT_EQ(count_dirty(bmap, nblocks * PAGES_PER_BLOCK), nblocks * nsel);
+ for (blk = 0; blk < nblocks; blk++) {
+ for (i = 0; i < nsel; i++) {
+ idx = blk * PAGES_PER_BLOCK + sel[i];
+ TEST_ASSERT(test_bit(idx, bmap),
+ "Block %u page %u not dirty", blk, sel[i]);
+ }
+ }
+}
+
+static void run_transitions(unsigned int nblocks, unsigned int nrounds, bool manual)
+{
+ size_t npages = nblocks * PAGES_PER_BLOCK;
+ struct kvm_vcpu *vcpu;
+ unsigned long *bmap;
+ struct kvm_vm *vm;
+ unsigned int r;
+ u64 gpa;
+
+ vm = vm_create_with_one_vcpu(&vcpu, transitions_guest_code);
+ if (manual)
+ vm_enable_cap(vm, KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2,
+ KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE);
+
+ gpa = (vm_compute_max_gfn(vm) * vm->page_size) - nblocks * BLOCK_SIZE;
+ gpa = align_down(gpa, BLOCK_SIZE);
+
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS_HUGETLB_2MB, gpa,
+ DATA_SLOT, npages, 0);
+ virt_map(vm, TEST_GVA, gpa, npages);
+ vcpu_args_set(vcpu, 3, TEST_GVA, (u64)nblocks, (u64)nrounds);
+
+ bmap = bitmap_zalloc(npages);
+
+ /* Logging off: fault in hugetlb-backed blocks before enabling logging. */
+ expect_sync(vcpu, STAGE_BLOCKS_READY);
+
+ for (r = 0; r < nrounds; r++) {
+ vm_mem_region_set_flags(vm, DATA_SLOT, KVM_MEM_LOG_DIRTY_PAGES);
+
+ /* Writes under logging: split (eager or at fault) + 4K tracking. */
+ expect_sync(vcpu, STAGE_LOGGED_WRITES);
+
+ bitmap_zero(bmap, npages);
+ kvm_vm_get_dirty_log(vm, DATA_SLOT, bmap);
+ check_selected_dirty(bmap, nblocks, sel_a, NR_SEL_A);
+
+ if (manual)
+ kvm_vm_clear_dirty_log(vm, DATA_SLOT, bmap, 0, npages);
+
+ expect_sync(vcpu, STAGE_LOGGED_WRITES2);
+
+ bitmap_zero(bmap, npages);
+ kvm_vm_get_dirty_log(vm, DATA_SLOT, bmap);
+ check_selected_dirty(bmap, nblocks, sel_b, NR_SEL_B);
+
+ if (manual)
+ kvm_vm_clear_dirty_log(vm, DATA_SLOT, bmap, 0, npages);
+
+ vm_mem_region_set_flags(vm, DATA_SLOT, 0);
+
+ /* Logging off: writes must complete on the existing 4K leaves, data intact. */
+ expect_sync(vcpu, STAGE_ROUND_DONE);
+ }
+
+ expect_done(vcpu);
+
+ free(bmap);
+ kvm_vm_free(vm);
+}
+
+static void test_aligned_transitions(void)
+{
+ run_transitions(NR_BLOCKS, 1, false);
+}
+
+static void test_repeated_transitions(void)
+{
+ run_transitions(2, 4, false);
+}
+
+static void test_manual_protect_transitions(void)
+{
+ run_transitions(2, 2, true);
+}
+
+#define UNALIGNED_NPAGES (2 * PAGES_PER_BLOCK)
+/* Guest offset of the next 2M boundary past the unaligned slot start. */
+#define UNALIGNED_BLOCK1_OFF (BLOCK_SIZE - SZ_4K)
+
+static void unaligned_guest_code(u64 base)
+{
+ u64 *first = (u64 *)(base + UNALIGNED_BLOCK1_OFF);
+ unsigned int pg;
+
+ /* Fault in the second 2M block only, as 4K leaves. The first stays empty. */
+ for (pg = 0; pg < PAGES_PER_BLOCK; pg++)
+ WRITE_ONCE(*(first + pg * (SZ_4K / sizeof(u64))), 0x1);
+ GUEST_SYNC(STAGE_BLOCKS_READY);
+
+ /* Logging on: this is the page the broken walker never write-protected. */
+ WRITE_ONCE(*first, 0x2);
+ GUEST_SYNC(STAGE_LOGGED_WRITES);
+
+ GUEST_DONE();
+}
+
+static void test_unaligned_slot(void)
+{
+ struct kvm_vcpu *vcpu;
+ unsigned long *bmap;
+ struct kvm_vm *vm;
+ u64 gpa;
+
+ vm = vm_create_with_one_vcpu(&vcpu, unaligned_guest_code);
+
+ gpa = (vm_compute_max_gfn(vm) * vm->page_size) - 3 * BLOCK_SIZE;
+ gpa = align_down(gpa, BLOCK_SIZE) + SZ_4K;
+
+ /* Plain anonymous backing: the library disables THP on it. */
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, gpa, DATA_SLOT,
+ UNALIGNED_NPAGES, 0);
+ virt_map(vm, TEST_GVA, gpa, UNALIGNED_NPAGES);
+ vcpu_args_set(vcpu, 1, TEST_GVA);
+
+ bmap = bitmap_zalloc(UNALIGNED_NPAGES);
+
+ expect_sync(vcpu, STAGE_BLOCKS_READY);
+
+ /* wp_range() starts at the unaligned slot base and meets an empty 2M block. */
+ vm_mem_region_set_flags(vm, DATA_SLOT, KVM_MEM_LOG_DIRTY_PAGES);
+
+ expect_sync(vcpu, STAGE_LOGGED_WRITES);
+
+ kvm_vm_get_dirty_log(vm, DATA_SLOT, bmap);
+ TEST_ASSERT_EQ(count_dirty(bmap, UNALIGNED_NPAGES), 1);
+ TEST_ASSERT(test_bit(UNALIGNED_BLOCK1_OFF / SZ_4K, bmap),
+ "First page of the second block was not write-protected");
+
+ vm_mem_region_set_flags(vm, DATA_SLOT, 0);
+ expect_done(vcpu);
+
+ free(bmap);
+ kvm_vm_free(vm);
+}
+
+static bool hugepages_available(size_t bytes)
+{
+ void *mem = mmap(NULL, bytes, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB,
+ -1, 0);
+
+ if (mem == MAP_FAILED)
+ return false;
+ munmap(mem, bytes);
+ return true;
+}
+
+int main(void)
+{
+ if (hugepages_available(NR_BLOCKS * BLOCK_SIZE)) {
+ test_aligned_transitions();
+ test_repeated_transitions();
+ if (kvm_has_cap(KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2))
+ test_manual_protect_transitions();
+ else
+ ksft_print_msg("SKIP manual protect: capability unsupported\n");
+ } else {
+ ksft_print_msg("SKIP hugetlb scenarios: need %d reserved 2M hugepages\n",
+ NR_BLOCKS);
+ }
+
+ test_unaligned_slot();
+
+ ksft_print_msg("All ok!\n");
+ return 0;
+}
base-commit: bae99073549aba7c3d79685a4640a46aa5c55340
--
2.52.0
^ permalink raw reply [flat|nested] only message in thread
only message in thread, other threads:[~2026-10-12 15:30 UTC | newest]
Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-12 15:30 [PATCH] KVM: riscv: selftests: Add G-stage dirty logging transition test SeungJu Cheon
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®