mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v2 0/4] KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM
@ 2026-09-28  6:46 Fuad Tabba
  2026-09-28  6:46 ` [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1 Fuad Tabba
                   ` (3 more replies)
  0 siblings, 4 replies; 9+ messages in thread
From: Fuad Tabba @ 2026-09-28  6:46 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel
  Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Steffen Eiden,
	Catalin Marinas, Will Deacon, Mark Rutland, Quentin Perret,
	Vincent Donnefort, Wei-Lin Chang, Fuad Tabba, linux-kernel

Hi folks,

Changes since v1 [1]:
- Patch 3: take a list of host bits per VM type instead of the host's
  value minus the bits EL2 owns; drop the HCR_GPF definition (Marc).
- Patches 2 and 3: use the HCR_EL2_* names in added lines (Marc).
- Patches 1-3: reworded the messages, and the comment in patch 2
  (Marc).
- Patch 1: collected Wei-Lin's Reviewed-by.

In pKVM, EL2 sets a non-protected VM's HCR_EL2 in pkvm_vcpu_reset_hcr(),
which misses the RW, TID5 and TTLBOS handling of vcpu_set_hcr(), and
takes only TWI, TWE and VSE from the host. As a result, an AArch32 VM
can't run, a VM can read GMID_EL1 or execute a TLBI OS its ID registers
hide, and the host's TVM, VI and VF never reach it.

The second patch clears RW for an AArch32 vCPU. The third also takes
from the host, for a non-protected VM, the bits the host varies with the
VM's configuration or at runtime: VI, VF, TVM, TID2, TID4, TID5 and
TTLBOS. The rest stay EL2's, and a protected VM still takes only TWI,
TWE and VSE.

The third patch depends on the first: once TTLBOS reaches the VM, a
trapped TLBI OS from a non-nested guest hits a WARN in
handle_tlbi_el1(), as it does without pKVM.

The last patch adds a selftest that checks a feature hidden in an ID
register is UNDEFINED in the guest. Its TLBI OS case fails in pKVM
before the third patch.

VSE still comes from the host as before. Syncing it back after delivery
is a separate fix [2].

Based on Linux 7.3-rc4 (93f51579e7df2).

Cheers,
/fuad

[1] https://lore.kernel.org/all/20260925090619.852995-1-fuad.tabba@linux.dev/
[2] https://lore.kernel.org/all/20260921101030.1231605-1-fuad.tabba@linux.dev/

Fuad Tabba (4):
  KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1
  KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs
  KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM
  KVM: arm64: selftests: Check a feature hidden in an ID register is
    UNDEF

 arch/arm64/kvm/hyp/include/nvhe/pkvm.h        |   9 +
 arch/arm64/kvm/hyp/nvhe/hyp-main.c            |   7 +-
 arch/arm64/kvm/hyp/nvhe/pkvm.c                |  25 ++-
 arch/arm64/kvm/sys_regs.c                     |   7 +-
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 .../selftests/kvm/arm64/hidden_features.c     | 184 ++++++++++++++++++
 6 files changed, 218 insertions(+), 15 deletions(-)
 create mode 100644 tools/testing/selftests/kvm/arm64/hidden_features.c


base-commit: 93f51579e7df248780214094418f205253383cc5
-- 
2.39.5


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1
  2026-09-28  6:46 [PATCH v2 0/4] KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
@ 2026-09-28  6:46 ` Fuad Tabba
  2026-09-28 17:05   ` Oliver Upton
  2026-09-28  6:46 ` [PATCH v2 2/4] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs Fuad Tabba
                   ` (2 subsequent siblings)
  3 siblings, 1 reply; 9+ messages in thread
From: Fuad Tabba @ 2026-09-28  6:46 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel
  Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Steffen Eiden,
	Catalin Marinas, Will Deacon, Mark Rutland, Quentin Perret,
	Vincent Donnefort, Wei-Lin Chang, Fuad Tabba, linux-kernel

KVM hides TLBI OS from a guest whose ID registers don't advertise it by
trapping the instructions: through the fine-grained traps on a CPU with
FGT, and through HCR_EL2.TTLBOS on one with FEAT_EVT2. With FGT,
triage_sysreg_trap() makes a trapped TLBI OS UNDEFINED. Without it, the
instruction reaches handle_tlbi_el1(), which assumes an EL1 TLBI only
traps from a guest at vEL2 and WARNs before checking whether the guest
supports it. The guest still gets its UNDEF after the WARN. A VMM can
trigger the WARN by hiding TLBI OS and having the guest execute one.

Check support before the WARN.

Fixes: 0cb8aae226768 ("KVM: arm64: nv: Add handling of outer-shareable TLBI operations")
Cc: stable@vger.kernel.org
Reviewed-by: Wei-Lin Chang <weilin.chang@arm.com>
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
 arch/arm64/kvm/sys_regs.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/arch/arm64/kvm/sys_regs.c b/arch/arm64/kvm/sys_regs.c
index 44aae52c473d7..0ce29ce678b08 100644
--- a/arch/arm64/kvm/sys_regs.c
+++ b/arch/arm64/kvm/sys_regs.c
@@ -4257,6 +4257,10 @@ static bool handle_tlbi_el1(struct kvm_vcpu *vcpu, struct sys_reg_params *p,
 {
 	u32 sys_encoding = sys_insn(p->Op0, p->Op1, p->CRn, p->CRm, p->Op2);
 
+	/* Without FGT, HCR_EL2.TTLBOS also traps a hidden TLBI OS from vEL1 */
+	if (!kvm_supported_tlbi_s1e1_op(vcpu, sys_encoding))
+		return undef_access(vcpu, p, r);
+
 	/*
 	 * If we're here, this is because we've trapped on a EL1 TLBI
 	 * instruction that affects the EL1 translation regime while
@@ -4279,9 +4283,6 @@ static bool handle_tlbi_el1(struct kvm_vcpu *vcpu, struct sys_reg_params *p,
 
 	WARN_ON(!vcpu_is_el2(vcpu));
 
-	if (!kvm_supported_tlbi_s1e1_op(vcpu, sys_encoding))
-		return undef_access(vcpu, p, r);
-
 	if (vcpu_el2_e2h_is_set(vcpu) && vcpu_el2_tge_is_set(vcpu)) {
 		kvm_handle_s1e2_tlbi(vcpu, sys_encoding, p->regval);
 		return true;
-- 
2.39.5


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 2/4] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs
  2026-09-28  6:46 [PATCH v2 0/4] KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
  2026-09-28  6:46 ` [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1 Fuad Tabba
@ 2026-09-28  6:46 ` Fuad Tabba
  2026-09-28  6:46 ` [PATCH v2 3/4] KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
  2026-09-28  6:46 ` [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF Fuad Tabba
  3 siblings, 0 replies; 9+ messages in thread
From: Fuad Tabba @ 2026-09-28  6:46 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel
  Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Steffen Eiden,
	Catalin Marinas, Will Deacon, Mark Rutland, Quentin Perret,
	Vincent Donnefort, Wei-Lin Chang, Fuad Tabba, linux-kernel

pKVM keeps its own copy of each vCPU's HCR_EL2 at EL2, initialised with
RW set unconditionally. Nothing stops userspace from creating a
non-protected AArch32 VM, and with RW set, entering one of its vCPUs is
an illegal exception return: KVM_RUN fails with KVM_EXIT_FAIL_ENTRY.

Clear RW for a vCPU that is AArch32 at EL1, when the CPU has AArch32
EL1. On a CPU without it, RW stays set and the entry still fails, rather
than EL2 switching *32_EL2 registers that are UNDEFINED there. The host
already rejects such a vCPU, but EL2 takes the vCPU's features from the
host and doesn't rely on that.

Fixes: b56680de9c648 ("KVM: arm64: Initialize trap register values in hyp in pKVM")
Cc: stable@vger.kernel.org
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
 arch/arm64/kvm/hyp/nvhe/pkvm.c | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
index 459bd9eb7e4bc..62d432144d44c 100644
--- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
+++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
@@ -54,6 +54,15 @@ static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu)
 	else
 		vcpu->arch.hcr_el2 |= HCR_TID2;
 
+	/*
+	 * Without AArch32 EL1, leave RW set and let the entry fail with an
+	 * illegal exception return: the *32_EL2 registers EL2 would otherwise
+	 * switch are UNDEFINED there.
+	 */
+	if (vcpu_has_feature(vcpu, KVM_ARM_VCPU_EL1_32BIT) &&
+	    cpus_have_final_cap(ARM64_HAS_32BIT_EL1))
+		vcpu->arch.hcr_el2 &= ~HCR_EL2_RW;
+
 	if (vcpu_has_ptrauth(vcpu))
 		vcpu->arch.hcr_el2 |= (HCR_API | HCR_APK);
 
-- 
2.39.5


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 3/4] KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM
  2026-09-28  6:46 [PATCH v2 0/4] KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
  2026-09-28  6:46 ` [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1 Fuad Tabba
  2026-09-28  6:46 ` [PATCH v2 2/4] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs Fuad Tabba
@ 2026-09-28  6:46 ` Fuad Tabba
  2026-09-28  6:46 ` [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF Fuad Tabba
  3 siblings, 0 replies; 9+ messages in thread
From: Fuad Tabba @ 2026-09-28  6:46 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel
  Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Steffen Eiden,
	Catalin Marinas, Will Deacon, Mark Rutland, Quentin Perret,
	Vincent Donnefort, Wei-Lin Chang, Fuad Tabba, linux-kernel

For each vCPU, pKVM's EL2 builds its own HCR_EL2 and takes only TWI, TWE
and VSE from the host's value on each entry. For a non-protected VM it
has fallen behind the host's: on a CPU with MTE the guest can read
GMID_EL1, on one with FEAT_EVT2 but no FGT it can execute a TLBI OS its
ID registers hide, an interrupt injected without a vGIC (VI, VF) never
arrives, and set/way emulation loses the TVM trap it relies on.

For a non-protected VM, also take the bits the host varies with the VM's
configuration or at runtime: VI, VF, TVM, TID2, TID4, TID5 and TTLBOS.
They only pend interrupts for, or add traps to, a VM the host controls.
The traps exit to the host, which handles them as it does without pKVM.
The bits that change what EL2 does on entry and exit (E2H, RW, API/APK)
or depend only on the CPU (TEA, TERR, FWB) stay EL2's. A protected VM
still takes only TWI, TWE and VSE.

EL2 now sets TID2 or TID4 only for a protected VM, and never sets ATA,
as the host rejects KVM_CAP_ARM_MTE in pKVM.

Fixes: b56680de9c648 ("KVM: arm64: Initialize trap register values in hyp in pKVM")
Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
 arch/arm64/kvm/hyp/include/nvhe/pkvm.h |  9 +++++++++
 arch/arm64/kvm/hyp/nvhe/hyp-main.c     |  7 ++++---
 arch/arm64/kvm/hyp/nvhe/pkvm.c         | 18 ++++++++----------
 3 files changed, 21 insertions(+), 13 deletions(-)

diff --git a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
index c904647d2f760..5c050f21066ab 100644
--- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
+++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h
@@ -12,6 +12,15 @@
 #include <nvhe/gfp.h>
 #include <nvhe/spinlock.h>
 
+/*
+ * HCR_EL2 bits EL2 takes from the host on each entry, per VM type. The rest
+ * are EL2's own and nothing the host sets there reaches the guest.
+ */
+#define PKVM_HCR_EL2_HOST_PVM	(HCR_EL2_TWI | HCR_EL2_TWE | HCR_EL2_VSE)
+#define PKVM_HCR_EL2_HOST_NPVM	(PKVM_HCR_EL2_HOST_PVM | HCR_EL2_VI | HCR_EL2_VF |	\
+				 HCR_EL2_TVM | HCR_EL2_TID2 | HCR_EL2_TID4 |		\
+				 HCR_EL2_TID5 | HCR_EL2_TTLBOS)
+
 /*
  * Holds the relevant data for maintaining the vcpu state completely at hyp.
  */
diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-main.c b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
index 9a3b92e626adb..ac64a036b0a95 100644
--- a/arch/arm64/kvm/hyp/nvhe/hyp-main.c
+++ b/arch/arm64/kvm/hyp/nvhe/hyp-main.c
@@ -216,6 +216,7 @@ static void sync_debug_state(struct pkvm_hyp_vcpu *hyp_vcpu)
 static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
 {
 	struct kvm_vcpu *host_vcpu = hyp_vcpu->host_vcpu;
+	u64 host_hcr_mask = PKVM_HCR_EL2_HOST_PVM;
 
 	fpsimd_sve_flush();
 	flush_debug_state(hyp_vcpu);
@@ -228,6 +229,7 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
 	if (!pkvm_hyp_vcpu_is_protected(hyp_vcpu)) {
 		if (vcpu_get_flag(host_vcpu, PKVM_HOST_STATE_DIRTY))
 			flush_hyp_vcpu_state(hyp_vcpu);
+		host_hcr_mask = PKVM_HCR_EL2_HOST_NPVM;
 	} else {
 		hyp_vcpu->vcpu.arch.ctxt = host_vcpu->arch.ctxt;
 	}
@@ -241,9 +243,8 @@ static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu)
 	 * trap-control bit, so it must flow to the hyp vCPU alongside TWI/TWE
 	 * for the vSError to be delivered. sync_hyp_vcpu() reflects it back.
 	 */
-	hyp_vcpu->vcpu.arch.hcr_el2 &= ~(HCR_TWI | HCR_TWE | HCR_VSE);
-	hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) &
-						 (HCR_TWI | HCR_TWE | HCR_VSE);
+	hyp_vcpu->vcpu.arch.hcr_el2 &= ~host_hcr_mask;
+	hyp_vcpu->vcpu.arch.hcr_el2 |= READ_ONCE(host_vcpu->arch.hcr_el2) & host_hcr_mask;
 
 	hyp_vcpu->vcpu.arch.iflags	= host_vcpu->arch.iflags;
 
diff --git a/arch/arm64/kvm/hyp/nvhe/pkvm.c b/arch/arm64/kvm/hyp/nvhe/pkvm.c
index 62d432144d44c..7ef09867f2802 100644
--- a/arch/arm64/kvm/hyp/nvhe/pkvm.c
+++ b/arch/arm64/kvm/hyp/nvhe/pkvm.c
@@ -30,6 +30,7 @@ unsigned int kvm_host_sve_max_vl;
  */
 static DEFINE_PER_CPU(struct pkvm_hyp_vcpu *, loaded_hyp_vcpu);
 
+/* The PKVM_HCR_EL2_HOST_{PVM,NPVM} bits of this value come from the host on each entry. */
 static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu)
 {
 	vcpu->arch.hcr_el2 = HCR_GUEST_FLAGS;
@@ -47,13 +48,6 @@ static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu)
 	if (cpus_have_final_cap(ARM64_HAS_STAGE2_FWB))
 		vcpu->arch.hcr_el2 |= HCR_FWB;
 
-	if (cpus_have_final_cap(ARM64_HAS_EVT) &&
-	    !cpus_have_final_cap(ARM64_MISMATCHED_CACHE_TYPE) &&
-	    kvm_read_vm_id_reg(vcpu->kvm, SYS_CTR_EL0) == read_cpuid(CTR_EL0))
-		vcpu->arch.hcr_el2 |= HCR_TID4;
-	else
-		vcpu->arch.hcr_el2 |= HCR_TID2;
-
 	/*
 	 * Without AArch32 EL1, leave RW set and let the entry fail with an
 	 * illegal exception return: the *32_EL2 registers EL2 would otherwise
@@ -65,9 +59,6 @@ static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu)
 
 	if (vcpu_has_ptrauth(vcpu))
 		vcpu->arch.hcr_el2 |= (HCR_API | HCR_APK);
-
-	if (kvm_has_mte(vcpu->kvm))
-		vcpu->arch.hcr_el2 |= HCR_ATA;
 }
 
 static void pvm_init_traps_hcr(struct kvm_vcpu *vcpu)
@@ -85,6 +76,13 @@ static void pvm_init_traps_hcr(struct kvm_vcpu *vcpu)
 	 */
 	val |= HCR_TACR | HCR_TIDCP | HCR_TID3 | HCR_TID1;
 
