summaryrefslogtreecommitdiff
path: root/tools/testing
diff options
context:
space:
mode:
authorPaolo Bonzini <pbonzini@redhat.com>2026-08-24 12:42:07 -0400
committerPaolo Bonzini <pbonzini@redhat.com>2026-08-24 12:42:07 -0400
commit76671054f9a1ff6abb976583cd8da37650acdc97 (patch)
tree38a80e6e9e919fd9646fc4ebb951bbc53565c6ea /tools/testing
parentd75b48460559423f8c38aafed7a9cdec91d26d57 (diff)
parentaa8e5dc6a7a2a1141ab40706a51010adcd0e57d2 (diff)
downloadlinux-76671054f9a1ff6abb976583cd8da37650acdc97.tar.gz
linux-76671054f9a1ff6abb976583cd8da37650acdc97.zip
Merge tag 'kvmarm-7.3' of https://git.kernel.org/pub/scm/linux/kernel/git/kvmarm/kvmarm into HEAD
KVM/arm64 changes for 7.3 - Add support for 'slot' based PMU events, paired with new UAPI that compels the user to select a specific PMU implementation - Lazy save/restore of vCPU state for pKVM, along with various fixes and cleanups to the management of vCPU state between the untrusted host and pKVM hypervisor - Disable traps of EL1 registers for nested hypervisors when FEAT_NV2p1 is present, guaranteeing that EL2-specific register bits are stateful in the EL1 counterpart - Leverage FEAT_NV3 to avoid unnecessary ERET/TLBI traps when the scope of those instructions remains 'in host' (i.e. L1 kernel/userspace) - Pile of fixes for the management of the VNCR pseudo-TLB, such as under-invalidations and races with concurrent TLBIs on other vCPUs - Consolidate the non-protected and pKVM view of ICH_VTR_EL2 to a runtime-patched constant, allowing the same data to be shared with pKVM prior to dropping host privileges - Considerable pile of LLM-assisted fixes around the shop but mostly in the VGIC, our in-kernel generator of bugs (and sometimes interrupts)
Diffstat (limited to 'tools/testing')
-rw-r--r--tools/testing/selftests/kvm/Makefile.kvm1
-rw-r--r--tools/testing/selftests/kvm/arm64/debug-exceptions.c41
-rw-r--r--tools/testing/selftests/kvm/arm64/get-reg-list.c3
-rw-r--r--tools/testing/selftests/kvm/arm64/stage2_block_transitions.c226
4 files changed, 271 insertions, 0 deletions
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index bb34e4f9c542..96bab7002d39 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -185,6 +185,7 @@ TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
TEST_GEN_PROGS_arm64 += arm64/set_id_regs
TEST_GEN_PROGS_arm64 += arm64/smccc_filter
+TEST_GEN_PROGS_arm64 += arm64/stage2_block_transitions
TEST_GEN_PROGS_arm64 += arm64/vcpu_width_config
TEST_GEN_PROGS_arm64 += arm64/vgic_init
TEST_GEN_PROGS_arm64 += arm64/vgic_irq
diff --git a/tools/testing/selftests/kvm/arm64/debug-exceptions.c b/tools/testing/selftests/kvm/arm64/debug-exceptions.c
index 3eb4b1b6682d..7dc5f0b4f6ad 100644
--- a/tools/testing/selftests/kvm/arm64/debug-exceptions.c
+++ b/tools/testing/selftests/kvm/arm64/debug-exceptions.c
@@ -527,6 +527,46 @@ void test_single_step_from_userspace(int test_cnt)
kvm_vm_free(vm);
}
+static void guest_code_wp(void)
+{
+ write_data = 'x';
+ GUEST_DONE();
+}
+
+/*
+ * A userspace hardware watchpoint (KVM_GUESTDBG_USE_HW) must fire and report
+ * the accessed address in debug.arch.far, exercising the watchpoint exit path.
+ */
+static void test_watchpoint_from_userspace(void)
+{
+ struct kvm_guest_debug debug = {};
+ struct kvm_vcpu *vcpu;
+ struct kvm_run *run;
+ struct kvm_vm *vm;
+
+ vm = vm_create_with_one_vcpu(&vcpu, guest_code_wp);
+ run = vcpu->run;
+
+ debug.control = KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW;
+ debug.arch.dbg_wcr[0] = DBGWCR_LEN8 | DBGWCR_RD | DBGWCR_WR |
+ DBGWCR_EL1 | DBGWCR_E;
+ /*
+ * BAS = 0xff (LEN8) requires a doubleword-aligned DBGWVR; FAR still
+ * reports the exact accessed byte.
+ */
+ debug.arch.dbg_wvr[0] = PC(write_data) & ~7UL;
+ vcpu_guest_debug_set(vcpu, &debug);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(run->exit_reason == KVM_EXIT_DEBUG,
+ "Expected KVM_EXIT_DEBUG, got %u", run->exit_reason);
+ TEST_ASSERT((u64)run->debug.arch.far == PC(write_data),
+ "Watchpoint FAR 0x%lx != accessed address 0x%lx",
+ (u64)run->debug.arch.far, PC(write_data));
+
+ kvm_vm_free(vm);
+}
+
/*
* Run debug testing using the various breakpoint#, watchpoint# and
* context-aware breakpoint# with the given ID_AA64DFR0_EL1 configuration.
@@ -600,6 +640,7 @@ int main(int argc, char *argv[])
test_guest_debug_exceptions_all(aa64dfr0);
test_single_step_from_userspace(ss_iteration);
+ test_watchpoint_from_userspace();
return 0;
}
diff --git a/tools/testing/selftests/kvm/arm64/get-reg-list.c b/tools/testing/selftests/kvm/arm64/get-reg-list.c
index 0a3a94c4cca1..533994687b5d 100644
--- a/tools/testing/selftests/kvm/arm64/get-reg-list.c
+++ b/tools/testing/selftests/kvm/arm64/get-reg-list.c
@@ -67,6 +67,7 @@ static struct feature_id_reg feat_id_regs[] = {
REG_FEAT(VDISR_EL2, ID_AA64PFR0_EL1, RAS, IMP),
REG_FEAT(VSESR_EL2, ID_AA64PFR0_EL1, RAS, IMP),
REG_FEAT(VNCR_EL2, ID_AA64MMFR4_EL1, NV_frac, NV2_ONLY),
+ REG_FEAT(NVHCR_EL2, ID_AA64MMFR4_EL1, NV_frac, NV3),
REG_FEAT(CNTHV_CTL_EL2, ID_AA64MMFR1_EL1, VH, IMP),
REG_FEAT(CNTHV_CVAL_EL2,ID_AA64MMFR1_EL1, VH, IMP),
REG_FEAT(ZCR_EL2, ID_AA64PFR0_EL1, SVE, IMP),
@@ -532,6 +533,7 @@ static __u64 base_regs[] = {
static __u64 pmu_regs[] = {
ARM64_SYS_REG(3, 0, 9, 14, 1), /* PMINTENSET_EL1 */
ARM64_SYS_REG(3, 0, 9, 14, 2), /* PMINTENCLR_EL1 */
+ ARM64_SYS_REG(3, 0, 9, 14, 6), /* PMMIR_EL1 */
ARM64_SYS_REG(3, 3, 9, 12, 0), /* PMCR_EL0 */
ARM64_SYS_REG(3, 3, 9, 12, 1), /* PMCNTENSET_EL0 */
ARM64_SYS_REG(3, 3, 9, 12, 2), /* PMCNTENCLR_EL0 */
@@ -770,6 +772,7 @@ static __u64 el2_regs[] = {
SYS_REG(SP_EL2),
SYS_REG(VDISR_EL2),
SYS_REG(VSESR_EL2),
+ SYS_REG(NVHCR_EL2),
};
static __u64 el2_e2h0_regs[] = {
diff --git a/tools/testing/selftests/kvm/arm64/stage2_block_transitions.c b/tools/testing/selftests/kvm/arm64/stage2_block_transitions.c
new file mode 100644
index 000000000000..5fd47f4ada1f
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/stage2_block_transitions.c
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ *
+ * stage2_block_transitions - Exercise stage-2 block/page granularity changes
+ * that dirty logging forces at fault time, and assert the guest completes.
+ *
+ * Both scenarios need the fault handler to allocate at fault time (a fresh
+ * mapping and/or page-table pages while holding mmu_lock), so a fault path
+ * that fails to stage that memory manifests as a KVM_RUN error or, worse, a
+ * host crash. The asserted property is host-agnostic: the guest runs the
+ * sequence to completion and every KVM_RUN succeeds. On a pKVM host, where a
+ * non-protected guest's stage-2 faults are serviced by the pkvm_pgtable_*()
+ * backend, the same sequences also guard that backend's fault-time staging.
+ *
+ * Scenario 1 - block collapse on dirty-logging disable:
+ * A write under dirty logging installs a 4K page; GET_DIRTY_LOG
+ * re-write-protects it; logging is disabled; a second write takes a
+ * permission fault that collapses the page into a hugetlb-backed block,
+ * which requires a fresh mapping object under mmu_lock.
+ *
+ * Scenario 2 - block split under dirty logging:
+ * Several hugetlb-backed blocks are faulted in as non-executable blocks,
+ * dirty logging is enabled (write-protect only), then the guest executes
+ * into each block. Each instruction fetch takes an execute permission
+ * fault that must split the block into pages during logging, draining
+ * page-table pages. Skipped on CTR_EL0.DIC hardware, where mappings are
+ * made executable eagerly and the execute fault never occurs.
+ */
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/mman.h>
+#include <linux/sizes.h>
+#include <sys/mman.h>
+
+#include <asm/sysreg.h>
+
+#include "kvm_util.h"
+#include "processor.h"
+#include "test_util.h"
+#include "ucall.h"
+
+#define DATA_SLOT 1
+#define TEST_GVA 0xc0000000UL
+#define BLOCK_SIZE SZ_2M
+
+/* AArch64 "ret" (ret x30): a self-contained, returnable executable payload. */
+#define RET_INSN 0xd65f03c0U
+
+/*
+ * A non-protected guest's per-VM stage-2 pool is seeded only with the PGD
+ * donation, which stage-2 init immediately consumes, so the page-table budget
+ * for a fault that does not top up is just the handful (~2x the stage-2 min
+ * pages) of memcache leftovers. Executing into this many distinct blocks
+ * demands far more than that budget: a fault path that tops up on every fault
+ * completes all of them, one that skips non-write faults runs out mid-sequence.
+ */
+#define NR_BLOCKS 16
+
+/* Scenario 2 guest -> host sync stages. */
+#define STAGE_SKIP_DIC 1
+#define STAGE_BLOCKS_READY 2
+
+static void collapse_guest_code(u64 gva)
+{
+ u64 *data = (u64 *)gva;
+
+ /* Under dirty logging: install a 4K writable page. */
+ WRITE_ONCE(*data, 0x1);
+ GUEST_SYNC(1);
+
+ /* Logging disabled: a permission fault collapses the page into a block. */
+ WRITE_ONCE(*data, 0x2);
+ GUEST_SYNC(2);
+
+ GUEST_DONE();
+}
+
+static void test_block_collapse(void)
+{
+ struct kvm_vcpu *vcpu;
+ unsigned long *bmap;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ size_t npages;
+ u64 gpa;
+
+ vm = vm_create_with_one_vcpu(&vcpu, collapse_guest_code);
+ npages = BLOCK_SIZE / vm->page_size;
+
+ gpa = (vm_compute_max_gfn(vm) * vm->page_size) - BLOCK_SIZE;
+ gpa = align_down(gpa, BLOCK_SIZE);
+
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS_HUGETLB_2MB, gpa,
+ DATA_SLOT, npages, KVM_MEM_LOG_DIRTY_PAGES);
+ virt_map(vm, TEST_GVA, gpa, npages);
+ vcpu_args_set(vcpu, 1, TEST_GVA);
+
+ bmap = bitmap_zalloc(BLOCK_SIZE / getpagesize());
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC && uc.args[1] == 1,
+ "Expected first sync, got cmd %lu arg %lu", uc.cmd, uc.args[1]);
+
+ /* GET_DIRTY_LOG re-write-protects the dirtied page; then stop logging. */
+ kvm_vm_get_dirty_log(vm, DATA_SLOT, bmap);
+ vm_mem_region_set_flags(vm, DATA_SLOT, 0);
+
+ /* The collapsing permission fault: a broken fault path faults here. */
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC && uc.args[1] == 2,
+ "Expected second sync, got cmd %lu arg %lu", uc.cmd, uc.args[1]);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE,
+ "Expected done, got cmd %lu", uc.cmd);
+
+ free(bmap);
+ kvm_vm_free(vm);
+}
+
+static void guest_sync_insn(u64 va)
+{
+ /* Make the just-written instruction coherent for execution (!DIC). */
+ asm volatile("dc cvau, %0\n"
+ "dsb ish\n"
+ "ic ivau, %0\n"
+ "dsb ish\n"
+ "isb\n"
+ :: "r" (va) : "memory");
+}
+
+static void split_guest_code(u64 base_gva, u64 nblocks)
+{
+ u64 i, va;
+
+ if (FIELD_GET(CTR_EL0_DIC_MASK, read_sysreg(ctr_el0))) {
+ GUEST_SYNC(STAGE_SKIP_DIC);
+ GUEST_DONE();
+ return;
+ }
+
+ /* Fault in each block (non-executable) and stage an executable payload. */
+ for (i = 0; i < nblocks; i++) {
+ va = base_gva + i * BLOCK_SIZE;
+ WRITE_ONCE(*(u32 *)va, RET_INSN);
+ guest_sync_insn(va);
+ }
+ GUEST_SYNC(STAGE_BLOCKS_READY);
+
+ /* Logging is now on: executing into each block splits it into pages. */
+ for (i = 0; i < nblocks; i++) {
+ va = base_gva + i * BLOCK_SIZE;
+ ((void (*)(void))va)();
+ }
+
+ GUEST_DONE();
+}
+
+static void test_exec_split_drain(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ size_t npages;
+ u64 gpa;
+
+ vm = vm_create_with_one_vcpu(&vcpu, split_guest_code);
+ npages = NR_BLOCKS * (BLOCK_SIZE / vm->page_size);
+
+ gpa = (vm_compute_max_gfn(vm) * vm->page_size) - NR_BLOCKS * 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, 2, TEST_GVA, (u64)NR_BLOCKS);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_SYNC,
+ "Expected sync, got cmd %lu", uc.cmd);
+ if (uc.args[1] == STAGE_SKIP_DIC) {
+ ksft_print_msg("SKIP block split: CTR_EL0.DIC == 1\n");
+ kvm_vm_free(vm);
+ return;
+ }
+ TEST_ASSERT(uc.args[1] == STAGE_BLOCKS_READY,
+ "Expected blocks-ready sync, got arg %lu", uc.args[1]);
+
+ /* Write-protect the blocks; the guest then splits them by executing. */
+ vm_mem_region_set_flags(vm, DATA_SLOT, KVM_MEM_LOG_DIRTY_PAGES);
+
+ vcpu_run(vcpu);
+ TEST_ASSERT(get_ucall(vcpu, &uc) == UCALL_DONE,
+ "Expected done, got cmd %lu", uc.cmd);
+
+ kvm_vm_free(vm);
+}
+
+/*
+ * The explicit-size hugetlb backing hard-fails region creation if the pages
+ * are not already reserved, so probe here and skip rather than abort. The
+ * peak reservation is scenario 2's; the two scenarios run and free in turn.
+ */
+static void require_hugepages(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)
+ ksft_exit_skip("Need %zu bytes of reserved 2M hugepages\n", bytes);
+ munmap(mem, bytes);
+}
+
+int main(void)
+{
+ require_hugepages(NR_BLOCKS * BLOCK_SIZE);
+
+ test_block_collapse();
+ test_exec_split_drain();
+
+ ksft_print_msg("All ok!\n");
+ return 0;
+}