mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [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®