+	if (cpus_have_final_cap(ARM64_HAS_EVT) &&
+	    !cpus_have_final_cap(ARM64_MISMATCHED_CACHE_TYPE) &&
+	    kvm_read_vm_id_reg(kvm, SYS_CTR_EL0) == read_cpuid(CTR_EL0))
+		val |= HCR_EL2_TID4;
+	else
+		val |= HCR_EL2_TID2;
+
 	if (!kvm_has_feat(kvm, ID_AA64PFR0_EL1, RAS, IMP)) {
 		val |= HCR_TERR | HCR_TEA;
 		val &= ~(HCR_FIEN);
-- 
2.39.5


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF
  2026-09-28  6:46 [PATCH v2 0/4] KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
                   ` (2 preceding siblings ...)
  2026-09-28  6:46 ` [PATCH v2 3/4] KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
@ 2026-09-28  6:46 ` Fuad Tabba
  2026-09-28  9:20   ` Venkata Rao Kakani
  3 siblings, 1 reply; 9+ messages in thread
From: Fuad Tabba @ 2026-09-28  6:46 UTC (permalink / raw)
  To: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel
  Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Steffen Eiden,
	Catalin Marinas, Will Deacon, Mark Rutland, Quentin Perret,
	Vincent Donnefort, Wei-Lin Chang, Fuad Tabba, linux-kernel

Userspace can hide a feature from a guest by clearing its field in a
writable ID register, and KVM then makes the feature's instructions
UNDEFINED in the guest by trapping or disabling them. No selftest checks
that.

Add a test that runs the instruction of each of TLBI OS, MOPS, TCR2_EL1
and FPMR once with its field as advertised and once with it cleared, and
expects an UNDEF only when cleared. A feature the vCPU doesn't advertise
is skipped, as is hidden TLBI OS on a CPU with neither FGT nor
FEAT_EVT2, where KVM can't trap it.

Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
---
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 .../selftests/kvm/arm64/hidden_features.c     | 184 ++++++++++++++++++
 2 files changed, 185 insertions(+)
 create mode 100644 tools/testing/selftests/kvm/arm64/hidden_features.c

diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
index 96bab7002d39e..864fdca7f362e 100644
--- a/tools/testing/selftests/kvm/Makefile.kvm
+++ b/tools/testing/selftests/kvm/Makefile.kvm
@@ -194,6 +194,7 @@ TEST_GEN_PROGS_arm64 += arm64/vgic_v5
 TEST_GEN_PROGS_arm64 += arm64/vpmu_counter_access
 TEST_GEN_PROGS_arm64 += arm64/no-vgic
 TEST_GEN_PROGS_arm64 += arm64/idreg-idst
+TEST_GEN_PROGS_arm64 += arm64/hidden_features
 TEST_GEN_PROGS_arm64 += arm64/kvm-uuid
 TEST_GEN_PROGS_arm64 += access_tracking_perf_test
 TEST_GEN_PROGS_arm64 += arch_timer
diff --git a/tools/testing/selftests/kvm/arm64/hidden_features.c b/tools/testing/selftests/kvm/arm64/hidden_features.c
new file mode 100644
index 0000000000000..194d746e7605e
--- /dev/null
+++ b/tools/testing/selftests/kvm/arm64/hidden_features.c
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * hidden_features - Check that a feature's instruction runs in the guest when
+ * its ID register field is advertised, and is UNDEFINED when userspace clears
+ * the field.
+ *
+ * Copyright (c) 2026 Google LLC
+ * Author: Fuad Tabba <fuad.tabba@linux.dev>
+ */
+#include "kvm_util.h"
+#include "processor.h"
+#include "test_util.h"
+
+static volatile bool undef;
+
+static void guest_tlbi_os(void)
+{
+	/* tlbi vmalle1os */
+	asm volatile("sys #0, c8, c1, #0\n\tdsb ish\n\tisb" ::: "memory");
+}
+
+static void guest_mops(void)
+{
+	register u64 *d asm("x0");
+	register u64 n asm("x1");
+	register u64 s asm("x2");
+	u64 buf[8];
+
+	d = buf;
+	n = sizeof(buf);
+	s = 0;
+	/* setp [x0]!, x1!, x2; setm; sete */
+	asm volatile(".inst 0x19c20420\n\t.inst 0x19c24420\n\t.inst 0x19c28420"
+		     : "+r"(d), "+r"(n) : "r"(s) : "cc", "memory");
+}
+
+static void guest_tcr2(void)
+{
+	read_sysreg_s(SYS_TCR2_EL1);
+}
+
+static void guest_fpmr(void)
+{
+	read_sysreg_s(SYS_FPMR);
+}
+
+struct feature {
+	const char *name;
+	u64 id_reg;
+	u64 mask;
+	u8 shift;
+	u64 min;
+	void (*insn)(void);
+	bool (*trappable)(struct kvm_vcpu *vcpu);
+};
+
+/* Without FGT, KVM traps a hidden TLBI OS only through HCR_EL2.TTLBOS (FEAT_EVT2). */
+static bool tlbi_os_trappable(struct kvm_vcpu *vcpu)
+{
+	u64 mmfr0 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR0_EL1));
+	u64 mmfr2 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR2_EL1));
+
+	return SYS_FIELD_GET(ID_AA64MMFR0_EL1, FGT, mmfr0) >= ID_AA64MMFR0_EL1_FGT_IMP ||
+	       SYS_FIELD_GET(ID_AA64MMFR2_EL1, EVT, mmfr2) >= ID_AA64MMFR2_EL1_EVT_TTLBxS;
+}
+
+#define FEATURE(n, reg, field, min_val, fn, trap)		\
+{								\
+	.name		= n,					\
+	.id_reg		= SYS_##reg,				\
+	.mask		= reg##_##field##_MASK,			\
+	.shift		= reg##_##field##_SHIFT,		\
+	.min		= reg##_##field##_##min_val,		\
+	.insn		= fn,					\
+	.trappable	= trap,					\
+}
+
+static const struct feature features[] = {
+	FEATURE("TLBI OS", ID_AA64ISAR0_EL1, TLB, OS, guest_tlbi_os, tlbi_os_trappable),
+	FEATURE("MOPS", ID_AA64ISAR2_EL1, MOPS, IMP, guest_mops, NULL),
+	FEATURE("TCR2_EL1", ID_AA64MMFR3_EL1, TCRX, IMP, guest_tcr2, NULL),
+	FEATURE("FPMR", ID_AA64PFR2_EL1, FPMR, IMP, guest_fpmr, NULL),
+};
+
+static void guest_code(const struct feature *feat)
+{
+	undef = false;
+	feat->insn();
+	GUEST_SYNC(undef);
+	GUEST_DONE();
+}
+
+static void guest_undef_handler(struct ex_regs *regs)
+{
+	undef = true;
+	regs->pc += 4;
+}
+
+static bool run(const struct feature *feat, bool hide)
+{
+	struct kvm_vcpu *vcpu;
+	struct kvm_vm *vm;
+	struct ucall uc;
+	bool got = false;
+	u64 val;
+
+	vm = vm_create_with_one_vcpu(&vcpu, (void *)guest_code);
+	vm_init_descriptor_tables(vm);
+	vcpu_init_descriptor_tables(vcpu);
+	vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ESR_ELx_EC_UNKNOWN, guest_undef_handler);
+	vcpu_args_set(vcpu, 1, feat);
+
+	if (hide) {
+		val = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg));
+		vcpu_set_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg), val & ~feat->mask);
+	}
+
+	for (;;) {
+		vcpu_run(vcpu);
+		switch (get_ucall(vcpu, &uc)) {
+		case UCALL_SYNC:
+			got = uc.args[1];
+			break;
+		case UCALL_ABORT:
+			REPORT_GUEST_ASSERT(uc);
+			break;
+		case UCALL_DONE:
+			kvm_vm_free(vm);
+			return got;
+		default:
+			TEST_FAIL("Unknown ucall %lu", uc.cmd);
+		}
+	}
+}
+
+static void probe_feature(const struct feature *feat, bool *present, bool *trappable)
+{
+	struct kvm_vcpu *vcpu;
+	struct kvm_vm *vm;
+	u64 val;
+
+	vm = vm_create_with_one_vcpu(&vcpu, NULL);
+	val = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg));
+	*present = ((val & feat->mask) >> feat->shift) >= feat->min;
+	*trappable = !feat->trappable || feat->trappable(vcpu);
+	kvm_vm_free(vm);
+}
+
+int main(void)
+{
+	const struct feature *feat;
+	bool present, trappable;
+	int i;
+
+	test_disable_default_vgic();
+
+	ksft_print_header();
+	ksft_set_plan(ARRAY_SIZE(features) * 2);
+
+	for (i = 0; i < ARRAY_SIZE(features); i++) {
+		feat = &features[i];
+
+		probe_feature(feat, &present, &trappable);
+		if (!present) {
+			ksft_test_result_skip("%s advertised, not supported\n", feat->name);
+			ksft_test_result_skip("%s hidden, not supported\n", feat->name);
+			continue;
+		}
+
+		if (run(feat, false))
+			ksft_test_result_fail("%s advertised, UNDEF\n", feat->name);
+		else
+			ksft_test_result_pass("%s advertised\n", feat->name);
+
+		if (!trappable)
+			ksft_test_result_skip("%s hidden, not trappable\n", feat->name);
+		else if (run(feat, true))
+			ksft_test_result_pass("%s hidden\n", feat->name);
+		else
+			ksft_test_result_fail("%s hidden, no UNDEF\n", feat->name);
+	}
+
+	ksft_finished();
+}
-- 
2.39.5


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF
  2026-09-28  6:46 ` [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF Fuad Tabba
@ 2026-09-28  9:20   ` Venkata Rao Kakani
  2026-09-28  9:28     ` Fuad Tabba
  0 siblings, 1 reply; 9+ messages in thread
From: Venkata Rao Kakani @ 2026-09-28  9:20 UTC (permalink / raw)
  To: Fuad Tabba, Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel
  Cc: Joey Gouly, Suzuki K Poulose, Zenghui Yu, Steffen Eiden,
	Catalin Marinas, Will Deacon, Mark Rutland, Quentin Perret,
	Vincent Donnefort, Wei-Lin Chang, Fuad Tabba, linux-kernel


On 28-09-2026 12:16 pm, Fuad Tabba wrote:
> Userspace can hide a feature from a guest by clearing its field in a
> writable ID register, and KVM then makes the feature's instructions
> UNDEFINED in the guest by trapping or disabling them. No selftest checks
> that.
>
> Add a test that runs the instruction of each of TLBI OS, MOPS, TCR2_EL1
> and FPMR once with its field as advertised and once with it cleared, and
> expects an UNDEF only when cleared. A feature the vCPU doesn't advertise
> is skipped, as is hidden TLBI OS on a CPU with neither FGT nor
> FEAT_EVT2, where KVM can't trap it.
>
> Signed-off-by: Fuad Tabba <fuad.tabba@linux.dev>
> ---
>   tools/testing/selftests/kvm/Makefile.kvm      |   1 +
>   .../selftests/kvm/arm64/hidden_features.c     | 184 ++++++++++++++++++
>   2 files changed, 185 insertions(+)
>   create mode 100644 tools/testing/selftests/kvm/arm64/hidden_features.c
>
> diff --git a/tools/testing/selftests/kvm/Makefile.kvm b/tools/testing/selftests/kvm/Makefile.kvm
> index 96bab7002d39e..864fdca7f362e 100644
> --- a/tools/testing/selftests/kvm/Makefile.kvm
> +++ b/tools/testing/selftests/kvm/Makefile.kvm
> @@ -194,6 +194,7 @@ TEST_GEN_PROGS_arm64 += arm64/vgic_v5
>   TEST_GEN_PROGS_arm64 += arm64/vpmu_counter_access
>   TEST_GEN_PROGS_arm64 += arm64/no-vgic
>   TEST_GEN_PROGS_arm64 += arm64/idreg-idst
> +TEST_GEN_PROGS_arm64 += arm64/hidden_features
>   TEST_GEN_PROGS_arm64 += arm64/kvm-uuid
>   TEST_GEN_PROGS_arm64 += access_tracking_perf_test
>   TEST_GEN_PROGS_arm64 += arch_timer
> diff --git a/tools/testing/selftests/kvm/arm64/hidden_features.c b/tools/testing/selftests/kvm/arm64/hidden_features.c
> new file mode 100644
> index 0000000000000..194d746e7605e
> --- /dev/null
> +++ b/tools/testing/selftests/kvm/arm64/hidden_features.c
> @@ -0,0 +1,184 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * hidden_features - Check that a feature's instruction runs in the guest when
> + * its ID register field is advertised, and is UNDEFINED when userspace clears
> + * the field.
> + *
> + * Copyright (c) 2026 Google LLC
> + * Author: Fuad Tabba <fuad.tabba@linux.dev>
> + */
> +#include "kvm_util.h"
> +#include "processor.h"
> +#include "test_util.h"
> +
> +static volatile bool undef;
> +
> +static void guest_tlbi_os(void)
> +{
> +	/* tlbi vmalle1os */
> +	asm volatile("sys #0, c8, c1, #0\n\tdsb ish\n\tisb" ::: "memory");
> +}
> +
> +static void guest_mops(void)
> +{
> +	register u64 *d asm("x0");
> +	register u64 n asm("x1");
> +	register u64 s asm("x2");
> +	u64 buf[8];
> +
> +	d = buf;
> +	n = sizeof(buf);
> +	s = 0;
> +	/* setp [x0]!, x1!, x2; setm; sete */
> +	asm volatile(".inst 0x19c20420\n\t.inst 0x19c24420\n\t.inst 0x19c28420"
> +		     : "+r"(d), "+r"(n) : "r"(s) : "cc", "memory");
> +}
> +
> +static void guest_tcr2(void)
> +{
> +	read_sysreg_s(SYS_TCR2_EL1);
> +}
> +
> +static void guest_fpmr(void)
> +{
> +	read_sysreg_s(SYS_FPMR);
> +}
> +
> +struct feature {
> +	const char *name;
> +	u64 id_reg;
> +	u64 mask;
> +	u8 shift;
> +	u64 min;
> +	void (*insn)(void);
> +	bool (*trappable)(struct kvm_vcpu *vcpu);
> +};
> +
> +/* Without FGT, KVM traps a hidden TLBI OS only through HCR_EL2.TTLBOS (FEAT_EVT2). */
> +static bool tlbi_os_trappable(struct kvm_vcpu *vcpu)
> +{
> +	u64 mmfr0 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR0_EL1));
> +	u64 mmfr2 = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(SYS_ID_AA64MMFR2_EL1));
> +
> +	return SYS_FIELD_GET(ID_AA64MMFR0_EL1, FGT, mmfr0) >= ID_AA64MMFR0_EL1_FGT_IMP ||
> +	       SYS_FIELD_GET(ID_AA64MMFR2_EL1, EVT, mmfr2) >= ID_AA64MMFR2_EL1_EVT_TTLBxS;
> +}
> +
> +#define FEATURE(n, reg, field, min_val, fn, trap)		\
> +{								\
> +	.name		= n,					\
> +	.id_reg		= SYS_##reg,				\
> +	.mask		= reg##_##field##_MASK,			\
> +	.shift		= reg##_##field##_SHIFT,		\
> +	.min		= reg##_##field##_##min_val,		\
> +	.insn		= fn,					\
> +	.trappable	= trap,					\
> +}
> +
> +static const struct feature features[] = {
> +	FEATURE("TLBI OS", ID_AA64ISAR0_EL1, TLB, OS, guest_tlbi_os, tlbi_os_trappable),
> +	FEATURE("MOPS", ID_AA64ISAR2_EL1, MOPS, IMP, guest_mops, NULL),
> +	FEATURE("TCR2_EL1", ID_AA64MMFR3_EL1, TCRX, IMP, guest_tcr2, NULL),
> +	FEATURE("FPMR", ID_AA64PFR2_EL1, FPMR, IMP, guest_fpmr, NULL),
> +};
> +
> +static void guest_code(const struct feature *feat)
> +{
> +	undef = false;
> +	feat->insn();
> +	GUEST_SYNC(undef);
> +	GUEST_DONE();
> +}
> +
> +static void guest_undef_handler(struct ex_regs *regs)
> +{
> +	undef = true;
> +	regs->pc += 4;
> +}
> +
> +static bool run(const struct feature *feat, bool hide)
> +{
> +	struct kvm_vcpu *vcpu;
> +	struct kvm_vm *vm;
> +	struct ucall uc;
> +	bool got = false;
> +	u64 val;
> +
> +	vm = vm_create_with_one_vcpu(&vcpu, (void *)guest_code);
> +	vm_init_descriptor_tables(vm);
> +	vcpu_init_descriptor_tables(vcpu);
> +	vm_install_sync_handler(vm, VECTOR_SYNC_CURRENT, ESR_ELx_EC_UNKNOWN, guest_undef_handler);
> +	vcpu_args_set(vcpu, 1, feat);
> +
> +	if (hide) {
> +		val = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg));
> +		vcpu_set_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg), val & ~feat->mask);
> +	}
> +
> +	for (;;) {
> +		vcpu_run(vcpu);
> +		switch (get_ucall(vcpu, &uc)) {
> +		case UCALL_SYNC:
> +			got = uc.args[1];
> +			break;
> +		case UCALL_ABORT:
> +			REPORT_GUEST_ASSERT(uc);
> +			break;
> +		case UCALL_DONE:
> +			kvm_vm_free(vm);
> +			return got;
> +		default:
> +			TEST_FAIL("Unknown ucall %lu", uc.cmd);
> +		}
> +	}
     Return missing?
> +}
> +
> +static void probe_feature(const struct feature *feat, bool *present, bool *trappable)
> +{
> +	struct kvm_vcpu *vcpu;
> +	struct kvm_vm *vm;
> +	u64 val;
> +
> +	vm = vm_create_with_one_vcpu(&vcpu, NULL);
> +	val = vcpu_get_reg(vcpu, KVM_ARM64_SYS_REG(feat->id_reg));
> +	*present = ((val & feat->mask) >> feat->shift) >= feat->min;
> +	*trappable = !feat->trappable || feat->trappable(vcpu);
> +	kvm_vm_free(vm);
> +}
> +
> +int main(void)
> +{
> +	const struct feature *feat;
> +	bool present, trappable;
> +	int i;
> +
> +	test_disable_default_vgic();
> +
> +	ksft_print_header();
> +	ksft_set_plan(ARRAY_SIZE(features) * 2);
> +
> +	for (i = 0; i < ARRAY_SIZE(features); i++) {
> +		feat = &features[i];
> +
> +		probe_feature(feat, &present, &trappable);
> +		if (!present) {
> +			ksft_test_result_skip("%s advertised, not supported\n", feat->name);
> +			ksft_test_result_skip("%s hidden, not supported\n", feat->name);
> +			continue;
> +		}
> +
> +		if (run(feat, false))
> +			ksft_test_result_fail("%s advertised, UNDEF\n", feat->name);
> +		else
> +			ksft_test_result_pass("%s advertised\n", feat->name);
> +
> +		if (!trappable)
> +			ksft_test_result_skip("%s hidden, not trappable\n", feat->name);
> +		else if (run(feat, true))
> +			ksft_test_result_pass("%s hidden\n", feat->name);
> +		else
> +			ksft_test_result_fail("%s hidden, no UNDEF\n", feat->name);
> +	}
> +
> +	ksft_finished();
> +}

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF
  2026-09-28  9:20   ` Venkata Rao Kakani
