* [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs
@ 2026-11-04 8:48 Fuad Tabba
2026-11-04 8:48 ` [PATCH 01/16] KVM: arm64: selftests: Inject an abort before a vCPU's first run Fuad Tabba
` (16 more replies)
0 siblings, 17 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
Hi folks,
This series adds selftests for protected VMs. A test skips on a host
without pKVM, and so does any case where the host doesn't have the
required feature, e.g., PtrAuth, SVE or hugetlb pages.
A couple of points:
- Each area gets its own test rather than cases folded into existing
tests such as psci_test, hypercalls, set_id_regs, etc. A protected
guest requires its own boot path, and has to share the ucall pool
before its first ucall. On top of that, a protected guest gets the
opposite result for many of those tests' checks.
- pkvm_exceptions skips its breakpoint, watchpoint and single-step cases
while a protected guest's ID_AA64DFR0_EL1.DebugVer is 0. pkvm_id_regs
reads the same field to choose between the hidden and the exposed
debug view. With the patch that adds self-hosted debug to protected
guests [1], both tests run the debug cases with no change.
The series is structured as follows:
01: external_aborts: an abort injected before a vCPU's first run.
02-04: Generic selftest changes: creating a protected VM, the pKVM
function IDs and the ucall pool's address.
05-06: Protected-VM library: boot trampoline and helpers.
07-16: The tests, one per area.
Based on Linux 7.4-rc1 (6628e7d4c6b25).
Cheers,
/fuad
[1] https://lore.kernel.org/all/20261102093950.2922035-1-fuad.tabba@linux.dev/
Fuad Tabba (16):
KVM: arm64: selftests: Inject an abort before a vCPU's first run
KVM: selftests: Allow arm64 tests to create protected VMs
tools: arm-smccc: Add the pKVM memory sharing function IDs
KVM: selftests: Export the ucall pool's guest physical address
KVM: arm64: selftests: Add protected-VM boot trampoline
KVM: arm64: selftests: Add protected-VM library helpers
KVM: arm64: selftests: Add pkvm_host_abi host-interface test
KVM: arm64: selftests: Add pkvm_psci multi-vCPU PSCI test
KVM: arm64: selftests: Add pkvm_mem_share hypercall test
KVM: arm64: selftests: Add pkvm_smccc hypercall test
KVM: arm64: selftests: Add pkvm_id_regs register and trap test
KVM: arm64: selftests: Add pkvm_vcpu_regs register-state test
KVM: arm64: selftests: Add pkvm_irq interrupt test
KVM: arm64: selftests: Add pkvm_isolation host-access test
KVM: arm64: selftests: Add pkvm_lifecycle teardown test
KVM: arm64: selftests: Add pkvm_exceptions debug and SError test
tools/include/linux/arm-smccc.h | 22 +
tools/testing/selftests/kvm/Makefile.kvm | 12 +
.../selftests/kvm/arm64/external_aborts.c | 21 +
.../selftests/kvm/arm64/pkvm_exceptions.c | 261 ++++++++
.../selftests/kvm/arm64/pkvm_host_abi.c | 428 ++++++++++++
.../selftests/kvm/arm64/pkvm_id_regs.c | 306 +++++++++
tools/testing/selftests/kvm/arm64/pkvm_irq.c | 268 ++++++++
.../selftests/kvm/arm64/pkvm_isolation.c | 225 +++++++
.../selftests/kvm/arm64/pkvm_lifecycle.c | 110 +++
.../selftests/kvm/arm64/pkvm_mem_share.c | 394 +++++++++++
tools/testing/selftests/kvm/arm64/pkvm_psci.c | 632 ++++++++++++++++++
.../testing/selftests/kvm/arm64/pkvm_smccc.c | 117 ++++
.../selftests/kvm/arm64/pkvm_vcpu_regs.c | 221 ++++++
.../selftests/kvm/include/arm64/pkvm.h | 74 ++
.../selftests/kvm/include/ucall_common.h | 1 +
tools/testing/selftests/kvm/lib/arm64/pkvm.c | 245 +++++++
.../selftests/kvm/lib/arm64/pvm-entry.S | 58 ++
tools/testing/selftests/kvm/lib/kvm_util.c | 5 +-
.../testing/selftests/kvm/lib/ucall_common.c | 9 +-
19 files changed, 3406 insertions(+), 3 deletions(-)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_exceptions.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_host_abi.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_id_regs.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_irq.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_isolation.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_lifecycle.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_mem_share.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_psci.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_smccc.c
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_vcpu_regs.c
create mode 100644 tools/testing/selftests/kvm/include/arm64/pkvm.h
create mode 100644 tools/testing/selftests/kvm/lib/arm64/pkvm.c
create mode 100644 tools/testing/selftests/kvm/lib/arm64/pvm-entry.S
base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
--
2.47.3
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH 01/16] KVM: arm64: selftests: Inject an abort before a vCPU's first run
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 02/16] KVM: selftests: Allow arm64 tests to create protected VMs Fuad Tabba
` (15 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
KVM commits an external abort injected with KVM_SET_VCPU_EVENTS
immediately, even before the vCPU's first run. For a normal VM on a pKVM
host that path panicked the hypervisor until commit a2bb5f5c67a95
("KVM: arm64: Pin the host vCPU before adjusting its PC under pKVM"). No
test injects before the first run.
Add one, checking that the guest takes the abort at its entry point.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
.../selftests/kvm/arm64/external_aborts.c | 21 +++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/tools/testing/selftests/kvm/arm64/external_aborts.c b/tools/testing/selftests/kvm/arm64/external_aborts.c
index 1a858d0d82968..45c9300b2ca0e 100644
--- a/tools/testing/selftests/kvm/arm64/external_aborts.c
+++ b/tools/testing/selftests/kvm/arm64/external_aborts.c
@@ -113,6 +113,26 @@ static void vcpu_run_expect_sync(struct kvm_vcpu *vcpu)
__vcpu_run_expect(vcpu, UCALL_SYNC);
}
+static void test_sea_before_run_guest(void)
+{
+ GUEST_FAIL("Guest ran before taking the injected abort");
+}
+
+/* Test that an abort injected before the vCPU's first run is taken at its entry point. */
+static void test_sea_before_run(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm = vm_create_with_dabt_handler(&vcpu, test_sea_before_run_guest,
+ expect_sea_handler);
+
+ expected_abort_pc = (u64)test_sea_before_run_guest;
+ sync_global_to_guest(vm, expected_abort_pc);
+
+ vcpu_inject_sea(vcpu);
+ vcpu_run_expect_done(vcpu);
+ kvm_vm_free(vm);
+}
+
extern char test_mmio_abort_insn;
static noinline void test_mmio_abort_guest(void)
@@ -420,6 +440,7 @@ static void test_serror_amo(void)
int main(void)
{
+ test_sea_before_run();
test_mmio_abort();
test_mmio_nisv();
test_mmio_nisv_abort();
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 02/16] KVM: selftests: Allow arm64 tests to create protected VMs
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
2026-11-04 8:48 ` [PATCH 01/16] KVM: arm64: selftests: Inject an abort before a vCPU's first run Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 03/16] tools: arm-smccc: Add the pKVM memory sharing function IDs Fuad Tabba
` (14 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
On arm64, the VM type passed to KVM_CREATE_VM carries the IPA size,
which the selftest library fills in. The library rejects any type a test
provides in vm_shape.type. A protected VM is created by setting
KVM_VM_TYPE_ARM_PROTECTED in that type, so a test can't create one.
Accept KVM_VM_TYPE_ARM_PROTECTED from the test, and add the IPA size to
it as for any other VM.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/lib/kvm_util.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/tools/testing/selftests/kvm/lib/kvm_util.c b/tools/testing/selftests/kvm/lib/kvm_util.c
index d42e201eba763..96c1e70dba3d3 100644
--- a/tools/testing/selftests/kvm/lib/kvm_util.c
+++ b/tools/testing/selftests/kvm/lib/kvm_util.c
@@ -400,9 +400,10 @@ struct kvm_vm *____vm_create(struct vm_shape shape)
}
#ifdef __aarch64__
- TEST_ASSERT(!vm->type, "ARM doesn't support test-provided types");
+ TEST_ASSERT(!(vm->type & ~KVM_VM_TYPE_ARM_PROTECTED),
+ "ARM supports only KVM_VM_TYPE_ARM_PROTECTED as a test-provided type");
if (vm->pa_bits != 40)
- vm->type = KVM_VM_TYPE_ARM_IPA_SIZE(vm->pa_bits);
+ vm->type |= KVM_VM_TYPE_ARM_IPA_SIZE(vm->pa_bits);
#endif
vm_open(vm);
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 03/16] tools: arm-smccc: Add the pKVM memory sharing function IDs
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
2026-11-04 8:48 ` [PATCH 01/16] KVM: arm64: selftests: Inject an abort before a vCPU's first run Fuad Tabba
2026-11-04 8:48 ` [PATCH 02/16] KVM: selftests: Allow arm64 tests to create protected VMs Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 04/16] KVM: selftests: Export the ucall pool's guest physical address Fuad Tabba
` (13 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
The tools copy of arm-smccc.h lacks the pKVM guest hypercalls that the
protected-VM selftests make.
The kernel's arm-smccc.h defines the vendor-hyp UID with UUID_INIT(),
which requires linux/uuid.h, and tools doesn't have one. So rather than
sync the whole header, copy the function numbers and IDs of HYP_MEMINFO,
MEM_SHARE and MEM_UNSHARE from include/linux/arm-smccc.h.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/include/linux/arm-smccc.h | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/tools/include/linux/arm-smccc.h b/tools/include/linux/arm-smccc.h
index 63ce9bebccd32..8c8f4ad639018 100644
--- a/tools/include/linux/arm-smccc.h
+++ b/tools/include/linux/arm-smccc.h
@@ -111,6 +111,10 @@
/* KVM "vendor specific" services */
#define ARM_SMCCC_KVM_FUNC_FEATURES 0
#define ARM_SMCCC_KVM_FUNC_PTP 1
+/* Start of pKVM hypercall range */
+#define ARM_SMCCC_KVM_FUNC_HYP_MEMINFO 2
+#define ARM_SMCCC_KVM_FUNC_MEM_SHARE 3
+#define ARM_SMCCC_KVM_FUNC_MEM_UNSHARE 4
#define ARM_SMCCC_KVM_FUNC_FEATURES_2 127
#define ARM_SMCCC_KVM_NUM_FUNCS 128
@@ -133,6 +137,24 @@
ARM_SMCCC_OWNER_VENDOR_HYP, \
ARM_SMCCC_KVM_FUNC_PTP)
+#define ARM_SMCCC_VENDOR_HYP_KVM_HYP_MEMINFO_FUNC_ID \
+ ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, \
+ ARM_SMCCC_SMC_64, \
+ ARM_SMCCC_OWNER_VENDOR_HYP, \
+ ARM_SMCCC_KVM_FUNC_HYP_MEMINFO)
+
+#define ARM_SMCCC_VENDOR_HYP_KVM_MEM_SHARE_FUNC_ID \
+ ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, \
+ ARM_SMCCC_SMC_64, \
+ ARM_SMCCC_OWNER_VENDOR_HYP, \
+ ARM_SMCCC_KVM_FUNC_MEM_SHARE)
+
+#define ARM_SMCCC_VENDOR_HYP_KVM_MEM_UNSHARE_FUNC_ID \
+ ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, \
+ ARM_SMCCC_SMC_64, \
+ ARM_SMCCC_OWNER_VENDOR_HYP, \
+ ARM_SMCCC_KVM_FUNC_MEM_UNSHARE)
+
/* ptp_kvm counter type ID */
#define KVM_PTP_VIRT_COUNTER 0
#define KVM_PTP_PHYS_COUNTER 1
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 04/16] KVM: selftests: Export the ucall pool's guest physical address
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (2 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 03/16] tools: arm-smccc: Add the pKVM memory sharing function IDs Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 05/16] KVM: arm64: selftests: Add protected-VM boot trampoline Fuad Tabba
` (12 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
A protected guest's memory is private until the guest itself shares it,
so the guest has to share the ucall pool before its first ucall. The
host can't do that for it. Only the guest's copy of ucall_pool holds the
pool's address.
Add get_ucall_pool_gpa(), which reads that copy, so that the host can
pass the address to the guest.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/include/ucall_common.h | 1 +
tools/testing/selftests/kvm/lib/ucall_common.c | 9 ++++++++-
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/kvm/include/ucall_common.h b/tools/testing/selftests/kvm/include/ucall_common.h
index cbdcb0a50c4f9..3a6cdf9aa0464 100644
--- a/tools/testing/selftests/kvm/include/ucall_common.h
+++ b/tools/testing/selftests/kvm/include/ucall_common.h
@@ -41,6 +41,7 @@ __printf(5, 6) void ucall_assert(u64 cmd, const char *exp,
u64 get_ucall(struct kvm_vcpu *vcpu, struct ucall *uc);
void ucall_init(struct kvm_vm *vm, gpa_t mmio_gpa);
int ucall_nr_pages_required(u64 page_size);
+gpa_t get_ucall_pool_gpa(struct kvm_vm *vm);
/*
* Perform userspace call without any associated data. This bare call avoids
diff --git a/tools/testing/selftests/kvm/lib/ucall_common.c b/tools/testing/selftests/kvm/lib/ucall_common.c
index 029ce21f9f2fa..aa1f0f92c6d1d 100644
--- a/tools/testing/selftests/kvm/lib/ucall_common.c
+++ b/tools/testing/selftests/kvm/lib/ucall_common.c
@@ -21,10 +21,17 @@ int ucall_nr_pages_required(u64 page_size)
/*
* ucall_pool holds per-VM values (global data is duplicated by each VM), it
- * must not be accessed from host code.
+ * must not be accessed from host code. The host reads only a VM's copy.
*/
static struct ucall_header *ucall_pool;
+gpa_t get_ucall_pool_gpa(struct kvm_vm *vm)
+{
+ struct ucall_header **pool = addr_gva2hva(vm, (gva_t)&ucall_pool);
+
+ return addr_gva2gpa(vm, (gva_t)*pool);
+}
+
void ucall_init(struct kvm_vm *vm, gpa_t mmio_gpa)
{
struct ucall_header *hdr;
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 05/16] KVM: arm64: selftests: Add protected-VM boot trampoline
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (3 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 04/16] KVM: selftests: Export the ucall pool's guest physical address Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 06/16] KVM: arm64: selftests: Add protected-VM library helpers Fuad Tabba
` (11 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
EL2 takes only PC and X0 from the host when it boots a protected vCPU,
and resets everything else. So the registers the selftest library sets
before the first run never reach the guest.
Boot a protected guest through a trampoline that restores X0-X7, seven
EL1 system registers and the stack pointer from a block X0 points to,
then enables the MMU and branches to the guest's entry point.
Co-developed-by: Will Deacon <will@kernel.org>
Signed-off-by: Will Deacon <will@kernel.org>
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/include/arm64/pkvm.h | 50 ++++++++++++++++
.../selftests/kvm/lib/arm64/pvm-entry.S | 58 +++++++++++++++++++
3 files changed, 109 insertions(+)
create mode 100644 tools/testing/selftests/kvm/include/arm64/pkvm.h
create mode 100644 tools/testing/selftests/kvm/lib/arm64/pvm-entry.S
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 17a4256e90819..c254e82f9a786 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -35,6 +35,7 @@ LIBKVM_arm64 += lib/arm64/gic_v3.c
LIBKVM_arm64 += lib/arm64/gic_v3_its.c
LIBKVM_arm64 += lib/arm64/handlers.S
LIBKVM_arm64 += lib/arm64/processor.c
+LIBKVM_arm64 += lib/arm64/pvm-entry.S
LIBKVM_arm64 += lib/arm64/spinlock.c
LIBKVM_arm64 += lib/arm64/ucall.c
LIBKVM_arm64 += lib/arm64/vgic.c
diff --git a/tools/testing/selftests/kvm/include/arm64/pkvm.h b/tools/testing/selftests/kvm/include/arm64/pkvm.h
new file mode 100644
index 0000000000000..6ef769b8ff977
--- /dev/null
+++ b/tools/testing/selftests/kvm/include/arm64/pkvm.h
@@ -0,0 +1,50 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ * Author: Will Deacon <will@kernel.org>
+ */
+
+#ifndef SELFTEST_KVM_ARM64_PKVM_H
+#define SELFTEST_KVM_ARM64_PKVM_H
+
+#ifndef __ASSEMBLER__
+/*
+ * Register state the boot trampoline (pvm-entry.S) restores. Only PC and X0
+ * survive to a protected guest's first run, so X0 points at this block.
+ */
+struct pvm_boot_args {
+ u64 jump_tgt;
+
+ u64 regs[8];
+ u64 cpacr_el1;
+ u64 sctlr_el1;
+ u64 tcr_el1;
+ u64 mair_el1;
+ u64 ttbr0_el1;
+ u64 vbar_el1;
+
+ u64 tpidr_el1;
+ u64 sp_el1;
+};
+#endif
+
+#define PVM_BOOT_ARGS_OFF_JUMP_TGT 0
+#define PVM_BOOT_ARGS_OFF_X0 (PVM_BOOT_ARGS_OFF_JUMP_TGT + 8)
+#define PVM_BOOT_ARGS_OFF_X1 (PVM_BOOT_ARGS_OFF_X0 + 8)
+#define PVM_BOOT_ARGS_OFF_X2 (PVM_BOOT_ARGS_OFF_X1 + 8)
+#define PVM_BOOT_ARGS_OFF_X3 (PVM_BOOT_ARGS_OFF_X2 + 8)
+#define PVM_BOOT_ARGS_OFF_X4 (PVM_BOOT_ARGS_OFF_X3 + 8)
+#define PVM_BOOT_ARGS_OFF_X5 (PVM_BOOT_ARGS_OFF_X4 + 8)
+#define PVM_BOOT_ARGS_OFF_X6 (PVM_BOOT_ARGS_OFF_X5 + 8)
+#define PVM_BOOT_ARGS_OFF_X7 (PVM_BOOT_ARGS_OFF_X6 + 8)
+#define PVM_BOOT_ARGS_OFF_CPACR_EL1 (PVM_BOOT_ARGS_OFF_X7 + 8)
+#define PVM_BOOT_ARGS_OFF_SCTLR_EL1 (PVM_BOOT_ARGS_OFF_CPACR_EL1 + 8)
+#define PVM_BOOT_ARGS_OFF_TCR_EL1 (PVM_BOOT_ARGS_OFF_SCTLR_EL1 + 8)
+#define PVM_BOOT_ARGS_OFF_MAIR_EL1 (PVM_BOOT_ARGS_OFF_TCR_EL1 + 8)
+#define PVM_BOOT_ARGS_OFF_TTBR0_EL1 (PVM_BOOT_ARGS_OFF_MAIR_EL1 + 8)
+#define PVM_BOOT_ARGS_OFF_VBAR_EL1 (PVM_BOOT_ARGS_OFF_TTBR0_EL1 + 8)
+#define PVM_BOOT_ARGS_OFF_TPIDR_EL1 (PVM_BOOT_ARGS_OFF_VBAR_EL1 + 8)
+#define PVM_BOOT_ARGS_OFF_SP_EL1 (PVM_BOOT_ARGS_OFF_TPIDR_EL1 + 8)
+
+#endif /* SELFTEST_KVM_ARM64_PKVM_H */
diff --git a/tools/testing/selftests/kvm/lib/arm64/pvm-entry.S b/tools/testing/selftests/kvm/lib/arm64/pvm-entry.S
new file mode 100644
index 0000000000000..a0974cde78da6
--- /dev/null
+++ b/tools/testing/selftests/kvm/lib/arm64/pvm-entry.S
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ * Author: Will Deacon <will@kernel.org>
+ */
+
+#include "pkvm.h"
+
+ .text
+ .balign 16
+ .globl pvm_entry_start
+pvm_entry_start:
+ mov x30, x0
+
+ ldr x0, [x30, #PVM_BOOT_ARGS_OFF_X0]
+ ldr x1, [x30, #PVM_BOOT_ARGS_OFF_X1]
+ ldr x2, [x30, #PVM_BOOT_ARGS_OFF_X2]
+ ldr x3, [x30, #PVM_BOOT_ARGS_OFF_X3]
+ ldr x4, [x30, #PVM_BOOT_ARGS_OFF_X4]
+ ldr x5, [x30, #PVM_BOOT_ARGS_OFF_X5]
+ ldr x6, [x30, #PVM_BOOT_ARGS_OFF_X6]
+ ldr x7, [x30, #PVM_BOOT_ARGS_OFF_X7]
+
+ ldr x8, [x30, #PVM_BOOT_ARGS_OFF_JUMP_TGT]
+
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_CPACR_EL1]
+ msr cpacr_el1, x9
+
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_TCR_EL1]
+ msr tcr_el1, x9
+
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_MAIR_EL1]
+ msr mair_el1, x9
+
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_TTBR0_EL1]
+ msr ttbr0_el1, x9
+
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_VBAR_EL1]
+ msr vbar_el1, x9
+
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_TPIDR_EL1]
+ msr tpidr_el1, x9
+
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_SP_EL1]
+ mov sp, x9
+
+ /* No stale TLB entries, and the writes above in effect, before the MMU is on. */
+ tlbi vmalle1
+ dsb nsh
+ isb
+ ldr x9, [x30, #PVM_BOOT_ARGS_OFF_SCTLR_EL1]
+ msr sctlr_el1, x9
+ isb
+
+ br x8
+ .globl pvm_entry_end
+pvm_entry_end:
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 06/16] KVM: arm64: selftests: Add protected-VM library helpers
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (4 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 05/16] KVM: arm64: selftests: Add protected-VM boot trampoline Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 07/16] KVM: arm64: selftests: Add pkvm_host_abi host-interface test Fuad Tabba
` (10 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
A protected-VM test has to create the VM with the protected type and the
PSCI 0.2 vCPU feature, boot each vCPU through the trampoline from an
identity-mapped page, and have the guest share the ucall pool before its
first ucall. Secondaries start powered off for the guest to boot with
CPU_ON. A vCPU becomes a valid CPU_ON target only at its first KVM_RUN.
Add lib/arm64/pkvm.c to do this for the tests.
pkvm_vcpu_setup_boot_secondary() runs a secondary once with
immediate_exit set, so that it becomes a CPU_ON target. The library also
has a run loop, an SMCCC call for the guest, and a reader of the
process's locked-memory count. The host adds a protected VM's pinned
pages to that count. No capability advertises pKVM, so pkvm_supported()
tries to create a protected VM.
Co-developed-by: Will Deacon <will@kernel.org>
Signed-off-by: Will Deacon <will@kernel.org>
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/include/arm64/pkvm.h | 24 ++
tools/testing/selftests/kvm/lib/arm64/pkvm.c | 245 ++++++++++++++++++
3 files changed, 270 insertions(+)
create mode 100644 tools/testing/selftests/kvm/lib/arm64/pkvm.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index c254e82f9a786..e173a9adad3f2 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -34,6 +34,7 @@ LIBKVM_arm64 += lib/arm64/gic.c
LIBKVM_arm64 += lib/arm64/gic_v3.c
LIBKVM_arm64 += lib/arm64/gic_v3_its.c
LIBKVM_arm64 += lib/arm64/handlers.S
+LIBKVM_arm64 += lib/arm64/pkvm.c
LIBKVM_arm64 += lib/arm64/processor.c
LIBKVM_arm64 += lib/arm64/pvm-entry.S
LIBKVM_arm64 += lib/arm64/spinlock.c
diff --git a/tools/testing/selftests/kvm/include/arm64/pkvm.h b/tools/testing/selftests/kvm/include/arm64/pkvm.h
index 6ef769b8ff977..413c7ac4a9f16 100644
--- a/tools/testing/selftests/kvm/include/arm64/pkvm.h
+++ b/tools/testing/selftests/kvm/include/arm64/pkvm.h
@@ -9,6 +9,9 @@
#define SELFTEST_KVM_ARM64_PKVM_H
#ifndef __ASSEMBLER__
+#include "kvm_util.h"
+#include "ucall_common.h"
+
/*
* Register state the boot trampoline (pvm-entry.S) restores. Only PC and X0
* survive to a protected guest's first run, so X0 points at this block.
@@ -27,6 +30,27 @@ struct pvm_boot_args {
u64 tpidr_el1;
u64 sp_el1;
};
+
+/* Memslot of the trampoline's page. Test memslots start above it. */
+#define PKVM_IDMAP_MEMSLOT 1
+
+bool pkvm_supported(void);
+struct kvm_vm *pkvm_vm_create(unsigned int nr_vcpus, u32 features, struct kvm_vcpu *vcpus[]);
+struct kvm_vm *pkvm_vm_create_with_one_vcpu(struct kvm_vcpu **vcpu, void *guest_code);
+
+/* Call after all register setup: later register writes don't reach the guest. */
+void pkvm_vcpu_setup_boot(struct kvm_vcpu *vcpu, void *guest_code);
+
+/* Call before the primary's first run. */
+gpa_t pkvm_vcpu_setup_boot_secondary(struct kvm_vcpu *vcpu, void *guest_code);
+gpa_t pkvm_idmap_gpa(struct kvm_vm *vm);
+
+void pkvm_run_loop(struct kvm_vcpu *vcpu, void (*on_sync)(struct kvm_vcpu *vcpu, struct ucall *uc));
+size_t pkvm_host_locked_vm(void);
+
+unsigned long pkvm_hvc(u32 func, u64 arg0, u64 arg1, u64 arg2);
+
+void pkvm_guest_init(void);
#endif
#define PVM_BOOT_ARGS_OFF_JUMP_TGT 0
diff --git a/tools/testing/selftests/kvm/lib/arm64/pkvm.c b/tools/testing/selftests/kvm/lib/arm64/pkvm.c
new file mode 100644
index 0000000000000..f2d4cf48966e8
--- /dev/null
+++ b/tools/testing/selftests/kvm/lib/arm64/pkvm.c
@@ -0,0 +1,245 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Protected-VM support for the arm64 KVM selftest library.
+ *
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ * Author: Will Deacon <will@kernel.org>
+ */
+
+#include <errno.h>
+
+#include <linux/arm-smccc.h>
+#include <linux/kernel.h>
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "ucall_common.h"
+
+/* Set in the guest's copy only, by pkvm_vm_create(). */
+static gpa_t pkvm_ucall_pool_gpa;
+static size_t pkvm_ucall_pool_size;
+
+extern char pvm_entry_start[], pvm_entry_end[];
+
+#define ASSERT_BOOT_ARGS_OFF(field, off) \
+ static_assert(offsetof(struct pvm_boot_args, field) == PVM_BOOT_ARGS_OFF_##off, #off)
+
+ASSERT_BOOT_ARGS_OFF(jump_tgt, JUMP_TGT);
+ASSERT_BOOT_ARGS_OFF(regs[0], X0);
+ASSERT_BOOT_ARGS_OFF(regs[1], X1);
+ASSERT_BOOT_ARGS_OFF(regs[2], X2);
+ASSERT_BOOT_ARGS_OFF(regs[3], X3);
+ASSERT_BOOT_ARGS_OFF(regs[4], X4);
+ASSERT_BOOT_ARGS_OFF(regs[5], X5);
+ASSERT_BOOT_ARGS_OFF(regs[6], X6);
+ASSERT_BOOT_ARGS_OFF(regs[7], X7);
+ASSERT_BOOT_ARGS_OFF(cpacr_el1, CPACR_EL1);
+ASSERT_BOOT_ARGS_OFF(sctlr_el1, SCTLR_EL1);
+ASSERT_BOOT_ARGS_OFF(tcr_el1, TCR_EL1);
+ASSERT_BOOT_ARGS_OFF(mair_el1, MAIR_EL1);
+ASSERT_BOOT_ARGS_OFF(ttbr0_el1, TTBR0_EL1);
+ASSERT_BOOT_ARGS_OFF(vbar_el1, VBAR_EL1);
+ASSERT_BOOT_ARGS_OFF(tpidr_el1, TPIDR_EL1);
+ASSERT_BOOT_ARGS_OFF(sp_el1, SP_EL1);
+
+unsigned long pkvm_hvc(u32 func, u64 arg0, u64 arg1, u64 arg2)
+{
+ struct arm_smccc_res res;
+
+ smccc_hvc(func, arg0, arg1, arg2, 0, 0, 0, 0, &res);
+ return res.a0;
+}
+
+/* Shares whole granules, so pages next to the pool are shared with it. */
+void pkvm_guest_init(void)
+{
+ u64 granule = pkvm_hvc(ARM_SMCCC_VENDOR_HYP_KVM_HYP_MEMINFO_FUNC_ID, 0, 0, 0);
+ gpa_t end = pkvm_ucall_pool_gpa + pkvm_ucall_pool_size;
+ gpa_t gpa;
+
+ for (gpa = pkvm_ucall_pool_gpa & ~(granule - 1); gpa < end; gpa += granule)
+ GUEST_ASSERT_EQ(pkvm_hvc(ARM_SMCCC_VENDOR_HYP_KVM_MEM_SHARE_FUNC_ID, gpa, 0, 0), 0);
+}
+
+bool pkvm_supported(void)
+{
+ int kvm_fd, vm_fd;
+
+ kvm_fd = open_kvm_dev_path_or_exit();
+ vm_fd = __kvm_ioctl(kvm_fd, KVM_CREATE_VM, (void *)KVM_VM_TYPE_ARM_PROTECTED);
+
+ if (vm_fd >= 0)
+ close(vm_fd);
+ close(kvm_fd);
+
+ return vm_fd >= 0;
+}
+
+/*
+ * The trampoline turns the MMU on, so it runs from an identity-mapped page:
+ * the first guest page of a host page at the top of the IPA space.
+ */
+gpa_t pkvm_idmap_gpa(struct kvm_vm *vm)
+{
+ unsigned int npages = vm_adjust_num_guest_pages(vm->mode, 1);
+
+ return (vm_compute_max_gfn(vm) + 1 - npages) * vm->page_size;
+}
+
+struct kvm_vm *pkvm_vm_create(unsigned int nr_vcpus, u32 features, struct kvm_vcpu *vcpus[])
+{
+ struct vm_shape shape = VM_SHAPE_DEFAULT;
+ struct kvm_vm *vm;
+ gpa_t idmap_gpa;
+ unsigned int i;
+
+ TEST_ASSERT(pvm_entry_end - pvm_entry_start +
+ nr_vcpus * sizeof(struct pvm_boot_args) <= MIN_PAGE_SIZE,
+ "trampoline and %u boot-args blocks overflow the idmap page", nr_vcpus);
+
+ shape.type = KVM_VM_TYPE_ARM_PROTECTED;
+ vm = __vm_create(shape, nr_vcpus, 0);
+
+ for (i = 0; i < nr_vcpus; i++) {
+ struct kvm_vcpu_init init;
+
+ kvm_get_default_vcpu_target(vm, &init);
+ init.features[0] |= features | BIT(KVM_ARM_VCPU_PSCI_0_2);
+ if (i > 0)
+ init.features[0] |= BIT(KVM_ARM_VCPU_POWER_OFF);
+
+ vcpus[i] = aarch64_vcpu_add(vm, i, &init, NULL);
+ }
+ kvm_arch_vm_finalize_vcpus(vm);
+
+ idmap_gpa = pkvm_idmap_gpa(vm);
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, idmap_gpa, PKVM_IDMAP_MEMSLOT,
+ vm_adjust_num_guest_pages(vm->mode, 1), 0);
+ memcpy(addr_gpa2hva(vm, idmap_gpa), pvm_entry_start, pvm_entry_end - pvm_entry_start);
+ virt_map(vm, idmap_gpa, idmap_gpa, 1);
+
+ vm_init_descriptor_tables(vm);
+ for (i = 0; i < nr_vcpus; i++) {
+ vcpu_set_reg(vcpus[i], ARM64_CORE_REG(regs.pc), idmap_gpa);
+ vcpu_init_descriptor_tables(vcpus[i]);
+ }
+
+ write_guest_global(vm, pkvm_ucall_pool_gpa, get_ucall_pool_gpa(vm));
+ write_guest_global(vm, pkvm_ucall_pool_size,
+ ucall_nr_pages_required(vm->page_size) * vm->page_size);
+
+ return vm;
+}
+
+/* Every block must be written before the primary's first run, when the host donates the page. */
+static gpa_t pkvm_boot_args_gpa(struct kvm_vcpu *vcpu)
+{
+ struct kvm_vm *vm = vcpu->vm;
+
+ return pkvm_idmap_gpa(vm) + vm->page_size - (vcpu->id + 1) * sizeof(struct pvm_boot_args);
+}
+
+static void pkvm_snapshot_boot_args(struct kvm_vcpu *vcpu, void *guest_code)
+{
+ struct pvm_boot_args *args = addr_gpa2hva(vcpu->vm, pkvm_boot_args_gpa(vcpu));
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(args->regs); i++)
+ args->regs[i] = vcpu_get_reg(vcpu, ARM64_CORE_REG(regs.regs[i]));
+
+ args->jump_tgt = (u64)guest_code;
+ args->cpacr_el1 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_CPACR_EL1));
+ args->sctlr_el1 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_SCTLR_EL1));
+ args->tcr_el1 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_TCR_EL1));
+ args->mair_el1 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_MAIR_EL1));
+ args->ttbr0_el1 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_TTBR0_EL1));
+ args->vbar_el1 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_VBAR_EL1));
+ args->tpidr_el1 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_TPIDR_EL1));
+ args->sp_el1 = vcpu_get_reg(vcpu, ARM64_CORE_REG(sp_el1));
+}
+
+void pkvm_vcpu_setup_boot(struct kvm_vcpu *vcpu, void *guest_code)
+{
+ pkvm_snapshot_boot_args(vcpu, guest_code);
+ vcpu_set_reg(vcpu, ARM64_CORE_REG(regs.regs[0]), pkvm_boot_args_gpa(vcpu));
+}
+
+gpa_t pkvm_vcpu_setup_boot_secondary(struct kvm_vcpu *vcpu, void *guest_code)
+{
+ int ret;
+
+ pkvm_snapshot_boot_args(vcpu, guest_code);
+
+ /*
+ * A guest's CPU_ON requires the target's hyp vCPU, which KVM_RUN creates.
+ * With immediate_exit set, KVM_RUN returns before the vCPU runs.
+ */
+ vcpu->run->immediate_exit = 1;
+ ret = __vcpu_run(vcpu);
+ vcpu->run->immediate_exit = 0;
+ TEST_ASSERT(ret == -1 && errno == EINTR,
+ "secondary hyp-vCPU registration: rc %d errno %d", ret, errno);
+
+ return pkvm_boot_args_gpa(vcpu);
+}
+
+struct kvm_vm *pkvm_vm_create_with_one_vcpu(struct kvm_vcpu **vcpu, void *guest_code)
+{
+ struct kvm_vm *vm = pkvm_vm_create(1, 0, vcpu);
+
+ pkvm_vcpu_setup_boot(*vcpu, guest_code);
+ return vm;
+}
+
+void pkvm_run_loop(struct kvm_vcpu *vcpu, void (*on_sync)(struct kvm_vcpu *vcpu, struct ucall *uc))
+{
+ bool done = false;
+ struct ucall uc;
+
+ while (!done) {
+ u64 cmd;
+
+ vcpu_run(vcpu);
+
+ switch (cmd = get_ucall(vcpu, &uc)) {
+ case UCALL_SYNC:
+ if (on_sync)
+ on_sync(vcpu, &uc);
+ break;
+ case UCALL_DONE:
+ done = true;
+ break;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ break;
+ case UCALL_PRINTF:
+ REPORT_GUEST_PRINTF(uc);
+ break;
+ default:
+ TEST_FAIL("Unexpected guest exit: ucall %lu, arg %lu", cmd, uc.args[1]);
+ }
+ }
+}
+
+/* Bytes the current process has locked, per RLIMIT_MEMLOCK accounting. */
+size_t pkvm_host_locked_vm(void)
+{
+ size_t size = 0, locked_kb = 0;
+ char *line = NULL;
+ FILE *f;
+
+ f = fopen("/proc/self/status", "r");
+ TEST_ASSERT(f, "fopen /proc/self/status");
+
+ while (getline(&line, &size, f) > 0) {
+ if (sscanf(line, "VmLck:\t%zu kB", &locked_kb) == 1)
+ break;
+ }
+
+ free(line);
+ fclose(f);
+
+ return locked_kb << 10;
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 07/16] KVM: arm64: selftests: Add pkvm_host_abi host-interface test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (5 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 06/16] KVM: arm64: selftests: Add protected-VM library helpers Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 08/16] KVM: arm64: selftests: Add pkvm_psci multi-vCPU PSCI test Fuad Tabba
` (9 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
The host isn't trusted with a protected guest's state, so once a
protected vCPU has run, KVM rejects the ioctls that would read or change
that state. The host can still inject a virtual SError and read back
whether one is pending. The VM gets only the capabilities and vCPU
features pKVM supports.
Check what the host's KVM interface allows and rejects for a protected
VM, before and after the first run, and that the host accounts the VM's
memory as locked until the VM is freed.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/pkvm_host_abi.c | 428 ++++++++++++++++++
2 files changed, 429 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_host_abi.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index e173a9adad3f2..42e530e0e573b 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -190,6 +190,7 @@ TEST_GEN_PROGS_arm64 += arm64/nv_pre_fault_memory_test
TEST_GEN_PROGS_arm64 += arm64/external_aborts
TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
+TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
TEST_GEN_PROGS_arm64 += arm64/set_id_regs
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_host_abi.c b/tools/testing/selftests/kvm/arm64/pkvm_host_abi.c
new file mode 100644
index 0000000000000..4e048c6c2752d
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_host_abi.c
@@ -0,0 +1,428 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_host_abi - what the host's KVM interface allows a protected VM.
+ *
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <errno.h>
+#include <sys/mman.h>
+
+#include <linux/bits.h>
+#include <linux/psci.h>
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+#define TEST_MEMSLOT (PKVM_IDMAP_MEMSLOT + 1)
+#define TEST_GPA BIT_ULL(30)
+#define TEST_PAGES 16
+
+/* Every probe sets PSCI_0_2, so that VCPU_INIT fails only on the feature under test. */
+#define PVM_INIT_FEATURES BIT(KVM_ARM_VCPU_PSCI_0_2)
+
+#define CAP(c) { KVM_CAP_##c, "KVM_CAP_" #c }
+
+/* Caps a protected VM has on any pKVM host. */
+static const struct {
+ long cap;
+ const char *name;
+} allowed_caps[] = {
+ CAP(ONE_REG),
+ CAP(ARM_PSCI),
+ CAP(ARM_PSCI_0_2),
+ CAP(NR_VCPUS),
+ CAP(MAX_VCPUS),
+ CAP(MAX_VCPU_ID),
+ CAP(ARM_VM_IPA_SIZE),
+ CAP(MP_STATE),
+ CAP(VCPU_EVENTS),
+ CAP(VCPU_ATTRIBUTES),
+ CAP(IMMEDIATE_EXIT),
+ CAP(IOEVENTFD),
+ CAP(IRQFD_RESAMPLE),
+ CAP(ARM_IRQ_LINE_LAYOUT_2),
+};
+
+/* Set in the guest's copy by the host before the first run. */
+static u64 test_size;
+
+static void guest_finalize(void)
+{
+ pkvm_guest_init();
+ GUEST_DONE();
+}
+
+/* One write per MIN_PAGE_SIZE faults in every page of the test memslot. */
+static void guest_touch_memory(void)
+{
+ volatile u64 *end = (u64 *)(TEST_GPA + test_size);
+ volatile u64 *p;
+
+ pkvm_guest_init();
+
+ for (p = (u64 *)TEST_GPA; p < end; p += MIN_PAGE_SIZE / sizeof(*p))
+ *p = 1;
+
+ GUEST_DONE();
+}
+
+/* A protected VM with one vCPU and no VCPU_INIT yet. */
+static struct kvm_vcpu *create_uninitialized_vcpu(struct kvm_vm **vm_out)
+{
+ struct vm_shape shape = VM_SHAPE_DEFAULT;
+
+ shape.type = KVM_VM_TYPE_ARM_PROTECTED;
+ *vm_out = __vm_create(shape, 1, 0);
+ return __vm_vcpu_add(*vm_out, 0);
+}
+
+/* The errno of KVM_ARM_VCPU_INIT with @features on a protected vCPU, or 0. */
+static int vcpu_init_errno(u32 features)
+{
+ struct kvm_vcpu_init init;
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ int err;
+
+ vcpu = create_uninitialized_vcpu(&vm);
+ kvm_get_default_vcpu_target(vm, &init);
+ init.features[0] |= features;
+
+ err = __vcpu_ioctl(vcpu, KVM_ARM_VCPU_INIT, &init) ? errno : 0;
+
+ kvm_vm_free(vm);
+ return err;
+}
+
+static void assert_feature_accepted(u32 feature, const char *name)
+{
+ int err = vcpu_init_errno(PVM_INIT_FEATURES | feature);
+
+ TEST_ASSERT(!err, "%s on a protected vCPU: errno %d", name, err);
+ ksft_test_result_pass("%s accepted at VCPU_INIT\n", name);
+}
+
+static void assert_feature_rejected(u32 feature, const char *name)
+{
+ int err = vcpu_init_errno(PVM_INIT_FEATURES | feature);
+
+ TEST_ASSERT(err == EINVAL, "%s on a protected vCPU: errno %d", name, err);
+ ksft_test_result_pass("%s rejected at VCPU_INIT\n", name);
+}
+
+static void test_vcpu_init_conditional_features(void)
+{
+ int err = vcpu_init_errno(0);
+
+ TEST_ASSERT(err == EINVAL, "no PSCI_0_2 on a protected vCPU: errno %d", err);
+ assert_feature_accepted(BIT(KVM_ARM_VCPU_PSCI_0_2), "PSCI_0_2");
+
+ if (kvm_has_cap(KVM_CAP_ARM_PMU_V3))
+ assert_feature_rejected(BIT(KVM_ARM_VCPU_PMU_V3), "PMU_V3");
+ else
+ ksft_test_result_skip("PMU_V3 rejected at VCPU_INIT: PMUv3 unavailable\n");
+
+ if (kvm_has_cap(KVM_CAP_ARM_SVE))
+ assert_feature_rejected(BIT(KVM_ARM_VCPU_SVE), "SVE");
+ else
+ ksft_test_result_skip("SVE rejected at VCPU_INIT: SVE unavailable\n");
+
+ if (kvm_has_cap(KVM_CAP_ARM_PTRAUTH_ADDRESS) && kvm_has_cap(KVM_CAP_ARM_PTRAUTH_GENERIC))
+ assert_feature_accepted(BIT(KVM_ARM_VCPU_PTRAUTH_ADDRESS) |
+ BIT(KVM_ARM_VCPU_PTRAUTH_GENERIC), "PtrAuth");
+ else
+ ksft_test_result_skip("PtrAuth accepted at VCPU_INIT: PtrAuth unavailable\n");
+}
+
+/* KVM_ARM_VCPU_INIT can be repeated until the vCPU's first run. */
+static void test_vcpu_init_repeated(void)
+{
+ struct kvm_vcpu_init init;
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vcpu = create_uninitialized_vcpu(&vm);
+ kvm_get_default_vcpu_target(vm, &init);
+ init.features[0] |= PVM_INIT_FEATURES;
+ vcpu_ioctl(vcpu, KVM_ARM_VCPU_INIT, &init);
+ vcpu_ioctl(vcpu, KVM_ARM_VCPU_INIT, &init);
+ kvm_vm_free(vm);
+
+ ksft_test_result_pass("VCPU_INIT repeated before the first run\n");
+}
+
+static void assert_ext(struct kvm_vm *vm, long ext, bool want_nonzero, const char *name)
+{
+ int r = __vm_ioctl(vm, KVM_CHECK_EXTENSION, (void *)ext);
+
+ if (want_nonzero)
+ TEST_ASSERT(r > 0, "%s: expected nonzero, got %d", name, r);
+ else
+ TEST_ASSERT(r == 0, "%s: expected 0, got %d", name, r);
+}
+
+static void test_caps(void)
+{
+ struct kvm_vm *vm = vm_create_barebones_type(KVM_VM_TYPE_ARM_PROTECTED);
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(allowed_caps); i++) {
+ assert_ext(vm, allowed_caps[i].cap, true, allowed_caps[i].name);
+ ksft_test_result_pass("%s allowed\n", allowed_caps[i].name);
+ }
+
+ if (kvm_has_cap(KVM_CAP_ARM_PTRAUTH_ADDRESS) && kvm_has_cap(KVM_CAP_ARM_PTRAUTH_GENERIC)) {
+ assert_ext(vm, KVM_CAP_ARM_PTRAUTH_ADDRESS, true, "KVM_CAP_ARM_PTRAUTH_ADDRESS");
+ assert_ext(vm, KVM_CAP_ARM_PTRAUTH_GENERIC, true, "KVM_CAP_ARM_PTRAUTH_GENERIC");
+ ksft_test_result_pass("KVM_CAP_ARM_PTRAUTH_* allowed\n");
+ } else {
+ ksft_test_result_skip("KVM_CAP_ARM_PTRAUTH_* allowed: PtrAuth unavailable\n");
+ }
+
+ if (kvm_has_cap(KVM_CAP_ARM_SVE)) {
+ assert_ext(vm, KVM_CAP_ARM_SVE, false, "KVM_CAP_ARM_SVE");
+ ksft_test_result_pass("KVM_CAP_ARM_SVE blocked\n");
+ } else {
+ ksft_test_result_skip("KVM_CAP_ARM_SVE blocked: SVE unavailable\n");
+ }
+
+ if (kvm_has_cap(KVM_CAP_ARM_PMU_V3)) {
+ assert_ext(vm, KVM_CAP_ARM_PMU_V3, false, "KVM_CAP_ARM_PMU_V3");
+ ksft_test_result_pass("KVM_CAP_ARM_PMU_V3 blocked\n");
+ } else {
+ ksft_test_result_skip("KVM_CAP_ARM_PMU_V3 blocked: PMUv3 unavailable\n");
+ }
+
+ assert_ext(vm, KVM_CAP_READONLY_MEM, false, "KVM_CAP_READONLY_MEM");
+ ksft_test_result_pass("KVM_CAP_READONLY_MEM blocked\n");
+
+ assert_ext(vm, KVM_CAP_SET_GUEST_DEBUG, false, "KVM_CAP_SET_GUEST_DEBUG");
+ ksft_test_result_pass("KVM_CAP_SET_GUEST_DEBUG blocked\n");
+
+ assert_ext(vm, KVM_CAP_ARM_INJECT_EXT_DABT, false, "KVM_CAP_ARM_INJECT_EXT_DABT");
+ ksft_test_result_pass("KVM_CAP_ARM_INJECT_EXT_DABT blocked\n");
+
+ /* Blocked explicitly, and reads 0 anyway on a host without MTE. */
+ assert_ext(vm, KVM_CAP_ARM_MTE, false, "KVM_CAP_ARM_MTE");
+ ksft_test_result_pass("KVM_CAP_ARM_MTE blocked\n");
+
+ kvm_vm_free(vm);
+}
+
+/* VM ioctls whose capability a protected VM doesn't get fail with EINVAL. */
+static void test_vm_ioctls_rejected(void)
+{
+ struct kvm_vm *vm = vm_create_barebones_type(KVM_VM_TYPE_ARM_PROTECTED);
+ struct kvm_arm_counter_offset offset = {};
+ struct reg_mask_range range = {};
+ int ret;
+
+ ret = __kvm_has_device_attr(vm->fd, KVM_ARM_VM_SMCCC_CTRL, KVM_ARM_VM_SMCCC_FILTER);
+ TEST_ASSERT(ret == -1 && errno == EINVAL, "SMCCC_CTRL: rc %d errno %d", ret, errno);
+ ret = __vm_ioctl(vm, KVM_ARM_SET_COUNTER_OFFSET, &offset);
+ TEST_ASSERT(ret == -1 && errno == EINVAL, "SET_COUNTER_OFFSET: rc %d errno %d", ret, errno);
+ ret = __vm_ioctl(vm, KVM_ARM_GET_REG_WRITABLE_MASKS, &range);
+ TEST_ASSERT(ret == -1 && errno == EINVAL, "GET_REG_WRITABLE_MASKS: rc %d errno %d",
+ ret, errno);
+
+ kvm_vm_free(vm);
+ ksft_test_result_pass("VM ioctls without their capability rejected\n");
+}
+
+/* The host allows every ONE_REG until a protected vCPU's first KVM_RUN. */
+static void test_one_reg_gate(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ u64 val;
+ int ret;
+
+ vm = pkvm_vm_create(1, 0, &vcpu);
+
+ /*
+ * pkvm_vcpu_setup_boot() snapshots regs[0..7] and SCTLR_EL1 for the
+ * trampoline, so write back SCTLR_EL1 unchanged and probe regs[10].
+ */
+ ret = __vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_SCTLR_EL1), &val);
+ TEST_ASSERT_EQ(ret, 0);
+ ret = __vcpu_set_reg(vcpu, KVM_ARM64_SYS_REG(SYS_SCTLR_EL1), val);
+ TEST_ASSERT_EQ(ret, 0);
+ ret = __vcpu_get_reg(vcpu, ARM64_CORE_REG(regs.pc), &val);
+ TEST_ASSERT_EQ(ret, 0);
+ ret = __vcpu_set_reg(vcpu, ARM64_CORE_REG(regs.regs[10]), 0x1234);
+ TEST_ASSERT_EQ(ret, 0);
+
+ pkvm_vcpu_setup_boot(vcpu, guest_finalize);
+ pkvm_run_loop(vcpu, NULL);
+
+ ret = __vcpu_set_reg(vcpu, KVM_ARM64_SYS_REG(SYS_SCTLR_EL1), 0);
+ TEST_ASSERT(ret == -1 && errno == EPERM, "non-CORE post-run: rc %d errno %d", ret, errno);
+ ret = __vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_SCTLR_EL1), &val);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "non-CORE get post-run: rc %d errno %d", ret, errno);
+
+ ret = __vcpu_get_reg(vcpu, ARM64_CORE_REG(regs.pc), &val);
+ TEST_ASSERT(ret == -1 && errno == EPERM, "CORE pc post-run: rc %d errno %d", ret, errno);
+ ret = __vcpu_set_reg(vcpu, ARM64_CORE_REG(regs.regs[4]), 0);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "CORE regs[4] post-run: rc %d errno %d", ret, errno);
+ ksft_test_result_pass("ONE_REG gate\n");
+
+ kvm_vm_free(vm);
+}
+
+static void test_vcpu_ioctl_gates(void)
+{
+ struct kvm_guest_debug dbg = { .control = KVM_GUESTDBG_ENABLE };
+ struct kvm_vcpu_events events = {};
+ struct kvm_vcpu_init init;
+ struct kvm_vcpu *vcpu;
+ u64 ipa = TEST_GPA;
+ struct kvm_vm *vm;
+ int ret;
+
+ vm = pkvm_vm_create(1, 0, &vcpu);
+
+ ret = __vcpu_ioctl(vcpu, KVM_SET_GUEST_DEBUG, &dbg);
+ TEST_ASSERT(ret == -1 && errno == EPERM, "pre-run SET_GUEST_DEBUG: rc %d errno %d",
+ ret, errno);
+ /* EL2 implements PSCI 1.1, so the host can't set an older version. */
+ ret = __vcpu_set_reg(vcpu, KVM_REG_ARM_PSCI_VERSION, PSCI_VERSION(1, 0));
+ TEST_ASSERT(ret == -1 && errno == EINVAL, "PSCI_VERSION 1.0: rc %d errno %d", ret, errno);
+ vcpu_set_reg(vcpu, KVM_REG_ARM_PSCI_VERSION, PSCI_VERSION(1, 1));
+ ksft_test_result_pass("pre-run: SET_GUEST_DEBUG refused, PSCI_VERSION 1.1 at least\n");
+
+ pkvm_vcpu_setup_boot(vcpu, guest_finalize);
+ pkvm_run_loop(vcpu, NULL);
+
+ ret = __vcpu_ioctl(vcpu, KVM_SET_GUEST_DEBUG, &dbg);
+ TEST_ASSERT(ret == -1 && errno == EPERM, "post-run SET_GUEST_DEBUG: rc %d errno %d",
+ ret, errno);
+ kvm_get_default_vcpu_target(vm, &init);
+ init.features[0] |= PVM_INIT_FEATURES;
+ ret = __vcpu_ioctl(vcpu, KVM_ARM_VCPU_INIT, &init);
+ TEST_ASSERT(ret == -1 && errno == EPERM, "post-run VCPU_INIT: rc %d errno %d", ret, errno);
+
+ events.exception.ext_dabt_pending = 1;
+ ret = __vcpu_ioctl(vcpu, KVM_SET_VCPU_EVENTS, &events);
+ TEST_ASSERT(ret == -1 && errno == EPERM, "ext_dabt_pending: rc %d errno %d", ret, errno);
+ events.exception.ext_dabt_pending = 0;
+ events.exception.serror_pending = 1;
+ vcpu_events_set(vcpu, &events);
+ memset(&events, 0, sizeof(events));
+ vcpu_events_get(vcpu, &events);
+ TEST_ASSERT(events.exception.serror_pending, "post-run SError not read back");
+
+ ret = __vcpu_has_device_attr(vcpu, KVM_ARM_VCPU_PVTIME_CTRL, KVM_ARM_VCPU_PVTIME_IPA);
+ TEST_ASSERT(ret == -1 && errno == ENXIO, "HAS PVTIME_IPA: rc %d errno %d", ret, errno);
+ ret = __vcpu_device_attr_get(vcpu, KVM_ARM_VCPU_PVTIME_CTRL, KVM_ARM_VCPU_PVTIME_IPA, &ipa);
+ TEST_ASSERT(ret == -1 && errno == ENXIO, "GET PVTIME_IPA: rc %d errno %d", ret, errno);
+ ret = __vcpu_device_attr_set(vcpu, KVM_ARM_VCPU_PVTIME_CTRL, KVM_ARM_VCPU_PVTIME_IPA, &ipa);
+ TEST_ASSERT(ret == -1 && errno == ENXIO, "SET PVTIME_IPA: rc %d errno %d", ret, errno);
+
+ kvm_vm_free(vm);
+ ksft_test_result_pass("post-run: debug, VCPU_INIT, abort injection and PVTIME refused\n");
+}
+
+/* GET_REG_LIST is gated on VCPU_INIT, not on the vCPU having run. */
+static void test_get_reg_list_gate(void)
+{
+ struct kvm_reg_list reg_list = { .n = 0 };
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ int ret;
+
+ vcpu = create_uninitialized_vcpu(&vm);
+ ret = __vcpu_ioctl(vcpu, KVM_GET_REG_LIST, ®_list);
+ TEST_ASSERT(ret == -1 && errno == ENOEXEC, "uninitialized: rc %d errno %d", ret, errno);
+ kvm_vm_free(vm);
+
+ vm = pkvm_vm_create(1, 0, &vcpu);
+ free(vcpu_get_reg_list(vcpu));
+ kvm_vm_free(vm);
+ ksft_test_result_pass("GET_REG_LIST gate\n");
+}
+
+static void test_memslot_flags_rejected(void)
+{
+ size_t size = getpagesize();
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ void *hva;
+ int ret;
+
+ vm = pkvm_vm_create(1, 0, &vcpu);
+
+ hva = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ TEST_ASSERT(hva != MAP_FAILED, "mmap failed: %d", errno);
+
+ ret = __vm_set_user_memory_region2(vm, TEST_MEMSLOT, KVM_MEM_READONLY, TEST_GPA,
+ size, hva, -1, 0);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "read-only memslot on a protected VM: rc %d errno %d", ret, errno);
+ ksft_test_result_pass("read-only memslot rejected at creation\n");
+
+ ret = __vm_set_user_memory_region2(vm, TEST_MEMSLOT, KVM_MEM_LOG_DIRTY_PAGES, TEST_GPA,
+ size, hva, -1, 0);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "dirty-logging memslot on a protected VM: rc %d errno %d", ret, errno);
+
+ vm_set_user_memory_region2(vm, TEST_MEMSLOT, 0, TEST_GPA, size, hva, -1, 0);
+ ret = __vm_set_user_memory_region2(vm, TEST_MEMSLOT, KVM_MEM_LOG_DIRTY_PAGES, TEST_GPA,
+ size, hva, -1, 0);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "dirty logging enabled on a protected VM: rc %d errno %d", ret, errno);
+ ksft_test_result_pass("dirty logging rejected at creation and when enabled\n");
+
+ munmap(hva, size);
+ kvm_vm_free(vm);
+}
+
+/* The host pins and accounts a protected VM's memory until the VM is freed. */
+static void test_memory_locked(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ size_t locked;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_touch_memory);
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, TEST_GPA, TEST_MEMSLOT,
+ TEST_PAGES, 0);
+ virt_map(vm, TEST_GPA, TEST_GPA, TEST_PAGES);
+ write_guest_global(vm, test_size, TEST_PAGES * vm->page_size);
+
+ pkvm_run_loop(vcpu, NULL);
+ locked = pkvm_host_locked_vm();
+ TEST_ASSERT(locked > TEST_PAGES * vm->page_size,
+ "guest memory not locked while the VM is alive: %zu bytes", locked);
+
+ kvm_vm_free(vm);
+ TEST_ASSERT_EQ(pkvm_host_locked_vm(), 0);
+
+ ksft_test_result_pass("memory locked while running\n");
+}
+
+int main(void)
+{
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(ARRAY_SIZE(allowed_caps) + 21);
+
+ assert_feature_rejected(BIT(KVM_ARM_VCPU_EL1_32BIT), "EL1_32BIT");
+ test_vcpu_init_conditional_features();
+ test_vcpu_init_repeated();
+ test_caps();
+ test_vm_ioctls_rejected();
+ test_one_reg_gate();
+ test_vcpu_ioctl_gates();
+ test_get_reg_list_gate();
+ test_memslot_flags_rejected();
+ test_memory_locked();
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 08/16] KVM: arm64: selftests: Add pkvm_psci multi-vCPU PSCI test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (6 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 07/16] KVM: arm64: selftests: Add pkvm_host_abi host-interface test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 09/16] KVM: arm64: selftests: Add pkvm_mem_share hypercall test Fuad Tabba
` (8 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
EL2 handles a protected guest's CPU_ON and CPU_OFF itself, so the host
can't power on a vCPU the guest hasn't, or change where it boots. EL2
also handles PSCI_VERSION and PSCI_FEATURES, and returns NOT_SUPPORTED
for unknown calls. It forwards the rest to the host.
Test CPU_ON, CPU_OFF, AFFINITY_INFO, PSCI_VERSION, PSCI_FEATURES,
SYSTEM_OFF, SYSTEM_RESET and SYSTEM_RESET2. Also check that the host
can't make a second vCPU runnable without a CPU_ON from the guest. Run
the secondaries on their own threads to race CPU_ON against CPU_OFF and
against another CPU_ON. EL2 marks a vCPU off at its CPU_OFF and returns
to the host. Until the host powers that vCPU off, a CPU_ON of it returns
ALREADY_ON.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
tools/testing/selftests/kvm/arm64/pkvm_psci.c | 632 ++++++++++++++++++
2 files changed, 633 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_psci.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 42e530e0e573b..aa256fb66d226 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -191,6 +191,7 @@ TEST_GEN_PROGS_arm64 += arm64/external_aborts
TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
+TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
TEST_GEN_PROGS_arm64 += arm64/set_id_regs
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_psci.c b/tools/testing/selftests/kvm/arm64/pkvm_psci.c
new file mode 100644
index 0000000000000..07a50af9c9ad9
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_psci.c
@@ -0,0 +1,632 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_psci - PSCI in a protected VM, including CPU_ON racing CPU_OFF and CPU_ON.
+ *
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <pthread.h>
+
+#include <linux/bitfield.h>
+#include <linux/psci.h>
+#include <asm/barrier.h>
+#include <asm/cputype.h>
+
+#include "arch_timer.h"
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+#define MAX_VCPUS 3
+
+/* A well-formed MPIDR with no matching vCPU. */
+#define BOGUS_MPIDR 0xf
+
+/* The primary's GUEST_SYNC: the host runs the secondary, then resumes the primary. */
+#define SYNC_RUN_SECONDARY 1
+
+#define STRESS_BOOTS 50
+#define ALT_BOOTS 50
+#define HANDSHAKE_CYCLES 50
+#define RACE_ROUNDS 10
+
+#define WAIT_MS 30000
+
+/* An identity-mapped page for the boot context the primary writes. */
+#define ALT_MEMSLOT (PKVM_IDMAP_MEMSLOT + 1)
+#define ALT_CTX BIT_ULL(30)
+
+/* Set in the guest's copy by the host before the first run. */
+static u64 boot_entry;
+static u64 vcpu_mpidr[MAX_VCPUS];
+static u64 vcpu_ctx[MAX_VCPUS];
+
+static u32 boots;
+static u64 expected_ctx;
+static u64 booted_ctx;
+static u32 go_off;
+
+static u32 race_ready;
+static u32 race_go;
+static u32 race_done;
+static unsigned long race_ret[2];
+
+#define wait_until(cond) \
+do { \
+ u64 __end = timer_get_cntct(VIRTUAL) + msec_to_cycles(WAIT_MS); \
+ \
+ while (!(cond)) \
+ __GUEST_ASSERT(timer_get_cntct(VIRTUAL) < __end, \
+ "timed out waiting for %s", #cond); \
+} while (0)
+
+static unsigned long __psci_cpu_on(u64 mpidr, u64 entry, u64 ctx)
+{
+ return pkvm_hvc(PSCI_0_2_FN64_CPU_ON, mpidr, entry, ctx);
+}
+
+static unsigned long psci_cpu_on(int id)
+{
+ return __psci_cpu_on(vcpu_mpidr[id], boot_entry, vcpu_ctx[id]);
+}
+
+static unsigned long psci_affinity_info(int id)
+{
+ return pkvm_hvc(PSCI_0_2_FN64_AFFINITY_INFO, vcpu_mpidr[id], 0, 0);
+}
+
+static void psci_cpu_off(void)
+{
+ pkvm_hvc(PSCI_0_2_FN_CPU_OFF, 0, 0, 0);
+ GUEST_ASSERT(0);
+}
+
+static void guest_primary(void)
+{
+ pkvm_guest_init();
+
+ GUEST_ASSERT_EQ(pkvm_hvc(PSCI_0_2_FN_PSCI_VERSION, 0, 0, 0), PSCI_VERSION(1, 1));
+ GUEST_ASSERT_EQ(pkvm_hvc(PSCI_1_0_FN_PSCI_FEATURES, PSCI_0_2_FN64_CPU_ON, 0, 0),
+ PSCI_RET_SUCCESS);
+ GUEST_ASSERT_EQ(pkvm_hvc(PSCI_1_0_FN_PSCI_FEATURES, PSCI_0_2_FN_MIGRATE, 0, 0),
+ PSCI_RET_NOT_SUPPORTED);
+
+ GUEST_ASSERT_EQ(psci_affinity_info(1), PSCI_0_2_AFFINITY_LEVEL_OFF);
+ GUEST_ASSERT_EQ(psci_cpu_on(1), PSCI_RET_SUCCESS);
+
+ /* The host handles AFFINITY_INFO, and returns ON for a CPU_ON target. */
+ GUEST_ASSERT_EQ(psci_affinity_info(1), PSCI_0_2_AFFINITY_LEVEL_ON);
+ GUEST_ASSERT_EQ(psci_cpu_on(1), PSCI_RET_ON_PENDING);
+
+ GUEST_SYNC(SYNC_RUN_SECONDARY);
+
+ GUEST_ASSERT_EQ(psci_affinity_info(1), PSCI_0_2_AFFINITY_LEVEL_ON);
+ GUEST_ASSERT_EQ(psci_cpu_on(1), PSCI_RET_ALREADY_ON);
+
+ GUEST_ASSERT_EQ(__psci_cpu_on(BOGUS_MPIDR, boot_entry, 0), PSCI_RET_INVALID_PARAMS);
+
+ GUEST_DONE();
+}
+
+/* The primary's pkvm_guest_init() set up ucall for the whole VM. */
+static void guest_secondary(void)
+{
+ GUEST_DONE();
+}
+
+static void guest_cpu_on_unregistered(void)
+{
+ pkvm_guest_init();
+ GUEST_ASSERT_EQ(psci_cpu_on(1), PSCI_RET_INVALID_PARAMS);
+ GUEST_DONE();
+}
+
+static void guest_boot_and_done(void)
+{
+ pkvm_guest_init();
+ GUEST_DONE();
+}
+
+static void guest_system_off(void)
+{
+ pkvm_hvc(PSCI_0_2_FN_SYSTEM_OFF, 0, 0, 0);
+ GUEST_ASSERT(0);
+}
+
+static void guest_system_reset(void)
+{
+ pkvm_hvc(PSCI_0_2_FN_SYSTEM_RESET, 0, 0, 0);
+ GUEST_ASSERT(0);
+}
+
+static void guest_system_reset2_smc32(void)
+{
+ pkvm_hvc(PSCI_1_1_FN_SYSTEM_RESET2, PSCI_1_1_RESET_TYPE_SYSTEM_WARM_RESET, 0, 0);
+ GUEST_ASSERT(0);
+}
+
+static void guest_system_reset2_smc64(void)
+{
+ pkvm_hvc(PSCI_1_1_FN64_SYSTEM_RESET2, PSCI_1_1_RESET_TYPE_SYSTEM_WARM_RESET, 0, 0);
+ GUEST_ASSERT(0);
+}
+
+static void guest_stress_primary(void)
+{
+ unsigned long successes = 0;
+
+ pkvm_guest_init();
+ while (READ_ONCE(boots) < STRESS_BOOTS) {
+ unsigned long ret = psci_cpu_on(1);
+
+ __GUEST_ASSERT(ret == PSCI_RET_SUCCESS || ret == PSCI_RET_ALREADY_ON ||
+ ret == PSCI_RET_ON_PENDING, "CPU_ON 0x%lx", ret);
+ successes += ret == PSCI_RET_SUCCESS;
+ }
+
+ /*
+ * SUCCESS means the secondary ran. The last boot has completed and the
+ * secondary stays on after it, so no CPU_ON and no boot is in flight.
+ */
+ GUEST_ASSERT_EQ(successes, STRESS_BOOTS);
+ GUEST_DONE();
+}
+
+static void guest_stress_secondary(void)
+{
+ u32 k = READ_ONCE(boots) + 1;
+
+ WRITE_ONCE(boots, k);
+ if (k == STRESS_BOOTS)
+ GUEST_DONE();
+ psci_cpu_off();
+}
+
+static void alt_boot(u64 ctx)
+{
+ u32 k = READ_ONCE(boots) + 1;
+
+ GUEST_ASSERT_EQ(ctx, READ_ONCE(expected_ctx));
+ WRITE_ONCE(booted_ctx, ctx);
+ /* Publishes booted_ctx, and the primary changes expected_ctx only once it sees this. */
+ smp_store_release(&boots, k);
+ if (k == ALT_BOOTS)
+ GUEST_DONE();
+ psci_cpu_off();
+}
+
+static void guest_alt_secondary_host_ctx(void)
+{
+ alt_boot(vcpu_ctx[1]);
+}
+
+static void guest_alt_secondary_guest_ctx(void)
+{
+ alt_boot(ALT_CTX);
+}
+
+static void clean_to_poc(void *p, size_t size)
+{
+ u64 line = 4UL << SYS_FIELD_GET(CTR_EL0, DminLine, read_sysreg(ctr_el0));
+ u64 addr;
+
+ for (addr = (u64)p & ~(line - 1); addr < (u64)p + size; addr += line)
+ asm volatile("dc cvac, %0" : : "r"(addr) : "memory");
+ dsb(sy);
+}
+
+static void guest_alt_primary(void)
+{
+ struct pvm_boot_args *alt = (struct pvm_boot_args *)ALT_CTX;
+ bool guest_ctx = true;
+ unsigned long ret;
+ u64 ctx;
+ u32 k;
+
+ pkvm_guest_init();
+
+ /* The trampoline reads the context with the MMU off. */
+ *alt = *(struct pvm_boot_args *)vcpu_ctx[1];
+ alt->jump_tgt = (u64)guest_alt_secondary_guest_ctx;
+ clean_to_poc(alt, sizeof(*alt));
+
+ for (k = 1; k <= ALT_BOOTS; k++) {
+ /*
+ * CPU_ON returns ALREADY_ON until the host has powered the secondary off.
+ * Each attempt passes the other context, so a boot from the previous attempt's
+ * context fails alt_boot()'s check.
+ */
+ do {
+ guest_ctx = !guest_ctx;
+ ctx = guest_ctx ? ALT_CTX : vcpu_ctx[1];
+ WRITE_ONCE(expected_ctx, ctx);
+ ret = __psci_cpu_on(vcpu_mpidr[1], boot_entry, ctx);
+ __GUEST_ASSERT(ret == PSCI_RET_SUCCESS || ret == PSCI_RET_ALREADY_ON,
+ "CPU_ON 0x%lx", ret);
+ } while (ret != PSCI_RET_SUCCESS);
+
+ /* Only the primary powers the secondary on, so no other boot is in flight. */
+ wait_until(smp_load_acquire(&boots) >= k);
+ GUEST_ASSERT_EQ(READ_ONCE(boots), k);
+ GUEST_ASSERT_EQ(READ_ONCE(booted_ctx), ctx);
+ }
+
+ GUEST_DONE();
+}
+
+static void guest_handshake_secondary(void)
+{
+ u32 k = READ_ONCE(boots) + 1;
+
+ WRITE_ONCE(boots, k);
+ if (k > HANDSHAKE_CYCLES)
+ GUEST_DONE();
+ wait_until(READ_ONCE(go_off) == k);
+ psci_cpu_off();
+}
+
+/* The secondary is ON until it has called CPU_OFF. */
+static bool is_off(int id)
+{
+ unsigned long ret = psci_affinity_info(id);
+
+ __GUEST_ASSERT(ret == PSCI_0_2_AFFINITY_LEVEL_ON || ret == PSCI_0_2_AFFINITY_LEVEL_OFF,
+ "AFFINITY_INFO 0x%lx", ret);
+ return ret == PSCI_0_2_AFFINITY_LEVEL_OFF;
+}
+
+static void guest_handshake_primary(void)
+{
+ u32 k;
+
+ pkvm_guest_init();
+
+ for (k = 1; k <= HANDSHAKE_CYCLES; k++) {
+ GUEST_ASSERT_EQ(psci_cpu_on(1), PSCI_RET_SUCCESS);
+
+ /* The secondary has booted and waits for go_off, so it is still ON. */
+ wait_until(READ_ONCE(boots) >= k);
+ GUEST_ASSERT_EQ(psci_affinity_info(1), PSCI_0_2_AFFINITY_LEVEL_ON);
+
+ WRITE_ONCE(go_off, k);
+
+ /* It is OFF and only the primary powers it on, so no boot is in flight. */
+ wait_until(is_off(1));
+ GUEST_ASSERT_EQ(READ_ONCE(boots), k);
+ }
+
+ /* One more boot lets the secondary finish instead of powering off. */
+ GUEST_ASSERT_EQ(psci_cpu_on(1), PSCI_RET_SUCCESS);
+ GUEST_DONE();
+}
+
+static void guest_race_caller(void)
+{
+ WRITE_ONCE(race_ready, 1);
+ wait_until(READ_ONCE(race_go));
+ race_ret[1] = psci_cpu_on(2);
+ /* Publishes race_ret[1] to the primary. */
+ smp_store_release(&race_done, 1);
+ GUEST_DONE();
+}
+
+/* No CPU_OFF: a target that powered off would let the loser win too. */
+static void guest_race_target(void)
+{
+ WRITE_ONCE(boots, READ_ONCE(boots) + 1);
+ GUEST_DONE();
+}
+
+static void guest_race_primary(void)
+{
+ unsigned long loser;
+ int winner;
+
+ pkvm_guest_init();
+
+ /* vCPU 1 has never been on, and the primary is its only caller. */
+ GUEST_ASSERT_EQ(psci_cpu_on(1), PSCI_RET_SUCCESS);
+ wait_until(READ_ONCE(race_ready));
+
+ WRITE_ONCE(race_go, 1);
+ race_ret[0] = psci_cpu_on(2);
+
+ /* Both callers have returned, and vCPU 2 has booted and stays on. */
+ wait_until(smp_load_acquire(&race_done));
+ wait_until(READ_ONCE(boots));
+ GUEST_ASSERT_EQ(READ_ONCE(boots), 1);
+
+ winner = race_ret[0] == PSCI_RET_SUCCESS ? 0 : 1;
+ loser = race_ret[!winner];
+ GUEST_ASSERT_EQ(race_ret[winner], PSCI_RET_SUCCESS);
+ __GUEST_ASSERT(loser == PSCI_RET_ON_PENDING || loser == PSCI_RET_ALREADY_ON,
+ "losing CPU_ON 0x%lx", loser);
+
+ GUEST_SYNC(loser);
+ GUEST_DONE();
+}
+
+static struct kvm_vcpu *secondary_vcpu;
+static u64 sync_arg;
+
+static void run_secondary_on_sync(struct kvm_vcpu *vcpu, struct ucall *uc)
+{
+ if (uc->args[1] == SYNC_RUN_SECONDARY)
+ pkvm_run_loop(secondary_vcpu, NULL);
+}
+
+static void wire_target(struct kvm_vm *vm, struct kvm_vcpu *vcpu)
+{
+ u64 mpidr = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_MPIDR_EL1));
+
+ write_guest_global(vm, vcpu_mpidr[vcpu->id], mpidr & MPIDR_HWID_BITMASK);
+ write_guest_global(vm, boot_entry, pkvm_idmap_gpa(vm));
+}
+
+static void wire_secondary(struct kvm_vm *vm, struct kvm_vcpu *vcpu, void *guest_code)
+{
+ gpa_t ctx;
+
+ wire_target(vm, vcpu);
+ ctx = pkvm_vcpu_setup_boot_secondary(vcpu, guest_code);
+ write_guest_global(vm, vcpu_ctx[vcpu->id], ctx);
+}
+
+static void test_protocol(void)
+{
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+ secondary_vcpu = vcpus[1];
+
+ pkvm_vcpu_setup_boot(vcpus[0], guest_primary);
+ wire_secondary(vm, vcpus[1], guest_secondary);
+
+ pkvm_run_loop(vcpus[0], run_secondary_on_sync);
+
+ kvm_vm_free(vm);
+}
+
+/* vCPU 1 exists but never runs, so EL2 never creates its hyp vCPU. */
+static void test_cpu_on_unregistered(void)
+{
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+
+ pkvm_vcpu_setup_boot(vcpus[0], guest_cpu_on_unregistered);
+ wire_target(vm, vcpus[1]);
+
+ pkvm_run_loop(vcpus[0], NULL);
+
+ kvm_vm_free(vm);
+}
+
+static void assert_system_event(void *guest_code, u32 type, u64 flags)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_run *run;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(1, 0, &vcpu);
+ pkvm_vcpu_setup_boot(vcpu, guest_code);
+
+ run = vcpu->run;
+ vcpu_run(vcpu);
+
+ TEST_ASSERT_KVM_EXIT_REASON(vcpu, KVM_EXIT_SYSTEM_EVENT);
+ TEST_ASSERT_EQ(run->system_event.type, type);
+ TEST_ASSERT_EQ(run->system_event.data[0], flags);
+
+ kvm_vm_free(vm);
+}
+
+/* Only one vCPU may be RUNNABLE when EL2 creates its hyp vCPU: the primary. */
+static void assert_second_runnable_rejected(struct kvm_vcpu *vcpu)
+{
+ int ret = _vcpu_run(vcpu);
+
+ TEST_ASSERT(ret == -1 && errno == EINVAL,
+ "second RUNNABLE vCPU: expected -1/EINVAL, got %d/%d", ret, errno);
+}
+
+static void test_forged_runnable(void)
+{
+ struct kvm_mp_state mps = { .mp_state = KVM_MP_STATE_RUNNABLE };
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+ pkvm_vcpu_setup_boot(vcpus[0], guest_boot_and_done);
+ pkvm_run_loop(vcpus[0], NULL);
+
+ vcpu_mp_state_set(vcpus[1], &mps);
+ assert_second_runnable_rejected(vcpus[1]);
+
+ kvm_vm_free(vm);
+}
+
+static void test_second_primary(void)
+{
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vcpu_init init;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+ pkvm_vcpu_setup_boot(vcpus[0], guest_boot_and_done);
+ pkvm_run_loop(vcpus[0], NULL);
+
+ /* Re-INIT without POWER_OFF also leaves the vCPU RUNNABLE. */
+ kvm_get_default_vcpu_target(vm, &init);
+ init.features[0] |= BIT(KVM_ARM_VCPU_PSCI_0_2);
+ vcpu_ioctl(vcpus[1], KVM_ARM_VCPU_INIT, &init);
+ assert_second_runnable_rejected(vcpus[1]);
+
+ kvm_vm_free(vm);
+}
+
+/* Once EL2 holds a secondary's hyp vCPU off, the host rejects powering it on. */
+static void test_run_gate_off(void)
+{
+ struct kvm_mp_state mps = { .mp_state = KVM_MP_STATE_RUNNABLE };
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vm *vm;
+ int ret;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+
+ pkvm_vcpu_setup_boot_secondary(vcpus[1], guest_boot_and_done);
+
+ ret = __vcpu_ioctl(vcpus[1], KVM_SET_MP_STATE, &mps);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "forged RUNNABLE: expected -1/EPERM, got %d/%d", ret, errno);
+ mps.mp_state = KVM_MP_STATE_SUSPENDED;
+ ret = __vcpu_ioctl(vcpus[1], KVM_SET_MP_STATE, &mps);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "forged SUSPENDED: expected -1/EPERM, got %d/%d", ret, errno);
+
+ kvm_vm_free(vm);
+}
+
+static void *vcpu_thread(void *vcpu)
+{
+ pkvm_run_loop(vcpu, NULL);
+ return NULL;
+}
+
+static void record_sync(struct kvm_vcpu *vcpu, struct ucall *uc)
+{
+ sync_arg = uc->args[1];
+}
+
+/* The secondary runs on its own thread, so a CPU_ON can race its CPU_OFF. */
+static void run_threaded(struct kvm_vcpu *vcpus[])
+{
+ pthread_t thread;
+
+ pthread_create(&thread, NULL, vcpu_thread, vcpus[1]);
+ pkvm_run_loop(vcpus[0], record_sync);
+ pthread_join(thread, NULL);
+}
+
+static void test_stress(void)
+{
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+
+ pkvm_vcpu_setup_boot(vcpus[0], guest_stress_primary);
+ wire_secondary(vm, vcpus[1], guest_stress_secondary);
+
+ run_threaded(vcpus);
+
+ kvm_vm_free(vm);
+}
+
+static void test_alternating_ctx(void)
+{
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, ALT_CTX, ALT_MEMSLOT,
+ vm_adjust_num_guest_pages(vm->mode, 1), 0);
+ virt_map(vm, ALT_CTX, ALT_CTX, 1);
+
+ pkvm_vcpu_setup_boot(vcpus[0], guest_alt_primary);
+ wire_secondary(vm, vcpus[1], guest_alt_secondary_host_ctx);
+
+ run_threaded(vcpus);
+
+ kvm_vm_free(vm);
+}
+
+static void test_handshake(void)
+{
+ struct kvm_vcpu *vcpus[2];
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+
+ pkvm_vcpu_setup_boot(vcpus[0], guest_handshake_primary);
+ wire_secondary(vm, vcpus[1], guest_handshake_secondary);
+
+ run_threaded(vcpus);
+
+ kvm_vm_free(vm);
+}
+
+/* Returns the number of rounds whose loser got ON_PENDING: its CPU_ON overlapped the winner's. */
+static int test_concurrent_cpu_on(void)
+{
+ int i, on_pending = 0;
+
+ for (i = 0; i < RACE_ROUNDS; i++) {
+ struct kvm_vcpu *vcpus[3];
+ pthread_t target;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(3, 0, vcpus);
+
+ pkvm_vcpu_setup_boot(vcpus[0], guest_race_primary);
+ wire_secondary(vm, vcpus[1], guest_race_caller);
+ wire_secondary(vm, vcpus[2], guest_race_target);
+
+ pthread_create(&target, NULL, vcpu_thread, vcpus[2]);
+ run_threaded(vcpus);
+ pthread_join(target, NULL);
+
+ kvm_vm_free(vm);
+
+ on_pending += sync_arg == PSCI_RET_ON_PENDING;
+ }
+
+ return on_pending;
+}
+
+int main(void)
+{
+ int on_pending;
+
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(13);
+
+ test_protocol();
+ ksft_test_result_pass("PSCI protocol and CPU_ON boot\n");
+ test_cpu_on_unregistered();
+ ksft_test_result_pass("CPU_ON of a vCPU with no hyp vCPU\n");
+ assert_system_event(guest_system_off, KVM_SYSTEM_EVENT_SHUTDOWN, 0);
+ ksft_test_result_pass("SYSTEM_OFF\n");
+ assert_system_event(guest_system_reset, KVM_SYSTEM_EVENT_RESET, 0);
+ ksft_test_result_pass("SYSTEM_RESET\n");
+ assert_system_event(guest_system_reset2_smc32, KVM_SYSTEM_EVENT_RESET,
+ KVM_SYSTEM_EVENT_RESET_FLAG_PSCI_RESET2);
+ ksft_test_result_pass("SYSTEM_RESET2 (SMC32)\n");
+ assert_system_event(guest_system_reset2_smc64, KVM_SYSTEM_EVENT_RESET,
+ KVM_SYSTEM_EVENT_RESET_FLAG_PSCI_RESET2);
+ ksft_test_result_pass("SYSTEM_RESET2 (SMC64)\n");
+ test_forged_runnable();
+ ksft_test_result_pass("forged RUNNABLE rejected at hyp vCPU creation\n");
+ test_second_primary();
+ ksft_test_result_pass("second RUNNABLE vCPU rejected\n");
+ test_run_gate_off();
+ ksft_test_result_pass("forged RUNNABLE and SUSPENDED of an OFF vCPU rejected\n");
+ test_stress();
+ ksft_test_result_pass("CPU_ON/CPU_OFF race: %d boots\n", STRESS_BOOTS);
+ test_alternating_ctx();
+ ksft_test_result_pass("CPU_ON alternating context: %d boots\n", ALT_BOOTS);
+ test_handshake();
+ ksft_test_result_pass("CPU_OFF/CPU_ON handshake: %d cycles\n", HANDSHAKE_CYCLES);
+ on_pending = test_concurrent_cpu_on();
+ ksft_test_result_pass("concurrent CPU_ON: one winner in %d rounds, %d ON_PENDING losers\n",
+ RACE_ROUNDS, on_pending);
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 09/16] KVM: arm64: selftests: Add pkvm_mem_share hypercall test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (7 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 08/16] KVM: arm64: selftests: Add pkvm_psci multi-vCPU PSCI test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 10/16] KVM: arm64: selftests: Add pkvm_smccc " Fuad Tabba
` (7 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
A protected guest gives the host access to one of its pages with
MEM_SHARE and takes it back with MEM_UNSHARE. The page is a host page,
whose size HYP_MEMINFO returns. At teardown EL2 zeroes every page the
guest hasn't left shared.
Exercise the three calls, the host's access to shared and private pages,
and the zeroing. Also check that KVM_RUN fails with EIO on
hugetlb-backed memory, that sharing an IPA outside every memslot makes
the guest take an external abort, and that faults each requiring new
stage-2 tables succeed.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/pkvm_mem_share.c | 394 ++++++++++++++++++
2 files changed, 395 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_mem_share.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index aa256fb66d226..88c1159cb3bf1 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -191,6 +191,7 @@ TEST_GEN_PROGS_arm64 += arm64/external_aborts
TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
+TEST_GEN_PROGS_arm64 += arm64/pkvm_mem_share
TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_mem_share.c b/tools/testing/selftests/kvm/arm64/pkvm_mem_share.c
new file mode 100644
index 0000000000000..9a4e96fc8d046
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_mem_share.c
@@ -0,0 +1,394 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_mem_share - MEM_SHARE, MEM_UNSHARE and HYP_MEMINFO, and EL2 zeroing a
+ * protected VM's private pages at teardown.
+ *
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <errno.h>
+#include <setjmp.h>
+#include <signal.h>
+#include <stdio.h>
+
+#include <linux/arm-smccc.h>
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+/*
+ * A page here is a host page, the granule EL2 shares in. Set in the guest's
+ * copy by the host before the first run.
+ */
+static u64 granule;
+
+#define GUEST_MEMSLOT (PKVM_IDMAP_MEMSLOT + 1)
+#define GPA_BASE BIT_ULL(30)
+#define GVA_BASE GPA_BASE
+#define GPA_PAGES 16
+#define GPA_SIZE ((size_t)GPA_PAGES * granule)
+
+/* Each case uses its own pages of the guest memslot. */
+#define P_SHARE 0
+#define P_SHARED_AT_TEARDOWN 1
+
+/* Recognizable value the host must still see in a page that isn't zeroed. */
+#define DIRTY_VAL 0x0badf00ddeadbeefUL
+
+/* An aligned IPA outside every memslot. */
+#define OUTSIDE_IPA BIT_ULL(31)
+
+/* What one last-level stage-2 table maps: 2M on a 4K host. */
+#define BLOCK_SIZE (granule / 8 * granule)
+
+/* One page in each block of a memslot centered on 512G. */
+#define PRESSURE_MEMSLOT (GUEST_MEMSLOT + 1)
+#define PRESSURE_PAGES 64
+#define PRESSURE_SIZE ((u64)PRESSURE_PAGES * BLOCK_SIZE)
+#define PRESSURE_GPA (BIT_ULL(39) - PRESSURE_SIZE / 2)
+#define PRESSURE_GVA BIT_ULL(32)
+
+/* Commands that act on a page take its index. */
+enum guest_cmd {
+ CMD_HOST_INC_SHARED = 1,
+ CMD_HOST_ACCESS_PRIVATE,
+};
+
+#define HOST_CMD(cmd, idx) GUEST_SYNC_ARGS(cmd, idx, 0, 0, 0)
+
+static u64 page_ipa(int idx)
+{
+ return GPA_BASE + (u64)idx * granule;
+}
+
+static volatile u64 *page_va(int idx)
+{
+ return (volatile u64 *)(GVA_BASE + (u64)idx * granule);
+}
+
+static unsigned long hvc_share(u64 ipa, u64 arg2, u64 arg3)
+{
+ return pkvm_hvc(ARM_SMCCC_VENDOR_HYP_KVM_MEM_SHARE_FUNC_ID, ipa, arg2, arg3);
+}
+
+static unsigned long hvc_unshare(u64 ipa, u64 arg2, u64 arg3)
+{
+ return pkvm_hvc(ARM_SMCCC_VENDOR_HYP_KVM_MEM_UNSHARE_FUNC_ID, ipa, arg2, arg3);
+}
+
+static void test_meminfo(void)
+{
+ u32 meminfo = ARM_SMCCC_VENDOR_HYP_KVM_HYP_MEMINFO_FUNC_ID;
+
+ GUEST_ASSERT_EQ(pkvm_hvc(meminfo, 0, 0, 0), granule);
+
+ GUEST_ASSERT_EQ(pkvm_hvc(meminfo, 1, 0, 0), SMCCC_RET_INVALID_PARAMETER);
+ GUEST_ASSERT_EQ(pkvm_hvc(meminfo, 0, 1, 0), SMCCC_RET_INVALID_PARAMETER);
+ GUEST_ASSERT_EQ(pkvm_hvc(meminfo, 0, 0, 1), SMCCC_RET_INVALID_PARAMETER);
+}
+
+static void test_arg_validation(void)
+{
+ u64 ipa = page_ipa(P_SHARE);
+
+ /* Non-zero arg2 or arg3, and an unaligned IPA, are rejected before any page walk. */
+ GUEST_ASSERT_EQ(hvc_share(ipa, 1, 0), SMCCC_RET_INVALID_PARAMETER);
+ GUEST_ASSERT_EQ(hvc_share(ipa, 0, 1), SMCCC_RET_INVALID_PARAMETER);
+ GUEST_ASSERT_EQ(hvc_share(ipa + 0x80, 0, 0), SMCCC_RET_INVALID_PARAMETER);
+ GUEST_ASSERT_EQ(hvc_unshare(ipa, 1, 0), SMCCC_RET_INVALID_PARAMETER);
+ GUEST_ASSERT_EQ(hvc_unshare(ipa, 0, 1), SMCCC_RET_INVALID_PARAMETER);
+ GUEST_ASSERT_EQ(hvc_unshare(ipa + 0x80, 0, 0), SMCCC_RET_INVALID_PARAMETER);
+}
+
+static void test_share_unshare(void)
+{
+ volatile u64 *val = page_va(P_SHARE);
+ u64 ipa = page_ipa(P_SHARE);
+
+ *val = 42;
+
+ HOST_CMD(CMD_HOST_ACCESS_PRIVATE, P_SHARE);
+ GUEST_ASSERT_EQ(*val, 42);
+
+ GUEST_ASSERT_EQ(hvc_share(ipa, 0, 0), SMCCC_RET_SUCCESS);
+ GUEST_ASSERT_EQ(hvc_share(ipa, 0, 0), SMCCC_RET_INVALID_PARAMETER);
+
+ HOST_CMD(CMD_HOST_INC_SHARED, P_SHARE);
+ GUEST_ASSERT_EQ(*val, 43);
+
+ GUEST_ASSERT_EQ(hvc_unshare(ipa, 0, 0), SMCCC_RET_SUCCESS);
+ GUEST_ASSERT_EQ(hvc_unshare(ipa, 0, 0), SMCCC_RET_INVALID_PARAMETER);
+
+ HOST_CMD(CMD_HOST_ACCESS_PRIVATE, P_SHARE);
+ GUEST_ASSERT_EQ(*val, 43);
+}
+
+/* Dirty every page, so that a page that reads 0 after teardown is one EL2 zeroed. */
+static void guest_dirty_region(void)
+{
+ volatile u64 *p = (volatile u64 *)GVA_BASE;
+ size_t i;
+
+ for (i = 0; i < GPA_SIZE / sizeof(*p); i++)
+ p[i] = DIRTY_VAL;
+}
+
+static void guest_code(void)
+{
+ pkvm_guest_init();
+
+ test_meminfo();
+ test_arg_validation();
+ test_share_unshare();
+
+ guest_dirty_region();
+
+ /* Teardown doesn't zero a page the guest left shared. */
+ GUEST_ASSERT_EQ(hvc_share(page_ipa(P_SHARED_AT_TEARDOWN), 0, 0), SMCCC_RET_SUCCESS);
+
+ GUEST_DONE();
+}
+
+static void expect_sea_handler(struct ex_regs *regs)
+{
+ u64 esr = read_sysreg(esr_el1);
+
+ GUEST_ASSERT_EQ(ESR_ELx_EC(esr), ESR_ELx_EC_DABT_CUR);
+ GUEST_ASSERT_EQ(esr & ESR_ELx_FSC_TYPE, ESR_ELx_FSC_EXTABT);
+
+ GUEST_DONE();
+}
+
+static void guest_share_outside(void)
+{
+ pkvm_guest_init();
+
+ hvc_share(OUTSIDE_IPA, 0, 0);
+ GUEST_FAIL("MEM_SHARE of an IPA outside every memslot returned to the guest");
+}
+
+/*
+ * Each fault requires a new stage-2 last-level table, and the first one above
+ * 512G a new table at every level below the root.
+ */
+static void guest_memcache_pressure(void)
+{
+ volatile u64 *p = (volatile u64 *)PRESSURE_GVA;
+ int i;
+
+ pkvm_guest_init();
+
+ for (i = 0; i < PRESSURE_PAGES; i++)
+ p[i * granule / sizeof(*p)] = i + 1;
+ for (i = 0; i < PRESSURE_PAGES; i++)
+ GUEST_ASSERT_EQ(p[i * granule / sizeof(*p)], i + 1);
+
+ GUEST_DONE();
+}
+
+static void guest_touch_one_page(void)
+{
+ pkvm_guest_init();
+
+ *page_va(0) = 1;
+
+ GUEST_DONE();
+}
+
+static sigjmp_buf sigsegv_jmpbuf;
+static volatile void *sigsegv_expected;
+
+/* The test maps the guest memslot itself, so it can read the memory after the VM is freed. */
+static u64 *guest_mem;
+
+static u64 *host_page(int idx)
+{
+ return guest_mem + (size_t)idx * granule / sizeof(*guest_mem);
+}
+
+static void sigsegv_handler(int sig, siginfo_t *si, void *ctx)
+{
+ TEST_ASSERT(si->si_addr == sigsegv_expected,
+ "SIGSEGV at %p, expected %p", si->si_addr, sigsegv_expected);
+ siglongjmp(sigsegv_jmpbuf, 1);
+}
+
+static void host_access_private(int idx)
+{
+ struct sigaction old, sa = {
+ .sa_sigaction = sigsegv_handler,
+ .sa_flags = SA_SIGINFO,
+ };
+ volatile u64 *p = host_page(idx);
+ bool faulted = false;
+
+ sigsegv_expected = (void *)p;
+ sigaction(SIGSEGV, &sa, &old);
+ if (sigsetjmp(sigsegv_jmpbuf, 1) == 0)
+ (*p)++;
+ else
+ faulted = true;
+ sigaction(SIGSEGV, &old, NULL);
+
+ TEST_ASSERT(faulted, "Host accessed private guest memory without faulting");
+}
+
+static void on_sync(struct kvm_vcpu *vcpu, struct ucall *uc)
+{
+ u64 *p = host_page(uc->args[2]);
+
+ switch (uc->args[1]) {
+ case CMD_HOST_INC_SHARED:
+ (*p)++;
+ break;
+ case CMD_HOST_ACCESS_PRIVATE:
+ host_access_private(uc->args[2]);
+ break;
+ default:
+ TEST_FAIL("Unexpected guest command %lu", uc->args[1]);
+ }
+}
+
+static void assert_page(int idx, u64 val)
+{
+ u64 *p = host_page(idx);
+ size_t i;
+
+ for (i = 0; i < granule / sizeof(*p); i++)
+ TEST_ASSERT(p[i] == val, "Guest page %d holds 0x%lx, not 0x%lx, after teardown",
+ idx, p[i], val);
+}
+
+static void test_run(void)
+{
+ const char *name = "share/unshare, and teardown zeroing";
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ int i;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_code);
+ guest_mem = kvm_mmap(GPA_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1);
+ vm_set_user_memory_region2(vm, GUEST_MEMSLOT, 0, GPA_BASE, GPA_SIZE, guest_mem, -1, 0);
+ virt_map(vm, GVA_BASE, GPA_BASE, GPA_SIZE / vm->page_size);
+ write_guest_global(vm, granule, granule);
+
+ pkvm_run_loop(vcpu, on_sync);
+ kvm_vm_free(vm);
+
+ for (i = 0; i < GPA_PAGES; i++)
+ assert_page(i, i == P_SHARED_AT_TEARDOWN ? DIRTY_VAL : 0);
+ kvm_munmap(guest_mem, GPA_SIZE);
+
+ ksft_test_result_pass("%s\n", name);
+}
+
+/*
+ * EL2 turns the share into a stage-2 translation fault on the IPA with ISV clear.
+ * The host finds no memslot for the IPA and injects an external abort.
+ */
+static void test_share_outside_memslot(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_share_outside);
+ vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ESR_ELx_EC_DABT_CUR, expect_sea_handler);
+
+ pkvm_run_loop(vcpu, NULL);
+ kvm_vm_free(vm);
+
+ ksft_test_result_pass("share of an IPA outside every memslot takes an external abort\n");
+}
+
+static void test_memcache_pressure(void)
+{
+ const char *name = "faults that each require new stage-2 tables";
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ void *mem;
+ int i;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_memcache_pressure);
+
+ /* 32G on a 64K host, of which the guest touches one page per block. */
+ mem = kvm_mmap(PRESSURE_SIZE, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1);
+ vm_set_user_memory_region2(vm, PRESSURE_MEMSLOT, 0, PRESSURE_GPA, PRESSURE_SIZE,
+ mem, -1, 0);
+ for (i = 0; i < PRESSURE_PAGES; i++)
+ virt_pg_map(vm, PRESSURE_GVA + i * granule, PRESSURE_GPA + i * BLOCK_SIZE);
+ write_guest_global(vm, granule, granule);
+
+ pkvm_run_loop(vcpu, NULL);
+ kvm_vm_free(vm);
+ kvm_munmap(mem, PRESSURE_SIZE);
+
+ ksft_test_result_pass("%s\n", name);
+}
+
+static unsigned long hugetlb_free_pages(void)
+{
+ unsigned long nr = 0;
+ char line[64];
+ FILE *f;
+
+ f = fopen("/proc/meminfo", "r");
+ TEST_ASSERT(f, "fopen /proc/meminfo");
+
+ while (fgets(line, sizeof(line), f)) {
+ if (sscanf(line, "HugePages_Free: %lu", &nr) == 1)
+ break;
+ }
+ fclose(f);
+
+ return nr;
+}
+
+/* Hugetlb pages aren't swap-backed, so the first fault on them fails. */
+static void test_hugetlb_refused(const char *name)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ u64 npages;
+ int ret;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_touch_one_page);
+ npages = get_backing_src_pagesz(VM_MEM_SRC_ANONYMOUS_HUGETLB) / vm->page_size;
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS_HUGETLB, GPA_BASE, GUEST_MEMSLOT,
+ npages, 0);
+ virt_map(vm, GVA_BASE, GPA_BASE, 1);
+
+ ret = _vcpu_run(vcpu);
+ TEST_ASSERT(ret == -1 && errno == EIO,
+ "hugetlb-backed memslot: KVM_RUN rc %d errno %d", ret, errno);
+
+ kvm_vm_free(vm);
+
+ ksft_test_result_pass("%s\n", name);
+}
+
+int main(void)
+{
+ const char *hugetlb = "hugetlb memory rejected for a protected VM";
+
+ TEST_REQUIRE(pkvm_supported());
+ granule = getpagesize();
+
+ ksft_print_header();
+ ksft_set_plan(4);
+
+ test_run();
+
+ if (hugetlb_free_pages())
+ test_hugetlb_refused(hugetlb);
+ else
+ ksft_test_result_skip("%s: no hugetlb pages free\n", hugetlb);
+
+ test_share_outside_memslot();
+ test_memcache_pressure();
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 10/16] KVM: arm64: selftests: Add pkvm_smccc hypercall test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (8 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 09/16] KVM: arm64: selftests: Add pkvm_mem_share hypercall test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 11/16] KVM: arm64: selftests: Add pkvm_id_regs register and trap test Fuad Tabba
` (6 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
EL2 handles a protected guest's SMCCC calls other than PSCI rather than
forwarding them to the host. The vendor-hyp feature bitmap advertises
only FEATURES and the three memory calls.
Check SMCCC_VERSION, the vendor-hyp UID and feature bitmap, that the
calls EL2 doesn't implement (FF-A, TRNG, an unknown function) return
NOT_SUPPORTED, and that an SMC makes the guest take an UNDEF.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../testing/selftests/kvm/arm64/pkvm_smccc.c | 117 ++++++++++++++++++
2 files changed, 118 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_smccc.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 88c1159cb3bf1..6bb436bd36205 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -193,6 +193,7 @@ TEST_GEN_PROGS_arm64 += arm64/page_fault_test
TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
TEST_GEN_PROGS_arm64 += arm64/pkvm_mem_share
TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
+TEST_GEN_PROGS_arm64 += arm64/pkvm_smccc
TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
TEST_GEN_PROGS_arm64 += arm64/set_id_regs
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_smccc.c b/tools/testing/selftests/kvm/arm64/pkvm_smccc.c
new file mode 100644
index 0000000000000..64bfa12b26824
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_smccc.c
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_smccc - the SMCCC calls EL2 handles for a protected VM, and its SMC.
+ *
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <linux/arm-smccc.h>
+#include <linux/bits.h>
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+/* KVM UID 28b46fb6-2ec5-11e9-a9ca-4b564d003a74, as four SMCCC result regs. */
+#define ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_0 0xb66fb428U
+#define ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_1 0xe911c52eU
+#define ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_2 0x564bcaa9U
+#define ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_3 0x743a004dU
+
+/* The vendor-hyp feature bitmap EL2 advertises to protected guests. */
+#define KVM_FEATURES ( \
+ BIT(ARM_SMCCC_KVM_FUNC_FEATURES) | \
+ BIT(ARM_SMCCC_KVM_FUNC_HYP_MEMINFO) | \
+ BIT(ARM_SMCCC_KVM_FUNC_MEM_SHARE) | \
+ BIT(ARM_SMCCC_KVM_FUNC_MEM_UNSHARE))
+
+#define FFA_VERSION_FUNC_ID \
+ ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_32, \
+ ARM_SMCCC_OWNER_STANDARD, 0x63)
+
+/* An unallocated vendor-hyp function id. */
+#define UNKNOWN_FUNC_ID \
+ ARM_SMCCC_CALL_VAL(ARM_SMCCC_FAST_CALL, ARM_SMCCC_SMC_64, \
+ ARM_SMCCC_OWNER_VENDOR_HYP, 30)
+
+extern unsigned char smc_insn;
+
+static volatile u64 undef_pc;
+
+static void guest_code(void)
+{
+ struct arm_smccc_res res;
+ unsigned long ret;
+
+ pkvm_guest_init();
+
+ /* The vendor-hyp calls require SMCCC 1.1. */
+ ret = pkvm_hvc(ARM_SMCCC_VERSION_FUNC_ID, 0, 0, 0);
+ __GUEST_ASSERT((s32)ret >= ARM_SMCCC_VERSION_1_1, "SMCCC_VERSION 0x%lx", ret);
+
+ smccc_hvc(ARM_SMCCC_VENDOR_HYP_CALL_UID_FUNC_ID, 0, 0, 0, 0, 0, 0, 0, &res);
+ GUEST_ASSERT_EQ(res.a0, ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_0);
+ GUEST_ASSERT_EQ(res.a1, ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_1);
+ GUEST_ASSERT_EQ(res.a2, ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_2);
+ GUEST_ASSERT_EQ(res.a3, ARM_SMCCC_VENDOR_HYP_UID_KVM_REG_3);
+
+ ret = pkvm_hvc(ARM_SMCCC_VENDOR_HYP_KVM_FEATURES_FUNC_ID, 0, 0, 0);
+ GUEST_ASSERT_EQ(ret, KVM_FEATURES);
+
+ /* EL2 returns NOT_SUPPORTED for a call it doesn't implement, rather than forward it. */
+ GUEST_ASSERT_EQ(pkvm_hvc(FFA_VERSION_FUNC_ID, 0, 0, 0), SMCCC_RET_NOT_SUPPORTED);
+ GUEST_ASSERT_EQ(pkvm_hvc(ARM_SMCCC_TRNG_VERSION, 0, 0, 0), SMCCC_RET_NOT_SUPPORTED);
+ GUEST_ASSERT_EQ(pkvm_hvc(UNKNOWN_FUNC_ID, 0, 0, 0), SMCCC_RET_NOT_SUPPORTED);
+
+ GUEST_DONE();
+}
+
+static void undef_handler(struct ex_regs *regs)
+{
+ undef_pc = regs->pc;
+ regs->pc += 4;
+}
+
+/* EL2 injects an UNDEF for a protected guest's SMC. */
+static void guest_smc(void)
+{
+ pkvm_guest_init();
+
+ {
+ register u64 x0 asm("x0") = ARM_SMCCC_VENDOR_HYP_KVM_FEATURES_FUNC_ID;
+
+ asm volatile("smc_insn: smc #0" : "+r"(x0) : : "memory");
+ }
+ GUEST_ASSERT_EQ(undef_pc, (u64)&smc_insn);
+ GUEST_DONE();
+}
+
+static void run_guest(void *guest_code, bool smc)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_code);
+ if (smc)
+ vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ESR_ELx_EC_UNKNOWN, undef_handler);
+ pkvm_run_loop(vcpu, NULL);
+ kvm_vm_free(vm);
+}
+
+int main(void)
+{
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(2);
+
+ run_guest(guest_code, false);
+ ksft_test_result_pass("SMCCC surface\n");
+
+ run_guest(guest_smc, true);
+ ksft_test_result_pass("SMC takes an UNDEF\n");
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 11/16] KVM: arm64: selftests: Add pkvm_id_regs register and trap test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (9 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 10/16] KVM: arm64: selftests: Add pkvm_smccc " Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 12/16] KVM: arm64: selftests: Add pkvm_vcpu_regs register-state test Fuad Tabba
` (5 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
pKVM gives a protected guest its own view of the ID registers, which
hides the features pKVM doesn't support. The registers of those features
UNDEF in the guest, except some of the hidden debug registers, which
read as zero and ignore writes.
Test that view and those UNDEFs from the guest, along with the registers
EL2 emulates with fixed values. The debug checks read
ID_AA64DFR0_EL1.DebugVer to choose between the hidden view and the
exposed one. PtrAuth, which a protected VM can be granted, gets its own
case.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/pkvm_id_regs.c | 306 ++++++++++++++++++
2 files changed, 307 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_id_regs.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 6bb436bd36205..8855c9abb2fd0 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -191,6 +191,7 @@ TEST_GEN_PROGS_arm64 += arm64/external_aborts
TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
+TEST_GEN_PROGS_arm64 += arm64/pkvm_id_regs
TEST_GEN_PROGS_arm64 += arm64/pkvm_mem_share
TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
TEST_GEN_PROGS_arm64 += arm64/pkvm_smccc
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_id_regs.c b/tools/testing/selftests/kvm/arm64/pkvm_id_regs.c
new file mode 100644
index 0000000000000..10c77ad9601dc
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_id_regs.c
@@ -0,0 +1,306 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_id_regs - the ID registers a protected VM sees, and which of its
+ * register accesses UNDEF.
+ *
+ * Copyright (C) 2025 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+
+#include "gic_v3.h"
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+/* vCPU 0's MPIDR: Aff0=0, bit31=1, MT=0. */
+#define MPIDR_VCPU0 0x80000000UL
+
+/* Hidden fields with a non-zero Not-Implemented encoding. */
+#define PVM_ID_AA64DFR0_NI ( \
+ SYS_FIELD_PREP_ENUM(ID_AA64DFR0_EL1, DoubleLock, NI) | \
+ SYS_FIELD_PREP_ENUM(ID_AA64DFR0_EL1, MTPMU, NI))
+#define PVM_ID_AA64MMFR4_NI SYS_FIELD_PREP_ENUM(ID_AA64MMFR4_EL1, E2H0, NI)
+
+/* What self-hosted debug exposes: DebugVer and the breakpoint, watchpoint and CTX counts. */
+#define PVM_ID_AA64DFR0_DEBUG (ID_AA64DFR0_EL1_DebugVer_MASK | ID_AA64DFR0_EL1_BRPs_MASK | \
+ ID_AA64DFR0_EL1_WRPs_MASK | ID_AA64DFR0_EL1_CTX_CMPs_MASK)
+
+/* Breakpoint and watchpoint 0 controls with E clear: every byte, EL1, loads and stores. */
+#define DBGBCR_VAL ((0xfUL << 5) | (1UL << 1))
+#define DBGWCR_VAL ((0xffUL << 5) | (3UL << 3) | (1UL << 1))
+
+extern unsigned char brk_insn;
+
+static volatile bool undef_seen;
+static volatile u64 brk_pc;
+
+static void undef_handler(struct ex_regs *regs)
+{
+ undef_seen = true;
+ regs->pc += 4;
+}
+
+static void brk_handler(struct ex_regs *regs)
+{
+ brk_pc = regs->pc;
+ regs->pc += 4;
+}
+
+/* Access @expr must make the guest take an UNDEF (EC_UNKNOWN). */
+#define GUEST_ASSERT_UNDEF(expr) do { \
+ undef_seen = false; \
+ expr; \
+ GUEST_ASSERT(undef_seen); \
+} while (0)
+
+/* @reg reads back @pattern with no UNDEF. */
+#define GUEST_ASSERT_RW(reg, pattern) do { \
+ u64 __saved = read_sysreg_s(reg); \
+ undef_seen = false; \
+ write_sysreg_s(pattern, reg); \
+ GUEST_ASSERT_EQ(read_sysreg_s(reg), pattern); \
+ write_sysreg_s(__saved, reg); \
+ GUEST_ASSERT(!undef_seen); \
+} while (0)
+
+static void assert_aarch32_id_raz(void)
+{
+ /* AArch32 is unsupported for protected guests: its ID block reads 0. */
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_PFR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_PFR1_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_DFR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AFR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_MMFR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_MMFR1_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_MMFR2_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_MMFR3_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_ISAR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_ISAR1_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_ISAR2_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_ISAR3_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_ISAR4_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_ISAR5_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_MMFR4_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_ISAR6_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_MVFR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_MVFR1_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_MVFR2_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(sys_reg(3, 0, 0, 3, 3)), 0); /* unallocated */
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_PFR2_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_DFR1_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_MMFR5_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(sys_reg(3, 0, 0, 3, 7)), 0); /* unallocated */
+}
+
+/* A zero DebugVer means EL2 hides self-hosted debug from the guest. */
+static bool debug_exposed(void)
+{
+ return SYS_FIELD_GET(ID_AA64DFR0_EL1, DebugVer, read_sysreg_s(SYS_ID_AA64DFR0_EL1));
+}
+
+static void assert_id_regs(void)
+{
+ u64 pfr0 = read_sysreg_s(SYS_ID_AA64PFR0_EL1);
+ u64 dfr0 = read_sysreg_s(SYS_ID_AA64DFR0_EL1);
+
+ if (debug_exposed()) {
+ u64 debugver = SYS_FIELD_GET(ID_AA64DFR0_EL1, DebugVer, dfr0);
+ u64 brps = SYS_FIELD_GET(ID_AA64DFR0_EL1, BRPs, dfr0);
+
+ /* Exposed debug: at most v8.8, and at least two breakpoints and two watchpoints. */
+ GUEST_ASSERT_EQ(dfr0 & ~PVM_ID_AA64DFR0_DEBUG, PVM_ID_AA64DFR0_NI);
+ GUEST_ASSERT(debugver >= ID_AA64DFR0_EL1_DebugVer_IMP);
+ GUEST_ASSERT(debugver <= ID_AA64DFR0_EL1_DebugVer_V8P8);
+ GUEST_ASSERT(brps >= 1);
+ GUEST_ASSERT(SYS_FIELD_GET(ID_AA64DFR0_EL1, WRPs, dfr0) >= 1);
+ GUEST_ASSERT(SYS_FIELD_GET(ID_AA64DFR0_EL1, CTX_CMPs, dfr0) <= brps);
+ } else {
+ /* Debug and PMU are hidden: DFR0 has a fixed value, no breakpoints. */
+ GUEST_ASSERT_EQ(dfr0, PVM_ID_AA64DFR0_NI);
+ }
+
+ /* With no vCPU features, SVE, ZFR0 and PtrAuth read 0. */
+ GUEST_ASSERT_EQ(SYS_FIELD_GET(ID_AA64PFR0_EL1, SVE, pfr0), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64ZFR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64ISAR1_EL1) &
+ (ID_AA64ISAR1_EL1_APA_MASK | ID_AA64ISAR1_EL1_API_MASK), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64ISAR2_EL1) &
+ (ID_AA64ISAR2_EL1_APA3_MASK | ID_AA64ISAR2_EL1_GPA3_MASK), 0);
+
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64DFR1_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64AFR0_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64AFR1_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64SMFR0_EL1), 0);
+ GUEST_ASSERT_EQ(SYS_FIELD_GET(ID_AA64PFR1_EL1, MTE, read_sysreg_s(SYS_ID_AA64PFR1_EL1)), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ID_AA64MMFR4_EL1), PVM_ID_AA64MMFR4_NI);
+
+ assert_aarch32_id_raz();
+}
+
+static void assert_debug_raz_wi(void)
+{
+ /* Breakpoint/watchpoint 0, MDSCR and OS-lock regs are RAZ/WI, with no UNDEF. */
+ undef_seen = false;
+ write_sysreg_s(~0UL, SYS_DBGBVRn_EL1(0));
+ write_sysreg_s(~0UL, SYS_DBGBCRn_EL1(0));
+ write_sysreg_s(~0UL, SYS_DBGWVRn_EL1(0));
+ write_sysreg_s(~0UL, SYS_DBGWCRn_EL1(0));
+ write_sysreg_s(~0UL, SYS_MDSCR_EL1);
+ write_sysreg_s(~0UL, SYS_OSLAR_EL1);
+ write_sysreg_s(~0UL, SYS_OSDLR_EL1);
+ isb();
+
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_DBGBVRn_EL1(0)), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_DBGBCRn_EL1(0)), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_DBGWVRn_EL1(0)), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_DBGWCRn_EL1(0)), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_MDSCR_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_OSLSR_EL1), 0);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_OSDLR_EL1), 0);
+ GUEST_ASSERT(!undef_seen);
+
+ /* Breakpoint/watchpoint indices above 0 are not implemented. */
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_DBGBVRn_EL1(1)));
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_DBGWVRn_EL1(1)));
+}
+
+static void assert_debug_regs(void)
+{
+ u64 addr = (u64)&brk_insn;
+
+ /* Breakpoint/watchpoint 0 hold what the guest writes. */
+ GUEST_ASSERT_RW(SYS_DBGBVRn_EL1(0), addr);
+ GUEST_ASSERT_RW(SYS_DBGBCRn_EL1(0), DBGBCR_VAL);
+ GUEST_ASSERT_RW(SYS_DBGWVRn_EL1(0), addr & ~7UL);
+ GUEST_ASSERT_RW(SYS_DBGWCRn_EL1(0), DBGWCR_VAL);
+
+ /*
+ * MDSCR doesn't UNDEF. The OS lock is implemented. The OS double lock isn't,
+ * so OSDLR is RAZ/WI.
+ */
+ undef_seen = false;
+ write_sysreg_s(read_sysreg_s(SYS_MDSCR_EL1), SYS_MDSCR_EL1);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_OSLSR_EL1) & OSLSR_EL1_OSLM_MASK,
+ OSLSR_EL1_OSLM_IMPLEMENTED);
+ write_sysreg_s(~0UL, SYS_OSDLR_EL1);
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_OSDLR_EL1), 0);
+ GUEST_ASSERT(!undef_seen);
+}
+
+static void assert_undef_regs(void)
+{
+ /* The PMU is hidden, and its registers UNDEF. */
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_PMCR_EL0));
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_PMCNTENSET_EL0));
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_PMUSERENR_EL0));
+
+ /* Limited-ordering region registers UNDEF. */
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_LORID_EL1));
+
+ /* AMU and MTE are hidden, and so are the PtrAuth keys without the PtrAuth vCPU features. */
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_AMCNTENSET0_EL0));
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_GCR_EL1));
+ GUEST_ASSERT_UNDEF((void)read_sysreg_s(SYS_APIAKEYLO_EL1));
+
+ /*
+ * Nor is SVE, so an SVE instruction UNDEFs even with CPACR_EL1.ZEN set. On a CPU
+ * with SME but no SVE, SMEN gates it instead, so set that too.
+ */
+ write_sysreg(read_sysreg(cpacr_el1) | CPACR_EL1_ZEN_EL1EN | CPACR_EL1_SMEN_EL1EN,
+ cpacr_el1);
+ isb();
+ GUEST_ASSERT_UNDEF(asm volatile(".arch_extension sve\n rdvl x0, #1" : : : "x0"));
+}
+
+static void assert_host_handled(void)
+{
+ /* Host-handled registers reach the guest with no UNDEF. */
+ undef_seen = false;
+ (void)read_sysreg_s(SYS_CTR_EL0);
+ (void)read_sysreg_s(SYS_CLIDR_EL1);
+ GUEST_ASSERT(!undef_seen);
+}
+
+static void guest_baseline(void)
+{
+ pkvm_guest_init();
+
+ assert_id_regs();
+ if (debug_exposed())
+ assert_debug_regs();
+ else
+ assert_debug_raz_wi();
+ assert_undef_regs();
+ assert_host_handled();
+
+ asm volatile("brk_insn: brk #0");
+ GUEST_ASSERT_EQ(brk_pc, (u64)&brk_insn);
+
+ /* EL2 emulates ICC_SRE_EL1 with a fixed value. */
+ GUEST_ASSERT_EQ(read_sysreg_s(SYS_ICC_SRE_EL1),
+ ICC_SRE_EL1_DIB | ICC_SRE_EL1_DFB | ICC_SRE_EL1_SRE);
+
+ GUEST_ASSERT_EQ(read_sysreg(mpidr_el1), MPIDR_VCPU0);
+
+ GUEST_DONE();
+}
+
+/* The host implements one of QARMA5, an IMP DEF algorithm or QARMA3: any may appear. */
+static void guest_ptrauth(void)
+{
+ pkvm_guest_init();
+ GUEST_ASSERT((read_sysreg_s(SYS_ID_AA64ISAR1_EL1) &
+ (ID_AA64ISAR1_EL1_APA_MASK | ID_AA64ISAR1_EL1_API_MASK)) |
+ (read_sysreg_s(SYS_ID_AA64ISAR2_EL1) &
+ (ID_AA64ISAR2_EL1_APA3_MASK | ID_AA64ISAR2_EL1_GPA3_MASK)));
+ GUEST_DONE();
+}
+
+static void run_guest(u32 features, void *guest_code)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create(1, features, &vcpu);
+ vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ESR_ELx_EC_UNKNOWN, undef_handler);
+ vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ESR_ELx_EC_BRK64, brk_handler);
+
+ pkvm_vcpu_setup_boot(vcpu, guest_code);
+ pkvm_run_loop(vcpu, NULL);
+ kvm_vm_free(vm);
+}
+
+/* The host filters capabilities per VM, so ask a protected VM. */
+static bool pvm_has_cap(long cap)
+{
+ struct kvm_vm *vm = vm_create_barebones_type(KVM_VM_TYPE_ARM_PROTECTED);
+ bool ret = vm_check_cap(vm, cap);
+
+ kvm_vm_free(vm);
+ return ret;
+}
+
+int main(void)
+{
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(2);
+
+ run_guest(0, guest_baseline);
+ ksft_test_result_pass("baseline ID view and UNDEF matrix\n");
+
+ if (pvm_has_cap(KVM_CAP_ARM_PTRAUTH_ADDRESS) && pvm_has_cap(KVM_CAP_ARM_PTRAUTH_GENERIC)) {
+ run_guest(BIT(KVM_ARM_VCPU_PTRAUTH_ADDRESS) |
+ BIT(KVM_ARM_VCPU_PTRAUTH_GENERIC), guest_ptrauth);
+ ksft_test_result_pass("PtrAuth ID delta\n");
+ } else {
+ ksft_test_result_skip("PtrAuth ID delta: PtrAuth unavailable\n");
+ }
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 12/16] KVM: arm64: selftests: Add pkvm_vcpu_regs register-state test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (10 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 11/16] KVM: arm64: selftests: Add pkvm_id_regs register and trap test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 13/16] KVM: arm64: selftests: Add pkvm_irq interrupt test Fuad Tabba
` (4 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
A protected vCPU's registers are held at EL2, which handles some exits
itself and returns others to the host. The host can move the vCPU's
thread to another CPU at any exit. The guest's registers have to keep
their values through all of that.
Verify that x19-x28 and v0-v31 keep the values the guest loaded across
each kind of exit, and across a host CPU migration at every MMIO exit.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/pkvm_vcpu_regs.c | 221 ++++++++++++++++++
2 files changed, 222 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_vcpu_regs.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 8855c9abb2fd0..1e40e793aa6f3 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -195,6 +195,7 @@ TEST_GEN_PROGS_arm64 += arm64/pkvm_id_regs
TEST_GEN_PROGS_arm64 += arm64/pkvm_mem_share
TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
TEST_GEN_PROGS_arm64 += arm64/pkvm_smccc
+TEST_GEN_PROGS_arm64 += arm64/pkvm_vcpu_regs
TEST_GEN_PROGS_arm64 += arm64/psci_test
TEST_GEN_PROGS_arm64 += arm64/sea_to_user
TEST_GEN_PROGS_arm64 += arm64/set_id_regs
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_vcpu_regs.c b/tools/testing/selftests/kvm/arm64/pkvm_vcpu_regs.c
new file mode 100644
index 0000000000000..a9b9615a9075b
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_vcpu_regs.c
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_vcpu_regs - a protected guest's registers across every kind of exit.
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <errno.h>
+#include <sched.h>
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+/* Accesses to this IPA, outside every memslot, exit to the VMM as MMIO. */
+#define MMIO_IPA BIT_ULL(31)
+#define MMIO_READ_VAL 0x0123456789abcdefUL
+
+/* One new host page per stage-2 fault. */
+#define FAULT_MEMSLOT (PKVM_IDMAP_MEMSLOT + 1)
+#define FAULT_GPA BIT_ULL(30)
+
+#define NR_EXITS 64
+
+enum exit_kind {
+ EXIT_MMIO_WRITE,
+ EXIT_MMIO_READ,
+ EXIT_HVC,
+ EXIT_SYSREG_EL2,
+ EXIT_SYSREG_HOST,
+ EXIT_S2_FAULT,
+ NR_EXIT_KINDS,
+};
+
+static const char * const exit_kind_names[] = {
+ [EXIT_MMIO_WRITE] = "MMIO writes to the VMM",
+ [EXIT_MMIO_READ] = "MMIO reads from the VMM",
+ [EXIT_HVC] = "hypercalls EL2 handles",
+ [EXIT_SYSREG_EL2] = "sysreg traps EL2 handles",
+ [EXIT_SYSREG_HOST] = "sysreg traps the host handles",
+ [EXIT_S2_FAULT] = "stage-2 faults on new memory",
+};
+
+/* x19-x28, v0-v31, then what the exit left in x0. */
+struct reg_state {
+ u64 x[10];
+ u64 v[32][2];
+ u64 x0;
+};
+
+#define REG_WORDS (offsetof(struct reg_state, x0) / sizeof(u64))
+
+static_assert(offsetof(struct reg_state, v) == 80, "v0 offset");
+static_assert(offsetof(struct reg_state, x0) == 592, "x0 offset");
+
+#define X_REGS "19, 20, 21, 22, 23, 24, 25, 26, 27, 28"
+#define V_REGS "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, " \
+ "16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31"
+
+/* @op x19-x28 and v0-v31 at their struct reg_state offsets from @buf. */
+#define REGS_OP(op, buf) \
+ ".irp num, " X_REGS "\n" op " x\\num, [%[" buf "], #8 * (\\num - 19)]\n.endr\n" \
+ ".irp num, " V_REGS "\n" op " q\\num, [%[" buf "], #80 + 16 * \\num]\n.endr\n"
+
+/* Load the registers, take the exit @insn causes, and store them again. */
+#define REGS_ACROSS(insn, addr) \
+({ \
+ asm volatile(REGS_OP("ldr", "in") insn "\n" REGS_OP("str", "out") \
+ "str x0, [%[out], #592]\n" \
+ : : [in] "r"(®s_in), [out] "r"(®s_out), [arg] "r"(addr) \
+ : "x0", "x1", "x2", "x3", \
+ "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", \
+ "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7", \
+ "v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15", \
+ "v16", "v17", "v18", "v19", "v20", "v21", "v22", "v23", \
+ "v24", "v25", "v26", "v27", "v28", "v29", "v30", "v31", \
+ "memory"); \
+})
+
+/* Set in the guest's copy by the host before the first run. */
+static int exit_kind;
+static u64 host_page_size;
+
+static struct reg_state regs_in, regs_out;
+
+static void guest_regs_across_exits(void)
+{
+ u64 *in = (u64 *)®s_in, *out = (u64 *)®s_out;
+ int i, j;
+
+ pkvm_guest_init();
+
+ for (i = 0; i < NR_EXITS; i++) {
+ /* Different in every word and on every exit. */
+ for (j = 0; j < REG_WORDS; j++)
+ in[j] = 0x0101010101010101UL * (j + 1) ^ (u64)i << 32;
+
+ switch (exit_kind) {
+ case EXIT_MMIO_WRITE:
+ REGS_ACROSS("str x0, [%[arg]]", MMIO_IPA);
+ break;
+ case EXIT_MMIO_READ:
+ REGS_ACROSS("ldr x0, [%[arg]]", MMIO_IPA);
+ GUEST_ASSERT_EQ(regs_out.x0, MMIO_READ_VAL);
+ break;
+ case EXIT_HVC:
+ REGS_ACROSS("mov w0, #0x80000000\n hvc #0", 0);
+ break;
+ case EXIT_SYSREG_EL2:
+ REGS_ACROSS("mrs x0, id_aa64pfr0_el1", 0);
+ break;
+ case EXIT_SYSREG_HOST:
+ REGS_ACROSS("mrs x0, clidr_el1", 0);
+ break;
+ case EXIT_S2_FAULT:
+ REGS_ACROSS("str x0, [%[arg]]", FAULT_GPA + i * host_page_size);
+ break;
+ }
+
+ for (j = 0; j < REG_WORDS; j++)
+ __GUEST_ASSERT(out[j] == in[j], "exit %d: %s%d is 0x%lx, not 0x%lx", i,
+ j < 10 ? "x" : "v", j < 10 ? 19 + j : (j - 10) / 2,
+ out[j], in[j]);
+ }
+
+ GUEST_DONE();
+}
+
+static cpu_set_t host_cpus;
+
+/* Move this thread, and with it the vCPU's next run, to another of its CPUs. */
+static void migrate_thread(void)
+{
+ int cpu = sched_getcpu();
+
+ do
+ cpu = (cpu + 1) % CPU_SETSIZE;
+ while (!CPU_ISSET(cpu, &host_cpus));
+
+ pin_self_to_cpu(cpu);
+ TEST_ASSERT_EQ(sched_getcpu(), cpu);
+}
+
+static void run_regs_across_exits(struct kvm_vcpu *vcpu, bool migrate)
+{
+ struct kvm_run *run = vcpu->run;
+ u64 data = MMIO_READ_VAL;
+ struct ucall uc;
+
+ for (;;) {
+ vcpu_run(vcpu);
+
+ if (run->exit_reason == KVM_EXIT_MMIO && run->mmio.phys_addr == MMIO_IPA) {
+ if (!run->mmio.is_write)
+ memcpy(run->mmio.data, &data, sizeof(data));
+ if (migrate)
+ migrate_thread();
+ continue;
+ }
+
+ switch (get_ucall(vcpu, &uc)) {
+ case UCALL_DONE:
+ return;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ break;
+ default:
+ TEST_FAIL("Unexpected exit: %s", exit_reason_str(run->exit_reason));
+ }
+ }
+}
+
+/* With @migrate, the vCPU moves to another host CPU at every MMIO exit. */
+static void test_regs_across_exits(enum exit_kind kind, bool migrate)
+{
+ struct kvm_vcpu *vcpu;
+ unsigned int npages;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_regs_across_exits);
+ npages = NR_EXITS * getpagesize() / vm->page_size;
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, FAULT_GPA, FAULT_MEMSLOT, npages, 0);
+ virt_map(vm, FAULT_GPA, FAULT_GPA, npages);
+ virt_map(vm, MMIO_IPA, MMIO_IPA, 1);
+ write_guest_global(vm, exit_kind, kind);
+ write_guest_global(vm, host_page_size, getpagesize());
+
+ run_regs_across_exits(vcpu, migrate);
+
+ kvm_vm_free(vm);
+}
+
+int main(void)
+{
+ int kind;
+
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(1 + NR_EXIT_KINDS);
+
+ for (kind = 0; kind < NR_EXIT_KINDS; kind++) {
+ test_regs_across_exits(kind, false);
+ ksft_test_result_pass("x19-x28 and v0-v31 across %s\n", exit_kind_names[kind]);
+ }
+
+ TEST_ASSERT(!sched_getaffinity(0, sizeof(host_cpus), &host_cpus),
+ "sched_getaffinity: errno %d", errno);
+ if (CPU_COUNT(&host_cpus) < 2) {
+ ksft_test_result_skip("x19-x28 and v0-v31 across host CPU migrations: one CPU\n");
+ } else {
+ test_regs_across_exits(EXIT_MMIO_WRITE, true);
+ TEST_ASSERT(!sched_setaffinity(0, sizeof(host_cpus), &host_cpus),
+ "sched_setaffinity: errno %d", errno);
+ ksft_test_result_pass("x19-x28 and v0-v31 across host CPU migrations\n");
+ }
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 13/16] KVM: arm64: selftests: Add pkvm_irq interrupt test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (11 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 12/16] KVM: arm64: selftests: Add pkvm_vcpu_regs register-state test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 14/16] KVM: arm64: selftests: Add pkvm_isolation host-access test Fuad Tabba
` (3 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
The host emulates a protected guest's vGIC, so the list registers are
copied between the host and EL2 each time the host runs the vCPU. If the
copy back is lost, the host never sees the guest's EOI and presents the
interrupt again. EL2 keeps its own copy of the guest's virtual timer
state, which the host can't change, and restores it each time the host
runs the vCPU.
Check that the virtual timer's PPI arrives exactly once, with CVAL and
CTL kept across exits before and while it's pending, that SGIs pass
between two vCPUs, and that the timer wakes a vCPU in WFI.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
tools/testing/selftests/kvm/arm64/pkvm_irq.c | 268 +++++++++++++++++++
2 files changed, 269 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_irq.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 1e40e793aa6f3..13998964facd5 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -192,6 +192,7 @@ TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
TEST_GEN_PROGS_arm64 += arm64/pkvm_id_regs
+TEST_GEN_PROGS_arm64 += arm64/pkvm_irq
TEST_GEN_PROGS_arm64 += arm64/pkvm_mem_share
TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
TEST_GEN_PROGS_arm64 += arm64/pkvm_smccc
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_irq.c b/tools/testing/selftests/kvm/arm64/pkvm_irq.c
new file mode 100644
index 0000000000000..f92fa6f0de8de
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_irq.c
@@ -0,0 +1,268 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_irq - the virtual timer's PPI and SGIs in a protected VM.
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <pthread.h>
+
+#include <linux/bitfield.h>
+#include <linux/psci.h>
+#include <asm/cputype.h>
+
+#include "arch_timer.h"
+#include "gic.h"
+#include "gic_v3.h"
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+#define TIMER_MS 10
+#define WAIT_MS 30000
+#define NR_EXITS 16
+#define SGI_ROUNDS 16
+
+#define SGI_PING 1
+#define SGI_PONG 2
+
+/* Set in the guest's copy by the host before the first run. */
+static u32 vtimer_irq;
+static u64 boot_entry;
+static u64 vcpu_mpidr[2];
+static u64 vcpu_ctx[2];
+
+static u32 irqs[2];
+static u32 last_intid[2];
+static u32 ready;
+
+#define wait_until(cond) \
+do { \
+ u64 __end = timer_get_cntct(VIRTUAL) + msec_to_cycles(WAIT_MS); \
+ \
+ while (!(cond)) \
+ __GUEST_ASSERT(timer_get_cntct(VIRTUAL) < __end, \
+ "timed out waiting for %s", #cond); \
+} while (0)
+
+static void guest_irq_handler(struct ex_regs *regs)
+{
+ u32 intid = gic_get_and_ack_irq();
+ u32 cpu = guest_get_vcpuid();
+
+ if (intid == IAR_SPURIOUS)
+ return;
+
+ if (intid == vtimer_irq) {
+ __GUEST_ASSERT(timer_get_ctl(VIRTUAL) & CTL_ISTATUS, "CNTV_CTL 0x%x",
+ timer_get_ctl(VIRTUAL));
+ GUEST_ASSERT(timer_get_cntct(VIRTUAL) >= timer_get_cval(VIRTUAL));
+ /* Mask the timer before the EOI, or its level-sensitive PPI is taken again. */
+ timer_set_ctl(VIRTUAL, CTL_IMASK);
+ }
+
+ WRITE_ONCE(last_intid[cpu], intid);
+ gic_set_eoi(intid);
+ WRITE_ONCE(irqs[cpu], irqs[cpu] + 1);
+}
+
+static void guest_irq_init(unsigned int nr_cpus)
+{
+ local_irq_disable();
+ pkvm_guest_init();
+ gic_init(GIC_V3, nr_cpus);
+}
+
+static u64 start_vtimer(void)
+{
+ u64 cval = timer_get_cntct(VIRTUAL) + msec_to_cycles(TIMER_MS);
+
+ gic_irq_enable(vtimer_irq);
+ timer_set_cval(VIRTUAL, cval);
+ timer_set_ctl(VIRTUAL, CTL_ENABLE);
+ return cval;
+}
+
+static void guest_vtimer(void)
+{
+ u64 cval;
+ int i;
+
+ guest_irq_init(1);
+ cval = start_vtimer();
+
+ /* Exits before the timer fires, then with its interrupt pending but masked. */
+ for (i = 0; i < NR_EXITS; i++)
+ GUEST_SYNC(i);
+ wait_until(timer_get_ctl(VIRTUAL) & CTL_ISTATUS);
+ for (i = 0; i < NR_EXITS; i++)
+ GUEST_SYNC(i);
+
+ GUEST_ASSERT_EQ(timer_get_cval(VIRTUAL), cval);
+ GUEST_ASSERT_EQ(timer_get_ctl(VIRTUAL), CTL_ENABLE | CTL_ISTATUS);
+ GUEST_ASSERT_EQ(READ_ONCE(irqs[0]), 0);
+
+ local_irq_enable();
+ wait_until(READ_ONCE(irqs[0]));
+
+ /* The EOI must reach the host, or it presents the interrupt again. */
+ for (i = 0; i < NR_EXITS; i++)
+ GUEST_SYNC(i);
+ local_irq_disable();
+
+ GUEST_ASSERT_EQ(READ_ONCE(irqs[0]), 1);
+ GUEST_ASSERT_EQ(READ_ONCE(last_intid[0]), vtimer_irq);
+ GUEST_DONE();
+}
+
+static void send_sgi(int id, u32 intid)
+{
+ u64 mpidr = vcpu_mpidr[id];
+
+ /* The target reads this vCPU's counters once it has taken the SGI. */
+ dsb(ishst);
+ write_sysreg_s(FIELD_PREP(ICC_SGI1R_SGI_ID_MASK, intid) |
+ FIELD_PREP(ICC_SGI1R_AFFINITY_1_MASK, MPIDR_AFFINITY_LEVEL(mpidr, 1)) |
+ FIELD_PREP(ICC_SGI1R_TARGET_LIST_MASK, BIT(MPIDR_AFFINITY_LEVEL(mpidr, 0))),
+ SYS_ICC_SGI1R_EL1);
+ isb();
+}
+
+static void guest_sgi_primary(void)
+{
+ u32 k;
+
+ guest_irq_init(2);
+ gic_irq_enable(SGI_PONG);
+
+ GUEST_ASSERT_EQ(pkvm_hvc(PSCI_0_2_FN64_CPU_ON, vcpu_mpidr[1], boot_entry, vcpu_ctx[1]),
+ PSCI_RET_SUCCESS);
+ wait_until(READ_ONCE(ready));
+
+ local_irq_enable();
+ for (k = 1; k <= SGI_ROUNDS; k++) {
+ send_sgi(1, SGI_PING);
+
+ /* vCPU 1 sends PONG only once it has taken PING, so neither is in flight. */
+ wait_until(READ_ONCE(irqs[0]) >= k);
+ GUEST_ASSERT_EQ(READ_ONCE(irqs[0]), k);
+ GUEST_ASSERT_EQ(READ_ONCE(irqs[1]), k);
+ GUEST_ASSERT_EQ(READ_ONCE(last_intid[0]), SGI_PONG);
+ GUEST_ASSERT_EQ(READ_ONCE(last_intid[1]), SGI_PING);
+ }
+ local_irq_disable();
+
+ GUEST_DONE();
+}
+
+/* The primary's guest_irq_init() set up ucall and the distributor. */
+static void guest_sgi_secondary(void)
+{
+ u32 k;
+
+ gic_init(GIC_V3, 2);
+ gic_irq_enable(SGI_PING);
+ local_irq_enable();
+ WRITE_ONCE(ready, 1);
+
+ for (k = 1; k <= SGI_ROUNDS; k++) {
+ wait_until(READ_ONCE(irqs[1]) >= k);
+ send_sgi(0, SGI_PONG);
+ }
+ local_irq_disable();
+
+ GUEST_DONE();
+}
+
+static void guest_wfi(void)
+{
+ guest_irq_init(1);
+ start_vtimer();
+
+ /* WFI can complete before the timer fires, so wait in it until the handler has run. */
+ while (!READ_ONCE(irqs[0])) {
+ wfi();
+ local_irq_enable();
+ isb(); /* handle IRQ */
+ local_irq_disable();
+ }
+
+ GUEST_ASSERT_EQ(READ_ONCE(irqs[0]), 1);
+ GUEST_ASSERT_EQ(READ_ONCE(last_intid[0]), vtimer_irq);
+ GUEST_DONE();
+}
+
+static void irq_vm_setup(struct kvm_vm *vm, struct kvm_vcpu *vcpu)
+{
+ vm_install_exception_handler(vm, VECTOR_IRQ_CURRENT, guest_irq_handler);
+ write_guest_global(vm, vtimer_irq, vcpu_get_vtimer_irq(vcpu));
+}
+
+static void test_one_vcpu(void *guest_code)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_code);
+ irq_vm_setup(vm, vcpu);
+
+ pkvm_run_loop(vcpu, NULL);
+
+ kvm_vm_free(vm);
+}
+
+static void wire_mpidr(struct kvm_vm *vm, struct kvm_vcpu *vcpu)
+{
+ u64 mpidr = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_MPIDR_EL1));
+
+ write_guest_global(vm, vcpu_mpidr[vcpu->id], mpidr & MPIDR_HWID_BITMASK);
+}
+
+static void *vcpu_thread(void *vcpu)
+{
+ pkvm_run_loop(vcpu, NULL);
+ return NULL;
+}
+
+static void test_sgi(void)
+{
+ struct kvm_vcpu *vcpus[2];
+ pthread_t thread;
+ struct kvm_vm *vm;
+ gpa_t ctx;
+
+ vm = pkvm_vm_create(2, 0, vcpus);
+ irq_vm_setup(vm, vcpus[0]);
+
+ pkvm_vcpu_setup_boot(vcpus[0], guest_sgi_primary);
+ wire_mpidr(vm, vcpus[0]);
+ wire_mpidr(vm, vcpus[1]);
+ write_guest_global(vm, boot_entry, pkvm_idmap_gpa(vm));
+ ctx = pkvm_vcpu_setup_boot_secondary(vcpus[1], guest_sgi_secondary);
+ write_guest_global(vm, vcpu_ctx[1], ctx);
+
+ pthread_create(&thread, NULL, vcpu_thread, vcpus[1]);
+ pkvm_run_loop(vcpus[0], NULL);
+ pthread_join(thread, NULL);
+
+ kvm_vm_free(vm);
+}
+
+int main(void)
+{
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(3);
+
+ test_one_vcpu(guest_vtimer);
+ ksft_test_result_pass("virtual timer PPI across exits\n");
+ test_sgi();
+ ksft_test_result_pass("SGIs between two vCPUs: %d rounds\n", SGI_ROUNDS);
+ test_one_vcpu(guest_wfi);
+ ksft_test_result_pass("the virtual timer wakes a vCPU waiting in WFI\n");
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 14/16] KVM: arm64: selftests: Add pkvm_isolation host-access test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (12 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 13/16] KVM: arm64: selftests: Add pkvm_irq interrupt test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 15/16] KVM: arm64: selftests: Add pkvm_lifecycle teardown test Fuad Tabba
` (2 subsequent siblings)
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
When a protected guest faults its memory in, the host donates its
pages through EL2 and keeps its userspace mapping of them. Nothing the
host does through that mapping exposes the guest's data. The host
kernel's copy_to_user() and copy_from_user() on that mapping fail. When
the host kernel pins such a page and accesses it through its own
mapping, as process_vm_readv() does, EL2 reclaims the page instead. EL2
zeroes the page before giving it back, and the guest loses it.
Test that the host kernel's copies to and from that memory fail with
EFAULT, that process_vm_readv() of it reads zeros and KVM_RUN then fails
at the guest's next access to the page, that MADV_DONTNEED and munmap()
leave the guest's pages pinned and accounted, and that a normal VM's
fault on them fails.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/pkvm_isolation.c | 225 ++++++++++++++++++
2 files changed, 226 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_isolation.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 13998964facd5..68bdd01f62270 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -193,6 +193,7 @@ TEST_GEN_PROGS_arm64 += arm64/page_fault_test
TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
TEST_GEN_PROGS_arm64 += arm64/pkvm_id_regs
TEST_GEN_PROGS_arm64 += arm64/pkvm_irq
+TEST_GEN_PROGS_arm64 += arm64/pkvm_isolation
TEST_GEN_PROGS_arm64 += arm64/pkvm_mem_share
TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
TEST_GEN_PROGS_arm64 += arm64/pkvm_smccc
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_isolation.c b/tools/testing/selftests/kvm/arm64/pkvm_isolation.c
new file mode 100644
index 0000000000000..fd59e5c6e473c
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_isolation.c
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_isolation - what the host can and can't do with a protected VM's memory.
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <errno.h>
+#include <sys/mman.h>
+#include <sys/uio.h>
+#include <unistd.h>
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+#define DATA_MEMSLOT (PKVM_IDMAP_MEMSLOT + 1)
+#define DATA_GPA BIT_ULL(30)
+#define DATA_PAGES 16
+#define DATA_SIZE ((size_t)DATA_PAGES * MIN_PAGE_SIZE)
+
+#define GUEST_VAL 0x0badf00ddeadbeefUL
+#define HOST_VAL 0x0123456789abcdefUL
+
+static volatile u64 *page_va(int idx)
+{
+ return (volatile u64 *)(DATA_GPA + (u64)idx * MIN_PAGE_SIZE);
+}
+
+/* The host acts on the guest's memory at the sync, then the guest checks it. */
+static void guest_code(void)
+{
+ int i;
+
+ pkvm_guest_init();
+
+ for (i = 0; i < DATA_PAGES; i++)
+ *page_va(i) = GUEST_VAL + i;
+
+ GUEST_SYNC(0);
+
+ for (i = 0; i < DATA_PAGES; i++)
+ GUEST_ASSERT_EQ(*page_va(i), GUEST_VAL + i);
+
+ GUEST_DONE();
+}
+
+static void guest_np_read(void)
+{
+ *page_va(0);
+ GUEST_DONE();
+}
+
+/* The host kernel's copy to or from private memory faults, and the uaccess fixup fails it. */
+static void host_write_from_private(void *hva)
+{
+ ssize_t ret;
+ int fds[2];
+
+ TEST_ASSERT(!pipe(fds), "pipe: errno %d", errno);
+
+ ret = write(fds[1], hva, MIN_PAGE_SIZE);
+ TEST_ASSERT(ret == -1 && errno == EFAULT,
+ "write() from private memory: rc %zd errno %d", ret, errno);
+
+ close(fds[0]);
+ close(fds[1]);
+}
+
+static void host_read_into_private(void *hva)
+{
+ u64 val = HOST_VAL;
+ ssize_t ret;
+ int fds[2];
+
+ TEST_ASSERT(!pipe(fds), "pipe: errno %d", errno);
+ TEST_ASSERT_EQ(write(fds[1], &val, sizeof(val)), sizeof(val));
+
+ ret = read(fds[0], hva, sizeof(val));
+ TEST_ASSERT(ret == -1 && errno == EFAULT,
+ "read() into private memory: rc %zd errno %d", ret, errno);
+
+ close(fds[0]);
+ close(fds[1]);
+}
+
+/* The host's long-term pins keep the guest's pages and their locked-memory accounting. */
+static void host_dontneed(void *hva)
+{
+ size_t locked = pkvm_host_locked_vm();
+
+ TEST_ASSERT(!madvise(hva, DATA_SIZE, MADV_DONTNEED), "madvise: errno %d", errno);
+ TEST_ASSERT_EQ(pkvm_host_locked_vm(), locked);
+
+ /* The host's mapping now has a new page, which the guest doesn't see. */
+ TEST_ASSERT_EQ(*(u64 *)hva, 0);
+ *(u64 *)hva = HOST_VAL;
+}
+
+static void host_munmap(void *hva)
+{
+ size_t locked = pkvm_host_locked_vm();
+
+ TEST_ASSERT(!munmap(hva, DATA_SIZE), "munmap: errno %d", errno);
+ TEST_ASSERT_EQ(pkvm_host_locked_vm(), locked);
+}
+
+/* A normal VM's fault on private memory fails, and the protected guest keeps its data. */
+static void host_np_vm_on_private(void *hva)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+ int ret;
+
+ vm = vm_create_with_one_vcpu(&vcpu, guest_np_read);
+ vm_set_user_memory_region(vm, DATA_MEMSLOT, 0, DATA_GPA, getpagesize(), hva);
+ virt_map(vm, DATA_GPA, DATA_GPA, 1);
+
+ ret = _vcpu_run(vcpu);
+ TEST_ASSERT(ret == -1 && errno == EPERM,
+ "normal VM's fault on private memory: rc %d errno %d", ret, errno);
+
+ kvm_vm_free(vm);
+}
+
+static void (*host_action)(void *hva);
+
+static void on_sync(struct kvm_vcpu *vcpu, struct ucall *uc)
+{
+ host_action(addr_gpa2hva(vcpu->vm, DATA_GPA));
+}
+
+static struct kvm_vm *vm_create_with_data(struct kvm_vcpu **vcpu)
+{
+ struct kvm_vm *vm = pkvm_vm_create_with_one_vcpu(vcpu, guest_code);
+
+ vm_userspace_mem_region_add(vm, VM_MEM_SRC_ANONYMOUS, DATA_GPA, DATA_MEMSLOT,
+ DATA_PAGES, 0);
+ virt_map(vm, DATA_GPA, DATA_GPA, DATA_PAGES);
+ return vm;
+}
+
+static void test_host_access(void (*action)(void *hva), const char *name)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vm = vm_create_with_data(&vcpu);
+
+ host_action = action;
+ pkvm_run_loop(vcpu, on_sync);
+
+ kvm_vm_free(vm);
+ TEST_ASSERT_EQ(pkvm_host_locked_vm(), 0);
+
+ ksft_test_result_pass("%s\n", name);
+}
+
+/*
+ * The host never reads the guest's data: a kernel copy from private memory
+ * reads zeros, and the guest's next access to the page fails.
+ */
+static void test_process_vm_readv(void)
+{
+ u64 buf[2] = { HOST_VAL, HOST_VAL };
+ struct iovec local = { .iov_base = buf, .iov_len = sizeof(buf) };
+ struct kvm_vcpu *vcpu;
+ struct iovec remote;
+ struct kvm_vm *vm;
+ struct ucall uc;
+ ssize_t ret;
+ int i;
+
+ vm = vm_create_with_data(&vcpu);
+ vcpu_run(vcpu);
+ TEST_ASSERT_EQ(get_ucall(vcpu, &uc), UCALL_SYNC);
+
+ remote = (struct iovec){ .iov_base = addr_gpa2hva(vm, DATA_GPA), .iov_len = sizeof(buf) };
+ ret = process_vm_readv(getpid(), &local, 1, &remote, 1, 0);
+ TEST_ASSERT_EQ(ret, sizeof(buf));
+ for (i = 0; i < ARRAY_SIZE(buf); i++)
+ TEST_ASSERT(!buf[i], "process_vm_readv() of private memory read 0x%lx", buf[i]);
+
+ /* The guest's next access is its read of that page. */
+ ret = _vcpu_run(vcpu);
+ TEST_ASSERT(ret == -1 && errno == EFAULT,
+ "guest access after process_vm_readv(): rc %zd errno %d", ret, errno);
+
+ kvm_vm_free(vm);
+ TEST_ASSERT_EQ(pkvm_host_locked_vm(), 0);
+
+ ksft_test_result_pass("process_vm_readv() of private memory reads no guest data\n");
+}
+
+static const struct {
+ void (*action)(void *hva);
+ const char *name;
+} host_accesses[] = {
+ { host_write_from_private, "write() from private memory fails with EFAULT" },
+ { host_read_into_private, "read() into private memory fails with EFAULT" },
+ { host_dontneed, "MADV_DONTNEED of private memory leaves the guest's data" },
+ { host_munmap, "munmap() of private memory leaves the guest's data" },
+};
+
+int main(void)
+{
+ const char *np_vm = "a normal VM's fault on private memory fails with EPERM";
+ int i;
+
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(ARRAY_SIZE(host_accesses) + 2);
+
+ for (i = 0; i < ARRAY_SIZE(host_accesses); i++)
+ test_host_access(host_accesses[i].action, host_accesses[i].name);
+
+ test_process_vm_readv();
+
+ test_host_access(host_np_vm_on_private, np_vm);
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 15/16] KVM: arm64: selftests: Add pkvm_lifecycle teardown test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (13 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 14/16] KVM: arm64: selftests: Add pkvm_isolation host-access test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 8:48 ` [PATCH 16/16] KVM: arm64: selftests: Add pkvm_exceptions debug and SError test Fuad Tabba
2026-11-04 14:10 ` [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Joey Gouly
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
Each protected VM holds an entry in EL2's table of KVM_MAX_PVMS (255)
VMs until teardown has reclaimed its pages, so if teardown leaks
entries, KVM_CREATE_VM eventually fails.
Create, run and free twice that many VMs, four alive at once and freed
in random order. Check that each VM's memory survives the others'
teardown, and that freeing a VM drops the process's locked memory by
what its runs added.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/pkvm_lifecycle.c | 110 ++++++++++++++++++
2 files changed, 111 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_lifecycle.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 68bdd01f62270..214f38e3f67a7 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -194,6 +194,7 @@ TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
TEST_GEN_PROGS_arm64 += arm64/pkvm_id_regs
TEST_GEN_PROGS_arm64 += arm64/pkvm_irq
TEST_GEN_PROGS_arm64 += arm64/pkvm_isolation
+TEST_GEN_PROGS_arm64 += arm64/pkvm_lifecycle
TEST_GEN_PROGS_arm64 += arm64/pkvm_mem_share
TEST_GEN_PROGS_arm64 += arm64/pkvm_psci
TEST_GEN_PROGS_arm64 += arm64/pkvm_smccc
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_lifecycle.c b/tools/testing/selftests/kvm/arm64/pkvm_lifecycle.c
new file mode 100644
index 0000000000000..0ee475799abc5
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_lifecycle.c
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_lifecycle - more protected VMs than EL2's VM table holds, freed out of order.
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include <stdlib.h>
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+/* EL2's VM table has KVM_MAX_PVMS entries, and a VM holds one until it's freed. */
+#define VM_TABLE_SIZE 255
+/* Enough that leaking the entry of one VM in two fills the table. */
+#define NR_VMS (2 * VM_TABLE_SIZE)
+#define NR_ALIVE 4
+
+struct pvm {
+ struct kvm_vm *vm;
+ struct kvm_vcpu *vcpu;
+ u64 tag;
+ size_t locked;
+};
+
+/* Set in the guest's copy by the host before the first run. */
+static u64 tag;
+
+static void guest_code(void)
+{
+ pkvm_guest_init();
+ for (;;)
+ GUEST_SYNC(READ_ONCE(tag));
+}
+
+/* The tag comes from the guest's own memory, so the VM must survive the others' teardown. */
+static void run_vm(struct pvm *p)
+{
+ size_t locked = pkvm_host_locked_vm();
+ struct ucall uc;
+ u64 cmd;
+
+ vcpu_run(p->vcpu);
+
+ switch (cmd = get_ucall(p->vcpu, &uc)) {
+ case UCALL_SYNC:
+ break;
+ case UCALL_ABORT:
+ REPORT_GUEST_ASSERT(uc);
+ break;
+ default:
+ TEST_FAIL("Unexpected guest exit: ucall %lu", cmd);
+ }
+ TEST_ASSERT_EQ(uc.args[1], p->tag);
+
+ p->locked += pkvm_host_locked_vm() - locked;
+}
+
+static void create_vm(struct pvm *p, u64 tag_val)
+{
+ p->vm = pkvm_vm_create_with_one_vcpu(&p->vcpu, guest_code);
+ p->tag = tag_val;
+ p->locked = 0;
+ write_guest_global(p->vm, tag, tag_val);
+
+ run_vm(p);
+ TEST_ASSERT(p->locked, "VM %lu pinned no memory", tag_val);
+}
+
+static void free_vm(struct pvm *p)
+{
+ size_t locked = pkvm_host_locked_vm();
+
+ kvm_vm_free(p->vm);
+ TEST_ASSERT_EQ(locked - pkvm_host_locked_vm(), p->locked);
+}
+
+int main(void)
+{
+ struct pvm vms[NR_ALIVE];
+ int i, j;
+
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(1);
+
+ for (i = 0; i < NR_ALIVE; i++)
+ create_vm(&vms[i], i + 1);
+
+ for (; i < NR_VMS; i++) {
+ struct pvm *victim = &vms[random() % NR_ALIVE];
+
+ for (j = 0; j < NR_ALIVE; j++)
+ run_vm(&vms[j]);
+
+ free_vm(victim);
+ create_vm(victim, i + 1);
+ }
+
+ for (j = 0; j < NR_ALIVE; j++)
+ free_vm(&vms[j]);
+
+ ksft_test_result_pass("%d VMs, %d alive at once, freed in random order\n",
+ NR_VMS, NR_ALIVE);
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* [PATCH 16/16] KVM: arm64: selftests: Add pkvm_exceptions debug and SError test
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (14 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 15/16] KVM: arm64: selftests: Add pkvm_lifecycle teardown test Fuad Tabba
@ 2026-11-04 8:48 ` Fuad Tabba
2026-11-04 14:10 ` [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Joey Gouly
16 siblings, 0 replies; 18+ messages in thread
From: Fuad Tabba @ 2026-11-04 8:48 UTC (permalink / raw)
To: Marc Zyngier, Oliver Upton
Cc: Will Deacon, Paolo Bonzini, Sean Christopherson, Shuah Khan,
Joey Gouly, Steffen Eiden, Suzuki K Poulose, Zenghui Yu,
Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
pKVM can hide self-hosted debug from a protected guest, which then reads
ID_AA64DFR0_EL1.DebugVer as 0. A virtual SError the host injects has to
be delivered by EL2, since the host can't see whether the guest has it
masked.
Verify that the guest's own handler takes a hardware breakpoint, a
watchpoint and a single step, and that an injected SError is pending in
ISR_EL1.A while PSTATE.A masks it and is taken once unmasked. The debug
cases skip while ID_AA64DFR0_EL1.DebugVer is 0.
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
tools/testing/selftests/kvm/Makefile.kvm | 1 +
.../selftests/kvm/arm64/pkvm_exceptions.c | 261 ++++++++++++++++++
2 files changed, 262 insertions(+)
create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_exceptions.c
diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 214f38e3f67a7..1045136aacc16 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -190,6 +190,7 @@ TEST_GEN_PROGS_arm64 += arm64/nv_pre_fault_memory_test
TEST_GEN_PROGS_arm64 += arm64/external_aborts
TEST_GEN_PROGS_arm64 += arm64/mmio_sign_ext
TEST_GEN_PROGS_arm64 += arm64/page_fault_test
+TEST_GEN_PROGS_arm64 += arm64/pkvm_exceptions
TEST_GEN_PROGS_arm64 += arm64/pkvm_host_abi
TEST_GEN_PROGS_arm64 += arm64/pkvm_id_regs
TEST_GEN_PROGS_arm64 += arm64/pkvm_irq
diff --git a/tools/testing/selftests/kvm/arm64/pkvm_exceptions.c b/tools/testing/selftests/kvm/arm64/pkvm_exceptions.c
new file mode 100644
index 0000000000000..f7f9b666dd089
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/pkvm_exceptions.c
@@ -0,0 +1,261 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * pkvm_exceptions - debug exceptions and a virtual SError in a protected VM.
+ *
+ * Copyright (C) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+
+#include "kvm_util.h"
+#include "pkvm.h"
+#include "processor.h"
+#include "test_util.h"
+
+/* Debug register programming bits, as debug-exceptions.c has them. */
+#define DBGBCR_LEN8 (0xff << 5)
+#define DBGBCR_EXEC (0x0 << 3)
+#define DBGBCR_EL1 (0x1 << 1)
+#define DBGBCR_E (0x1 << 0)
+
+#define DBGWCR_LEN8 (0xff << 5)
+#define DBGWCR_RD (0x1 << 3)
+#define DBGWCR_WR (0x2 << 3)
+#define DBGWCR_EL1 (0x1 << 1)
+#define DBGWCR_E (0x1 << 0)
+
+#define SPSR_SS (1 << 21)
+
+#define NR_STEPS 3
+
+enum { CMD_INJECT_SERROR, CMD_CHECK_SERROR };
+
+extern unsigned char hw_bp, ss_start;
+
+static volatile u64 ex_pc, ex_addr;
+static volatile u64 ss_addr[NR_STEPS + 1];
+static volatile int ss_idx;
+static volatile char write_data;
+static volatile bool serror_taken;
+
+static bool debug_exposed;
+
+/* Masks debug on return so the breakpointed instruction runs. */
+static void hw_bp_handler(struct ex_regs *regs)
+{
+ ex_pc = regs->pc;
+ regs->pstate |= PSR_D_BIT;
+}
+
+/* Records FAR, the accessed address, and masks debug on return so the access completes. */
+static void wp_handler(struct ex_regs *regs)
+{
+ ex_pc = regs->pc;
+ ex_addr = read_sysreg(far_el1);
+ regs->pstate |= PSR_D_BIT;
+}
+
+/* Records the stepped PC and sets SPSR.SS so that the next instruction is stepped too. */
+static void ss_handler(struct ex_regs *regs)
+{
+ __GUEST_ASSERT(ss_idx <= NR_STEPS, "step %d past the stepped range", ss_idx);
+ ss_addr[ss_idx++] = regs->pc;
+ regs->pstate |= SPSR_SS;
+}
+
+static void reset_debug_state(void)
+{
+ asm volatile("msr daifset, #8");
+
+ /* Clear the OS lock and double lock so debug exceptions are permitted. */
+ write_sysreg(0, osdlr_el1);
+ write_sysreg(0, oslar_el1);
+ isb();
+
+ write_sysreg(0, mdscr_el1);
+ write_sysreg(0, dbgbcr0_el1);
+ write_sysreg(0, dbgbvr0_el1);
+ write_sysreg(0, dbgwcr0_el1);
+ write_sysreg(0, dbgwvr0_el1);
+ isb();
+
+ ex_pc = 0;
+ ex_addr = 0;
+}
+
+/* A zero DebugVer means EL2 hides self-hosted debug, so the case has nothing to test. */
+static bool guest_debug_exposed(void)
+{
+ bool exposed = SYS_FIELD_GET(ID_AA64DFR0_EL1, DebugVer, read_sysreg(id_aa64dfr0_el1));
+
+ GUEST_SYNC(exposed);
+ return exposed;
+}
+
+static void enable_monitor_debug(void)
+{
+ asm volatile("msr daifclr, #8");
+ write_sysreg(read_sysreg(mdscr_el1) | MDSCR_EL1_KDE | MDSCR_EL1_MDE, mdscr_el1);
+ isb();
+}
+
+static void guest_hw_breakpoint(void)
+{
+ pkvm_guest_init();
+ if (!guest_debug_exposed())
+ GUEST_DONE();
+
+ reset_debug_state();
+ write_sysreg(DBGBCR_LEN8 | DBGBCR_EXEC | DBGBCR_EL1 | DBGBCR_E, dbgbcr0_el1);
+ write_sysreg((u64)&hw_bp, dbgbvr0_el1);
+ enable_monitor_debug();
+ asm volatile("hw_bp: nop");
+ GUEST_ASSERT_EQ(ex_pc, (u64)&hw_bp);
+
+ reset_debug_state();
+ GUEST_DONE();
+}
+
+static void guest_watchpoint(void)
+{
+ pkvm_guest_init();
+ if (!guest_debug_exposed())
+ GUEST_DONE();
+
+ reset_debug_state();
+ write_sysreg(DBGWCR_LEN8 | DBGWCR_RD | DBGWCR_WR | DBGWCR_EL1 | DBGWCR_E, dbgwcr0_el1);
+ /*
+ * BAS = 0xff watches all eight bytes only from a doubleword-aligned DBGWVR.
+ * FAR still holds the byte's address.
+ */
+ write_sysreg((u64)&write_data & ~7UL, dbgwvr0_el1);
+ enable_monitor_debug();
+ write_data = 'x';
+ GUEST_ASSERT_EQ(write_data, 'x');
+ GUEST_ASSERT_EQ(ex_addr, (u64)&write_data);
+
+ reset_debug_state();
+ GUEST_DONE();
+}
+
+static void guest_single_step(void)
+{
+ pkvm_guest_init();
+ if (!guest_debug_exposed())
+ GUEST_DONE();
+
+ reset_debug_state();
+ write_sysreg(read_sysreg(mdscr_el1) | MDSCR_EL1_KDE | MDSCR_EL1_SS, mdscr_el1);
+ isb();
+ ss_idx = 0;
+ /* Unmasking makes the step active-pending, so the first one is taken at ss_start. */
+ asm volatile("msr daifclr, #8\n"
+ "ss_start:\n"
+ "mrs x0, esr_el1\n"
+ "add x0, x0, #1\n"
+ "msr daifset, #8\n"
+ : : : "x0");
+ GUEST_ASSERT_EQ(ss_addr[0], (u64)&ss_start);
+ GUEST_ASSERT_EQ(ss_addr[1], (u64)&ss_start + 4);
+ GUEST_ASSERT_EQ(ss_addr[2], (u64)&ss_start + 8);
+
+ reset_debug_state();
+ GUEST_DONE();
+}
+
+static void serror_handler(struct ex_regs *regs)
+{
+ GUEST_ASSERT_EQ(ESR_ELx_EC(read_sysreg(esr_el1)), ESR_ELx_EC_SERROR);
+ serror_taken = true;
+}
+
+/*
+ * The guest sees the injected SError in ISR_EL1.A while PSTATE.A masks it, and
+ * takes it on unmasking. EL2 has to deliver it through HCR_EL2.VSE, because the
+ * host can't read a protected guest's PSTATE.A for serror_is_masked().
+ */
+static void guest_serror(void)
+{
+ pkvm_guest_init();
+
+ GUEST_SYNC(CMD_INJECT_SERROR);
+ GUEST_ASSERT(read_sysreg(isr_el1) & ISR_EL1_A);
+ GUEST_ASSERT(!serror_taken);
+
+ local_serror_enable();
+ isb();
+ local_serror_disable();
+
+ GUEST_ASSERT(serror_taken);
+ GUEST_SYNC(CMD_CHECK_SERROR);
+ GUEST_DONE();
+}
+
+static void serror_sync(struct kvm_vcpu *vcpu, struct ucall *uc)
+{
+ struct kvm_vcpu_events events = {};
+
+ switch (uc->args[1]) {
+ case CMD_INJECT_SERROR:
+ events.exception.serror_pending = true;
+ vcpu_events_set(vcpu, &events);
+ memset(&events, 0, sizeof(events));
+ vcpu_events_get(vcpu, &events);
+ TEST_ASSERT(events.exception.serror_pending, "vSError not pending after injection");
+ break;
+ case CMD_CHECK_SERROR:
+ vcpu_events_get(vcpu, &events);
+ TEST_ASSERT(!events.exception.serror_pending, "vSError still pending once taken");
+ break;
+ }
+}
+
+static void test_serror(void)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_serror);
+ vm_install_exception_handler(vm, VECTOR_ERROR_CURRENT, serror_handler);
+ pkvm_run_loop(vcpu, serror_sync);
+ kvm_vm_free(vm);
+}
+
+static void debug_sync(struct kvm_vcpu *vcpu, struct ucall *uc)
+{
+ debug_exposed = uc->args[1];
+}
+
+static void run_debug_guest(void *guest_code, int ec, handler_fn handler, const char *name)
+{
+ struct kvm_vcpu *vcpu;
+ struct kvm_vm *vm;
+
+ vm = pkvm_vm_create_with_one_vcpu(&vcpu, guest_code);
+ vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ec, handler);
+ pkvm_run_loop(vcpu, debug_sync);
+ kvm_vm_free(vm);
+
+ if (debug_exposed)
+ ksft_test_result_pass("%s\n", name);
+ else
+ ksft_test_result_skip("%s: self-hosted debug not exposed to protected guests\n",
+ name);
+}
+
+int main(void)
+{
+ TEST_REQUIRE(pkvm_supported());
+
+ ksft_print_header();
+ ksft_set_plan(4);
+
+ run_debug_guest(guest_hw_breakpoint, ESR_ELx_EC_BREAKPT_CUR, hw_bp_handler,
+ "hardware breakpoint");
+ run_debug_guest(guest_watchpoint, ESR_ELx_EC_WATCHPT_CUR, wp_handler, "watchpoint");
+ run_debug_guest(guest_single_step, ESR_ELx_EC_SOFTSTP_CUR, ss_handler, "single step");
+
+ test_serror();
+ ksft_test_result_pass("virtual SError taken on unmasking\n");
+
+ ksft_finished();
+}
--
2.47.3
^ permalink raw reply related [flat|nested] 18+ messages in thread
* Re: [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
` (15 preceding siblings ...)
2026-11-04 8:48 ` [PATCH 16/16] KVM: arm64: selftests: Add pkvm_exceptions debug and SError test Fuad Tabba
@ 2026-11-04 14:10 ` Joey Gouly
16 siblings, 0 replies; 18+ messages in thread
From: Joey Gouly @ 2026-11-04 14:10 UTC (permalink / raw)
To: Fuad Tabba
Cc: Marc Zyngier, Oliver Upton, Will Deacon, Paolo Bonzini,
Sean Christopherson, Shuah Khan, Steffen Eiden, Suzuki K Poulose,
Zenghui Yu, Vincent Donnefort, Quentin Perret, Fuad Tabba, kvmarm,
linux-arm-kernel, kvm, linux-kselftest, linux-kernel
On Wed, Nov 04, 2026 at 08:48:26AM +0000, Fuad Tabba wrote:
> Hi folks,
>
> This series adds selftests for protected VMs. A test skips on a host
> without pKVM, and so does any case where the host doesn't have the
> required feature, e.g., PtrAuth, SVE or hugetlb pages.
>
> A couple of points:
>
> - Each area gets its own test rather than cases folded into existing
> tests such as psci_test, hypercalls, set_id_regs, etc. A protected
> guest requires its own boot path, and has to share the ucall pool
> before its first ucall. On top of that, a protected guest gets the
> opposite result for many of those tests' checks.
>
> - pkvm_exceptions skips its breakpoint, watchpoint and single-step cases
> while a protected guest's ID_AA64DFR0_EL1.DebugVer is 0. pkvm_id_regs
> reads the same field to choose between the hidden and the exposed
> debug view. With the patch that adds self-hosted debug to protected
> guests [1], both tests run the debug cases with no change.
>
> The series is structured as follows:
>
> 01: external_aborts: an abort injected before a vCPU's first run.
> 02-04: Generic selftest changes: creating a protected VM, the pKVM
> function IDs and the ucall pool's address.
> 05-06: Protected-VM library: boot trampoline and helpers.
> 07-16: The tests, one per area.
>
> Based on Linux 7.4-rc1 (6628e7d4c6b25).
Hi!
I started to also write some realm tests a bit, you can check it out
here:
https://gitlab.arm.com/linux-arm/linux-jg/-/tree/cca-kselftest
It's WIP/prototype, but it may be good to have some sharing, if
possible.
For example I also added a 'pre-boot' stub, which looks like your
pvm-entry.S:
https://gitlab.arm.com/linux-arm/linux-jg/-/commit/cd3ae35d89974b02b7051a0c45cc68b4df8f9d3e
However I extracted the registers needed for the stub like this:
https://gitlab.arm.com/linux-arm/linux-jg/-/commit/f559d5b61fa1a37e2d7b23704bd367e079cf8eae
This is because the current Realm branch I'm working on top of rejects
writes to those system registers (TCR_EL1, CPACR_EL1 etc). Which is why
I had to modify these functions, along with __aarch64_vcpu_add() and
some others.
I'm not sure how much there is to gain by sharing overall, but good to
bring it up early!
Thanks,
Joey
>
> Cheers,
> /fuad
>
> [1] https://lore.kernel.org/all/20261102093950.2922035-1-fuad.tabba@linux.dev/
>
> Fuad Tabba (16):
> KVM: arm64: selftests: Inject an abort before a vCPU's first run
> KVM: selftests: Allow arm64 tests to create protected VMs
> tools: arm-smccc: Add the pKVM memory sharing function IDs
> KVM: selftests: Export the ucall pool's guest physical address
> KVM: arm64: selftests: Add protected-VM boot trampoline
> KVM: arm64: selftests: Add protected-VM library helpers
> KVM: arm64: selftests: Add pkvm_host_abi host-interface test
> KVM: arm64: selftests: Add pkvm_psci multi-vCPU PSCI test
> KVM: arm64: selftests: Add pkvm_mem_share hypercall test
> KVM: arm64: selftests: Add pkvm_smccc hypercall test
> KVM: arm64: selftests: Add pkvm_id_regs register and trap test
> KVM: arm64: selftests: Add pkvm_vcpu_regs register-state test
> KVM: arm64: selftests: Add pkvm_irq interrupt test
> KVM: arm64: selftests: Add pkvm_isolation host-access test
> KVM: arm64: selftests: Add pkvm_lifecycle teardown test
> KVM: arm64: selftests: Add pkvm_exceptions debug and SError test
>
> tools/include/linux/arm-smccc.h | 22 +
> tools/testing/selftests/kvm/Makefile.kvm | 12 +
> .../selftests/kvm/arm64/external_aborts.c | 21 +
> .../selftests/kvm/arm64/pkvm_exceptions.c | 261 ++++++++
> .../selftests/kvm/arm64/pkvm_host_abi.c | 428 ++++++++++++
> .../selftests/kvm/arm64/pkvm_id_regs.c | 306 +++++++++
> tools/testing/selftests/kvm/arm64/pkvm_irq.c | 268 ++++++++
> .../selftests/kvm/arm64/pkvm_isolation.c | 225 +++++++
> .../selftests/kvm/arm64/pkvm_lifecycle.c | 110 +++
> .../selftests/kvm/arm64/pkvm_mem_share.c | 394 +++++++++++
> tools/testing/selftests/kvm/arm64/pkvm_psci.c | 632 ++++++++++++++++++
> .../testing/selftests/kvm/arm64/pkvm_smccc.c | 117 ++++
> .../selftests/kvm/arm64/pkvm_vcpu_regs.c | 221 ++++++
> .../selftests/kvm/include/arm64/pkvm.h | 74 ++
> .../selftests/kvm/include/ucall_common.h | 1 +
> tools/testing/selftests/kvm/lib/arm64/pkvm.c | 245 +++++++
> .../selftests/kvm/lib/arm64/pvm-entry.S | 58 ++
> tools/testing/selftests/kvm/lib/kvm_util.c | 5 +-
> .../testing/selftests/kvm/lib/ucall_common.c | 9 +-
> 19 files changed, 3406 insertions(+), 3 deletions(-)
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_exceptions.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_host_abi.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_id_regs.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_irq.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_isolation.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_lifecycle.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_mem_share.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_psci.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_smccc.c
> create mode 100644 tools/testing/selftests/kvm/arm64/pkvm_vcpu_regs.c
> create mode 100644 tools/testing/selftests/kvm/include/arm64/pkvm.h
> create mode 100644 tools/testing/selftests/kvm/lib/arm64/pkvm.c
> create mode 100644 tools/testing/selftests/kvm/lib/arm64/pvm-entry.S
>
>
> base-commit: 6628e7d4c6b25c8d18f1345e3a20d21829aae7e4
> --
> 2.47.3
>
^ permalink raw reply [flat|nested] 18+ messages in thread
end of thread, other threads:[~2026-11-04 14:11 UTC | newest]
Thread overview: 18+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-04 8:48 [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Fuad Tabba
2026-11-04 8:48 ` [PATCH 01/16] KVM: arm64: selftests: Inject an abort before a vCPU's first run Fuad Tabba
2026-11-04 8:48 ` [PATCH 02/16] KVM: selftests: Allow arm64 tests to create protected VMs Fuad Tabba
2026-11-04 8:48 ` [PATCH 03/16] tools: arm-smccc: Add the pKVM memory sharing function IDs Fuad Tabba
2026-11-04 8:48 ` [PATCH 04/16] KVM: selftests: Export the ucall pool's guest physical address Fuad Tabba
2026-11-04 8:48 ` [PATCH 05/16] KVM: arm64: selftests: Add protected-VM boot trampoline Fuad Tabba
2026-11-04 8:48 ` [PATCH 06/16] KVM: arm64: selftests: Add protected-VM library helpers Fuad Tabba
2026-11-04 8:48 ` [PATCH 07/16] KVM: arm64: selftests: Add pkvm_host_abi host-interface test Fuad Tabba
2026-11-04 8:48 ` [PATCH 08/16] KVM: arm64: selftests: Add pkvm_psci multi-vCPU PSCI test Fuad Tabba
2026-11-04 8:48 ` [PATCH 09/16] KVM: arm64: selftests: Add pkvm_mem_share hypercall test Fuad Tabba
2026-11-04 8:48 ` [PATCH 10/16] KVM: arm64: selftests: Add pkvm_smccc " Fuad Tabba
2026-11-04 8:48 ` [PATCH 11/16] KVM: arm64: selftests: Add pkvm_id_regs register and trap test Fuad Tabba
2026-11-04 8:48 ` [PATCH 12/16] KVM: arm64: selftests: Add pkvm_vcpu_regs register-state test Fuad Tabba
2026-11-04 8:48 ` [PATCH 13/16] KVM: arm64: selftests: Add pkvm_irq interrupt test Fuad Tabba
2026-11-04 8:48 ` [PATCH 14/16] KVM: arm64: selftests: Add pkvm_isolation host-access test Fuad Tabba
2026-11-04 8:48 ` [PATCH 15/16] KVM: arm64: selftests: Add pkvm_lifecycle teardown test Fuad Tabba
2026-11-04 8:48 ` [PATCH 16/16] KVM: arm64: selftests: Add pkvm_exceptions debug and SError test Fuad Tabba
2026-11-04 14:10 ` [PATCH 00/16] KVM: arm64: selftests: Tests for protected VMs Joey Gouly
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox