Skip to contentSkip to conversation

KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM

PATCH Applied Fuad Tabba 3 participants 8 messages Sep 29

Subsystems (4)
lore
Share & tools
lore

Fuad Tabba

author reviewer
#0 lore raw

v3 cover KVM: arm64: Fix HCR_EL2 for non-protected VMs in pKVM

Hi folks,

Changes since v2 [1]:
- Patch 1: apply the FGUs on a CPU without FEAT_FGT instead of moving the check in handle_tlbi_el1() (Oliver [2]). Wei-Lin's Reviewed-by dropped with the rewrite.

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 on a CPU without FEAT_FGT hits a WARN in handle_tlbi_el1(), as it does without pKVM. The first makes the FGUs apply on such a CPU too, so the access is UNDEFINED before it reaches handle_tlbi_el1().

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 [3].

Based on Linux 7.3-rc4 (93f51579e7df2).

Cheers,
/fuad

[1] https://lore.kernel.org/all/[email protected]/
[2] https://lore.kernel.org/all/[email protected]/
[3] https://lore.kernel.org/all/[email protected]/

Fuad Tabba (4):
KVM: arm64: Apply the fine-grained UNDEFs without FEAT_FGT
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/emulate-nested.c               |   3 -
 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 ++-
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 .../selftests/kvm/arm64/hidden_features.c     | 184 ++++++++++++++++++
 6 files changed, 214 insertions(+), 15 deletions(-)
 create mode 100644 tools/testing/selftests/kvm/arm64/hidden_features.c

base-commit: 93f51579e7df248780214094418f205253383cc5

Signature · 1 line
2.39.5
author reviewer
#1 lore raw

PATCH v3 1/4 KVM: arm64: Apply the fine-grained UNDEFs without FEAT_FGT

triage_sysreg_trap() applies the FGU bits through the encoding's FGT entry in the trap xarray, and populate_nv_trap_config() only adds those entries on a CPU with FEAT_FGT. Without it, a hidden feature that still traps reaches its handler instead: a TLBI OS trapped through HCR_EL2.TTLBOS lands in handle_tlbi_el1(), which expects an EL1 TLBI only from vEL2 and WARNs before the guest gets its UNDEF. A VMM can trigger the WARN by hiding TLBI OS and having the guest execute one.

kvm_calculate_traps() computes the FGU bits with or without FEAT_FGT, and the entries map an encoding to its FGT bit whatever the CPU implements. Add them unconditionally, so that a hidden feature that traps is UNDEFINED on any CPU.

Fixes f5a5a406b4b8b ("KVM: arm64: Propagate and handle Fine-Grained UNDEF bits")

---

Notes:
For a nested guest on a CPU without FGT, check_fgt_bit() now runs too, but it forwards nothing: a guest without FEAT_FGT has its FGT registers RES0.

f5a5a406b4b8b added the FGU check to triage_sysreg_trap() behind an ARM64_HAS_FGT gate that populate_nv_trap_config() already had, so the check never fired without FGT.

 arch/arm64/kvm/emulate-nested.c | 3 ---
 1 file changed, 3 deletions(-)
arch/arm64/kvm/emulate-nested.c +0 -3
index 625604019fb32..b8529532b6e78 100644
@@ -2386,9 +2386,6 @@ int __init populate_nv_trap_config(void)
 				print_nv_trap_error(fgt, "FGT bit is reserved", ret);
 			}
 
-			if (!cpus_have_final_cap(ARM64_HAS_FGT))
-				continue;
-
 			prev = xa_store(&sr_forward_xa, enc,
 					xa_mk_value(tc.val), GFP_KERNEL);
 
Signature · 1 line
2.39.5
Reviewed-by on v3 1/4
#2 lore raw

Re: [PATCH v3 1/4] KVM: arm64: Apply the fine-grained UNDEFs without FEAT_FGT

Quoted 8 lines · Fuad Tabba

triage_sysreg_trap() applies the FGU bits through the encoding's FGT entry in the trap xarray, and populate_nv_trap_config() only adds those entries on a CPU with FEAT_FGT. Without it, a hidden feature that still traps reaches its handler instead: a TLBI OS trapped through HCR_EL2.TTLBOS lands in handle_tlbi_el1(), which expects an EL1 TLBI only from vEL2 and WARNs before the guest gets its UNDEF. A VMM can trigger the WARN by hiding TLBI OS and having the guest execute one.

Quoted 4 lines

kvm_calculate_traps() computes the FGU bits with or without FEAT_FGT, and the entries map an encoding to its FGT bit whatever the CPU implements. Add them unconditionally, so that a hidden feature that traps is UNDEFINED on any CPU.

This reads a bit mechanical to me, can I suggest:

```
FGUs don't actually require hardware support, so don't gate fgt trap
config insertion on FGT. This is the only action required to allow FGUs
always, as kvm_calculate_traps() already computes the FGU bits with or
without FEAT_FGT.

This also fixes a spurious WARN when a TLBI OS is run on a guest in vEL1
without FEAT_TLBIOS on non FEAT_FGT hardware. In this configuration the
course-grained HCR_EL2.TTLBOS trap reaches the handler handle_tlbi_el1(),
which expects an EL1 TLBI only from vEL2. With FGUs enabled the UNDEF
will be injected without needing the specific handler.
```

