[PATCH v2] KVM: arm64: selftests: Test VNCR translation invalidation
From: Itaru Kitayama
Date: Thu Oct 22 2026 - 04:15:53 EST
Add a standalone KVM selftest that checks VNCR accesses after L1
updates its stage-1 mapping and issues a TLBI.
Keep VNCR_EL2 unchanged while remapping its backing page A to B then
again to A. At each step, L2 reads and writes TPIDR_EL2, and L1
verifies that the accesses reach the selected page while leaving the
other page unchanged.
The test runs with L1-managed stage-2 translation disabled.
Signed-off-by: Itaru Kitayama <itaru.kitayama@xxxxxxxxxxx>
---
Changes in v2:
- Fix inverted page-frame and page-offset masks when mapping
the page-table descriptor, as reported by sashiko-bot
- Link to v1: https://patch.msgid.link/20261022-b4-vncr-mapping-test-v1-1-e9ad54d506be@xxxxxxxxxxx
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/nv_vncr_tlbi_test.c | 303 +++++++++++++++++++++
2 files changed, 304 insertions(+)
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index f43549a56f0b..9f0af6ef54ba 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -181,6 +181,7 @@ TEST_GEN_PROGS_arm64 += arm64/host_sve
TEST_GEN_PROGS_arm64 += arm64/hypercalls
TEST_GEN_PROGS_arm64 += arm64/external_aborts
TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
+TEST_GEN_PROGS_arm64 += arm64/nv_vncr_tlbi_test
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
diff --git a/tools/testing/selftests/kvm/arm64/nv_vncr_tlbi_test.c b/tools/testing/selftests/kvm/arm64/nv_vncr_tlbi_test.c
new file mode 100644
index 000000000000..283e03bb007b
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/nv_vncr_tlbi_test.c
@@ -0,0 +1,303 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Check VNCR translation invalidation through observable memory effects.
+ *
+ * L1 keeps VNCR_EL2 unchanged while remapping its virtual address:
+ *
+ * VNCR VA -> page A -> page B -> page A
+ *
+ * L2 reads and writes TPIDR_EL2 with HCR_EL2.{NV2,NV} == {1,1}.
+ * These accesses use the TPIDR_EL2 slot at VNCR offset 0x090.
+ *
+ * After each L2 HVC, L1 checks:
+ * - the value returned by the redirected register read;
+ * - the value written to the selected backing page;
+ * - that the other backing page remains unchanged.
+ *
+ * No L3 and no L1-managed stage-2 translation are needed.
+ */
+
+#include "kvm_util.h"
+#include "processor.h"
+#include "test_util.h"
+#include "ucall_common.h"
+
+#define VNCR_TPIDR_OFFSET 0x090
+
+#define SEED_A 0x1111222233334444ULL
+#define SEED_B 0x5555666677778888ULL
+#define WRITE_A 0xaaaabbbbccccddddULL
+#define WRITE_B 0x123456789abcdef0ULL
+#define WRITE_A_AGAIN 0xfedcba9876543210ULL
+
+#define L1_HCR (HCR_EL2_RW | HCR_EL2_E2H | \
+ HCR_EL2_NV | HCR_EL2_NV2)
+#define L2_PSTATE (PSR_MODE_EL1h | PSR_D_BIT | PSR_A_BIT | \
+ PSR_I_BIT | PSR_F_BIT)
+
+static struct test_config {
+ u64 vncr_va;
+ u64 *vncr_ptep;
+ u64 pte_a;
+ u64 pte_b;
+ u64 *slot_a;
+ u64 *slot_b;
+ u64 l2_sp;
+} cfg;
+
+static unsigned int round_nr;
+
+extern void l2_probe(void);
+extern char l2_after_hvc[];
+
+/*
+ * x1: value to write.
+ * x0: value read, observed by L1's HVC handler.
+ *
+ * No stack accesses or C calls in L2.
+ * Use the SMCCC convention of a zero HVC immediate.
+ */
+asm(
+".pushsection .text,\"ax\"\n"
+".balign 4\n"
+".global l2_probe\n"
+".type l2_probe, %function\n"
+"l2_probe:\n"
+" mrs x0, tpidr_el2\n"
+" msr tpidr_el2, x1\n"
+" dsb ish\n"
+" hvc #0\n"
+".global l2_after_hvc\n"
+"l2_after_hvc:\n"
+" brk #0\n"
+".size l2_probe, . - l2_probe\n"
+".popsection\n"
+);
+
+static void remap_vncr(u64 new_pte)
+{
+ u64 hcr = read_sysreg(hcr_el2);
+ u64 operand = cfg.vncr_va >> 12;
+
+ /*
+ * Use the VHE host translation regime for the EL1 TLBI
+ * instruction: HCR_EL2.{E2H,TGE} == {1,1}.
+ */
+ write_sysreg(hcr | HCR_EL2_TGE, hcr_el2);
+ isb();
+
+ /* Break before make. VNCR_EL2 itself is never rewritten. */
+ WRITE_ONCE(*cfg.vncr_ptep, 0);
+ dsb(ishst);
+
+ asm volatile("tlbi vaae1is, %0" : : "r" (operand) : "memory");
+ dsb(ish);
+ isb();
+
+ WRITE_ONCE(*cfg.vncr_ptep, new_pte);
+ dsb(ishst);
+ isb();
+
+ /* Permit return to L2 at EL1. */
+ write_sysreg(hcr, hcr_el2);
+ isb();
+}
+
+static void handle_l2_hvc(struct ex_regs *regs)
+{
+ u64 esr = read_sysreg(esr_el1);
+
+ GUEST_ASSERT_EQ(ESR_ELx_EC(esr), ESR_ELx_EC_HVC64);
+ GUEST_ASSERT_EQ(esr & 0xffff, 0x0);
+ GUEST_ASSERT_EQ(regs->pc, (u64)l2_after_hvc);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_VNCR_EL2), cfg.vncr_va);
+
+ dsb(ish);
+
+ switch (round_nr) {
+ case 0:
+ /* Warmed translation: VNCR VA -> A. */
+ GUEST_ASSERT_EQ(regs->regs[0], SEED_A);
+ GUEST_ASSERT_EQ(READ_ONCE(*cfg.slot_a), WRITE_A);
+ GUEST_ASSERT_EQ(READ_ONCE(*cfg.slot_b), SEED_B);
+
+ remap_vncr(cfg.pte_b);
+ regs->regs[1] = WRITE_B;
+ break;
+
+ case 1:
+ /* Following TLBI, reads and writes must now use B. */
+ GUEST_ASSERT_EQ(regs->regs[0], SEED_B);
+ GUEST_ASSERT_EQ(READ_ONCE(*cfg.slot_b), WRITE_B);
+ GUEST_ASSERT_EQ(READ_ONCE(*cfg.slot_a), WRITE_A);
+
+ remap_vncr(cfg.pte_a);
+ regs->regs[1] = WRITE_A_AGAIN;
+ break;
+
+ case 2:
+ /* Check invalidation again, with the mapping reversed. */
+ GUEST_ASSERT_EQ(regs->regs[0], WRITE_A);
+ GUEST_ASSERT_EQ(READ_ONCE(*cfg.slot_a), WRITE_A_AGAIN);
+ GUEST_ASSERT_EQ(READ_ONCE(*cfg.slot_b), WRITE_B);
+
+ GUEST_DONE();
+ default:
+ GUEST_FAIL("Unexpected round %u", round_nr);
+ return;
+ }
+
+ round_nr++;
+ regs->pc = (u64)l2_probe;
+ regs->pstate = L2_PSTATE;
+}
+
+static void guest_code(void)
+{
+ GUEST_ASSERT_EQ(get_current_el(), 2);
+ GUEST_ASSERT(read_sysreg(hcr_el2) & HCR_EL2_E2H);
+
+ /*
+ * With E2H=1, these EL1 accessors read L1's own configuration.
+ * EL12 accessors configure the EL1 context used to execute L2.
+ */
+ write_sysreg_s(read_sysreg(tcr_el1), SYS_TCR_EL12);
+ write_sysreg_s(read_sysreg(ttbr0_el1), SYS_TTBR0_EL12);
+ write_sysreg_s(read_sysreg(mair_el1), SYS_MAIR_EL12);
+ write_sysreg_s(read_sysreg(cpacr_el1), SYS_CPACR_EL12);
+ write_sysreg_s(read_sysreg(vbar_el1), SYS_VBAR_EL12);
+ write_sysreg_s(read_sysreg(sctlr_el1), SYS_SCTLR_EL12);
+ write_sysreg(cfg.l2_sp, sp_el1);
+ isb();
+
+ /*
+ * These aliases remain mapped throughout the test. They allow
+ * inspection of both backing pages independently of VNCR VA.
+ */
+ WRITE_ONCE(*cfg.slot_a, SEED_A);
+ WRITE_ONCE(*cfg.slot_b, SEED_B);
+ dsb(ish);
+
+ /* Program the VNCR address exactly once. */
+ write_sysreg_s(cfg.vncr_va, SYS_VNCR_EL2);
+
+ /* VM=0, TGE=0; enable nested register redirection for L2. */
+ write_sysreg(L1_HCR, hcr_el2);
+ isb();
+
+ GUEST_ASSERT_EQ(read_sysreg(hcr_el2) &
+ (HCR_EL2_NV | HCR_EL2_NV2),
+ HCR_EL2_NV | HCR_EL2_NV2);
+ GUEST_ASSERT(!(read_sysreg(hcr_el2) & HCR_EL2_VM));
+
+ write_sysreg((u64)l2_probe, elr_el2);
+ write_sysreg(L2_PSTATE, spsr_el2);
+ isb();
+
+ asm volatile(
+ "mov x1, %0\n"
+ "eret\n"
+ :
+ : "r" (WRITE_A)
+ : "x1", "memory");
+
+ __builtin_unreachable();
+}
+
+/* Add a guest virtual alias for an existing guest physical page. */
+static u64 map_alias(struct kvm_vm *vm, u64 gpa)
+{
+ u64 va = vm_unused_gva_gap(vm, vm->page_size,
+ KVM_UTIL_MIN_VADDR);
+
+ virt_map(vm, va, gpa, 1);
+ return va;
+}
+
+static void prepare_memory(struct kvm_vm *vm)
+{
+ u64 a = vm_alloc_page(vm);
+ u64 b = vm_alloc_page(vm);
+ u64 page_offset_mask = (u64)vm->page_size - 1;
+ u64 stack = vm_alloc_pages(vm, DEFAULT_STACK_PGS);
+ u64 ptep_gpa, ptep_alias;
+ u64 *ptep;
+
+ cfg.slot_a = (u64 *)(a + VNCR_TPIDR_OFFSET);
+ cfg.slot_b = (u64 *)(b + VNCR_TPIDR_OFFSET);
+ cfg.l2_sp = stack + DEFAULT_STACK_PGS * vm->page_size;
+
+ /* Initially, the separate VNCR virtual address maps page A. */
+ cfg.vncr_va = map_alias(vm, addr_gva2gpa(vm, a));
+
+ ptep = virt_get_pte_hva(vm, cfg.vncr_va);
+ cfg.pte_a = *ptep;
+ cfg.pte_b = *virt_get_pte_hva(vm, b);
+
+ TEST_ASSERT(cfg.pte_a != cfg.pte_b,
+ "Backing pages must have different descriptors");
+ TEST_ASSERT((cfg.pte_a & (PTE_VALID | PTE_TYPE_PAGE)) ==
+ (PTE_VALID | PTE_TYPE_PAGE),
+ "VNCR mapping must use a leaf page descriptor");
+
+ /*
+ * Map the page containing the descriptor into L1, so L1 can
+ * perform its own page-table update and architectural TLBI.
+ */
+ ptep_gpa = addr_hva2gpa(vm, ptep);
+ ptep_alias = map_alias(vm, ptep_gpa & ~page_offset_mask);
+ cfg.vncr_ptep = (u64 *)(ptep_alias + (ptep_gpa & page_offset_mask));
+
+ sync_global_to_guest(vm, cfg);
+}
+
+int main(void)
+{
+ struct kvm_vm *vm;
+ struct kvm_vcpu *vcpu;
+ struct ucall uc;
+ u64 mmfr4, cmd;
+
+ TEST_REQUIRE(test_supports_el2());
+
+ vm = __vm_create_with_one_vcpu(&vcpu, 32, guest_code);
+ TEST_REQUIRE(vcpu_has_el2(vcpu));
+
+ /*
+ * Target KVM's exposed NV2-only-or-later nested capability.
+ * Merely having a virtual EL2 is insufficient for this test.
+ */
+ mmfr4 = vcpu_get_reg(vcpu,
+ KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR4_EL1));
+ TEST_REQUIRE(FIELD_GET(ID_AA64MMFR4_EL1_NV_frac, mmfr4) >=
+ ID_AA64MMFR4_EL1_NV_frac_NV2_ONLY);
+
+ vm_init_descriptor_tables(vm);
+ vcpu_init_descriptor_tables(vcpu);
+ vm_install_sync_handler(vm, VECTOR_SYNC_LOWER_64,
+ ESR_ELx_EC_HVC64, handle_l2_hvc);
+
+ prepare_memory(vm);
+
+ for (;;) {
+ vcpu_run(vcpu);
+ cmd = get_ucall(vcpu, &uc);
+
+ switch (cmd) {
+ case UCALL_PRINTF:
+ REPORT_GUEST_PRINTF(uc);
+ break;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ break;
+ case UCALL_DONE:
+ pr_info("PASS: VNCR accesses followed A -> B -> A mappings; "
+ "inactive backing slots remained unchanged\n");
+ kvm_vm_free(vm);
+ return 0;
+ default:
+ TEST_FAIL("Unexpected ucall %lu, KVM exit reason %u",
+ cmd, vcpu->run->exit_reason);
+ }
+ }
+}
---
base-commit: c03c1a14507dfaf8b856c2608f9ac216021fc222
change-id: 20261022-b4-vncr-mapping-test-3a18cb49a2e2
Best regards,
--
Itaru Kitayama <itaru.kitayama@xxxxxxxxxxx>