@ 2026-09-28  9:28     ` Fuad Tabba
  0 siblings, 0 replies; 9+ messages in thread
From: Fuad Tabba @ 2026-09-28  9:28 UTC (permalink / raw)
  To: Venkata Rao Kakani
  Cc: Marc Zyngier, Oliver Upton, kvmarm, linux-arm-kernel, Joey Gouly,
	Suzuki K Poulose, Zenghui Yu, Steffen Eiden, Catalin Marinas,
	Will Deacon, Mark Rutland, Quentin Perret, Vincent Donnefort,
	Wei-Lin Chang, linux-kernel

Hi Venkata,

On Mon, 28 Sep 2026 at 10:20, Venkata Rao Kakani
<venkata.kakani@oss.qualcomm.com> wrote:
> > +     for (;;) {
> > +             vcpu_run(vcpu);
> > +             switch (get_ucall(vcpu, &uc)) {
> > +             case UCALL_SYNC:
> > +                     got = uc.args[1];
> > +                     break;
> > +             case UCALL_ABORT:
> > +                     REPORT_GUEST_ASSERT(uc);
> > +                     break;
> > +             case UCALL_DONE:
> > +                     kvm_vm_free(vm);
> > +                     return got;
> > +             default:
> > +                     TEST_FAIL("Unknown ucall %lu", uc.cmd);
> > +             }
> > +     }
>      Return missing?

No, the loop never falls through: the breaks only leave the switch,
UCALL_DONE is the one way out and returns there, and the ABORT and
default cases end the test. So control can't reach the end of the
function.

Thanks for having a look,
/fuad

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1
  2026-09-28  6:46 ` [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1 Fuad Tabba
@ 2026-09-28 17:05   ` Oliver Upton
  2026-09-28 19:17     ` Fuad Tabba
  0 siblings, 1 reply; 9+ messages in thread
From: Oliver Upton @ 2026-09-28 17:05 UTC (permalink / raw)
  To: Fuad Tabba
  Cc: Marc Zyngier, kvmarm, linux-arm-kernel, Joey Gouly,
	Suzuki K Poulose, Zenghui Yu, Steffen Eiden, Catalin Marinas,
	Will Deacon, Mark Rutland, Quentin Perret, Vincent Donnefort,
	Wei-Lin Chang, Fuad Tabba, linux-kernel

Hi Fuad,

On Mon, Sep 28, 2026 at 07:46:40AM +0100, Fuad Tabba wrote:
> KVM hides TLBI OS from a guest whose ID registers don't advertise it by
> trapping the instructions: through the fine-grained traps on a CPU with
> FGT, and through HCR_EL2.TTLBOS on one with FEAT_EVT2. With FGT,
> triage_sysreg_trap() makes a trapped TLBI OS UNDEFINED. Without it, the
> instruction reaches handle_tlbi_el1(), which assumes an EL1 TLBI only
> traps from a guest at vEL2 and WARNs before checking whether the guest
> supports it. The guest still gets its UNDEF after the WARN. A VMM can
> trigger the WARN by hiding TLBI OS and having the guest execute one.

So the FGUs don't actually require any hardware support, they're just a
representation of our UNDEF emulation that can be applied even on
non-FGT. That's actually the way we had them wired before the whole
config.c rewrite.

Can you look into reinstating the FGUs?

Thanks,
Oliver

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1
  2026-09-28 17:05   ` Oliver Upton
@ 2026-09-28 19:17     ` Fuad Tabba
  0 siblings, 0 replies; 9+ messages in thread
From: Fuad Tabba @ 2026-09-28 19:17 UTC (permalink / raw)
  To: Oliver Upton
  Cc: Marc Zyngier, kvmarm, linux-arm-kernel, Joey Gouly,
	Suzuki K Poulose, Zenghui Yu, Steffen Eiden, Catalin Marinas,
	Will Deacon, Mark Rutland, Quentin Perret, Vincent Donnefort,
	Wei-Lin Chang, linux-kernel

Hi Oliver,

On Mon, 28 Sep 2026 18:05:09 +0100, Oliver Upton <oupton@kernel.org> wrote:
[...]
> So the FGUs don't actually require any hardware support, they're just a
> representation of our UNDEF emulation that can be applied even on
> non-FGT. That's actually the way we had them wired before the whole
> config.c rewrite.
>
> Can you look into reinstating the FGUs?

Will do. The FGT entries never make it into the xarray without
ARM64_HAS_FGT, so the FGU check in triage_sysreg_trap() has nothing to
match on those CPUs. v3 drops that gate and leaves handle_tlbi_el1()
alone.

AFAICT that skip has been there since the FGT forwarding
infrastructure went in, before the FGUs existed, so I'll write it up
as making the FGUs work on non-FGT hosts.

Cheers,
/fuad

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-09-28 19:17 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-28  6:46 [PATCH v2 0/4] KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
2026-09-28  6:46 ` [PATCH v2 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1 Fuad Tabba
2026-09-28 17:05   ` Oliver Upton
2026-09-28 19:17     ` Fuad Tabba
2026-09-28  6:46 ` [PATCH v2 2/4] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs Fuad Tabba
2026-09-28  6:46 ` [PATCH v2 3/4] KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM Fuad Tabba
2026-09-28  6:46 ` [PATCH v2 4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF Fuad Tabba
2026-09-28  9:20   ` Venkata Rao Kakani
2026-09-28  9:28     ` Fuad Tabba

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®