Other than that, this is very nice! Sorry for missing this. It's way better to have FGU regardless of FGT and handle unexposed TLBIOS with FGU always. Thanks Oliver for suggesting this instead, and thanks Fuad for digging in.

Thanks,
Wei-Lin Chang

Quoted 36 lines
Fixes f5a5a406b4b8b ("KVM: arm64: Propagate and handle Fine-Grained UNDEF bits")

---

Notes:
For a nested guest on a CPU without FGT, check_fgt_bit() now runs too, but it forwards nothing: a guest without FEAT_FGT has its FGT registers RES0.

f5a5a406b4b8b added the FGU check to triage_sysreg_trap() behind an ARM64_HAS_FGT gate that populate_nv_trap_config() already had, so the check never fired without FGT.

 arch/arm64/kvm/emulate-nested.c | 3 ---
 1 file changed, 3 deletions(-)
arch/arm64/kvm/emulate-nested.c +0 -3
index 625604019fb32..b8529532b6e78 100644
@@ -2386,9 +2386,6 @@ int __init populate_nv_trap_config(void)
 				print_nv_trap_error(fgt, "FGT bit is reserved", ret);
 			}
 
-			if (!cpus_have_final_cap(ARM64_HAS_FGT))
-				continue;
-
 			prev = xa_store(&sr_forward_xa, enc,
 					xa_mk_value(tc.val), GFP_KERNEL);
 
Signature · 1 line
2.39.5
author reviewer
#3 lore raw

Re: [PATCH v3 1/4] KVM: arm64: Apply the fine-grained UNDEFs without FEAT_FGT

On Tue, 29 Sept 2026 at 15:54, Wei-Lin Chang <[email protected]> wrote:
[...]

Quoted 14 lines

This reads a bit mechanical to me, can I suggest:

```
FGUs don't actually require hardware support, so don't gate fgt trap
config insertion on FGT. This is the only action required to allow FGUs
always, as kvm_calculate_traps() already computes the FGU bits with or
without FEAT_FGT.

This also fixes a spurious WARN when a TLBI OS is run on a guest in vEL1
without FEAT_TLBIOS on non FEAT_FGT hardware. In this configuration the
course-grained HCR_EL2.TTLBOS trap reaches the handler handle_tlbi_el1(),
which expects an EL1 TLBI only from vEL2. With FGUs enabled the UNDEF
will be injected without needing the specific handler.
```

I cannot write commit messages! :) Thanks for the suggestion and the review. I'll reword if I respin.

Cheers,
/fuad

author reviewer
#4 lore raw

PATCH v3 2/4 KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs

In reply to Fuad Tabba

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")
---
 arch/arm64/kvm/hyp/nvhe/pkvm.c | 9 +++++++++
 1 file changed, 9 insertions(+)
arch/arm64/kvm/hyp/nvhe/pkvm.c +9 -0
index 459bd9eb7e4bc..62d432144d44c 100644
@@ -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);
 
Signature · 1 line
2.39.5
author reviewer
#5 lore raw

PATCH v3 3/4 KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM

In reply to Fuad Tabba

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")
---
 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(-)
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +9 -0
index c904647d2f760..5c050f21066ab 100644
@@ -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.
  */
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4 -3
index 9a3b92e626adb..ac64a036b0a95 100644
@@ -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;
 
arch/arm64/kvm/hyp/nvhe/pkvm.c +8 -10
index 62d432144d44c..7ef09867f2802 100644
@@ -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);
Signature · 1 line
2.39.5
author reviewer
#6 lore raw

PATCH v3 4/4 KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF

In reply to Fuad Tabba

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.

---
 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
tools/testing/selftests/kvm/Makefile.kvm +1 -0
index 96bab7002d39e..864fdca7f362e 100644
@@ -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
tools/testing/selftests/kvm/arm64/hidden_features.c new file +184 -0

184 lines not on this page · raw message

Signature · 1 line
2.39.5
maintainer Applied
#7 lore raw
In reply to Fuad Tabba
Quoted 5 lines · Fuad Tabba

Changes since v2 [1]:
- Patch 1: apply the FGUs on a CPU without FEAT_FGT instead of moving the check in handle_tlbi_el1() (Oliver [2]). Wei-Lin's Reviewed-by dropped with the rewrite.

Quoted 7 lines

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.

[...]

Applied to fixes, thanks!

[1/4] KVM: arm64: Apply the fine-grained UNDEFs without FEAT_FGT

      https://git.kernel.org/kvmarm/kvmarm/c/4bdd2b3a708c
[2/4] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs
      https://git.kernel.org/kvmarm/kvmarm/c/80ae56184b57
[3/4] KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM
      https://git.kernel.org/kvmarm/kvmarm/c/04447b95c62b
[4/4] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF
      https://git.kernel.org/kvmarm/kvmarm/c/afb4334fb52c

--
Best,
Oliver