Skip to contentSkip to conversation

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

PATCH Superseded Fuad Tabba 3 participants 10 messages Sep 25 – Sep 28

Subsystems (4)

v1 of 3 Superseded Latest is v3 →

lore
Share & tools
lore

Fuad Tabba

author reviewer
#0 lore raw

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

Hi folks,

In pKVM, EL2 sets a non-protected VM's HCR_EL2 in pkvm_vcpu_reset_hcr(), which covers only part 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.

Rather than add each missing bit at EL2, the third patch uses the host's HCR_EL2 on every entry, except for the bits EL2 owns. A bit added to vcpu_set_hcr() then reaches pKVM with no change at EL2, unless it opens state EL2 doesn't switch, in which case it goes in PKVM_HCR_EL2_OWNED, as ATA and DCT do.

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

Based on Linux 7.3-rc4 (93f51579e7df2).

Cheers,
/fuad

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

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/include/asm/kvm_arm.h              |   1 +
 arch/arm64/kvm/hyp/include/nvhe/pkvm.h        |  13 ++
 arch/arm64/kvm/hyp/nvhe/hyp-main.c            |   7 +-
 arch/arm64/kvm/hyp/nvhe/pkvm.c                |  24 ++-
 arch/arm64/kvm/sys_regs.c                     |   7 +-
 tools/testing/selftests/kvm/Makefile.kvm      |   1 +
 .../selftests/kvm/arm64/hidden_features.c     | 184 ++++++++++++++++++
 7 files changed, 222 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 1/4 KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1

A VMM can trigger the WARN_ON(!vcpu_is_el2()) in handle_tlbi_el1() on a CPU with FEAT_EVT2 and FEAT_TLBIOS but no FEAT_FGT, by setting ID_AA64ISAR0_EL1.TLB to NI and having the guest execute TLBI VMALLE1OS.

Without FGT, HCR_EL2.TTLBOS traps the instruction. KVM records no FGT trap information on such a host, so triage_sysreg_trap() doesn't UNDEF it. The instruction reaches handle_tlbi_el1(), which warns before checking whether the guest supports it.

Check support before the warning.

Fixes 0cb8aae226768 ("KVM: arm64: nv: Add handling of outer-shareable TLBI operations")
---
 arch/arm64/kvm/sys_regs.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
arch/arm64/kvm/sys_regs.c +4 -3
index 44aae52c473d7..0ce29ce678b08 100644
@@ -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;
Signature · 1 line
2.39.5
Reviewed-by on v1 1/4
#2 lore raw

Re: [PATCH v1 1/4] KVM: arm64: Don't WARN on an unsupported TLBI OS from vEL1

Quoted 9 lines · Fuad Tabba

A VMM can trigger the WARN_ON(!vcpu_is_el2()) in handle_tlbi_el1() on a CPU with FEAT_EVT2 and FEAT_TLBIOS but no FEAT_FGT, by setting ID_AA64ISAR0_EL1.TLB to NI and having the guest execute TLBI VMALLE1OS.

Without FGT, HCR_EL2.TTLBOS traps the instruction. KVM records no FGT trap information on such a host, so triage_sysreg_trap() doesn't UNDEF it. The instruction reaches handle_tlbi_el1(), which warns before checking whether the guest supports it.

Quoted 5 lines

Check support before the warning.

Fixes 0cb8aae226768 ("KVM: arm64: nv: Add handling of outer-shareable TLBI operations")

Looks correct to me:

Quoted 32 lines
---
 arch/arm64/kvm/sys_regs.c | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)
arch/arm64/kvm/sys_regs.c +4 -3
index 44aae52c473d7..0ce29ce678b08 100644
@@ -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;
Signature · 1 line
2.39.5
author reviewer
#3 lore raw

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

In reply to Fuad Tabba

In pKVM, KVM_RUN on a vCPU created with KVM_ARM_VCPU_EL1_32BIT fails with KVM_EXIT_FAIL_ENTRY. pkvm_vcpu_reset_hcr(), pKVM's EL2 counterpart of vcpu_set_hcr(), never clears HCR_EL2.RW, so the first ERET into the vCPU is an illegal exception return. Protected VMs are AArch64-only, so only non-protected VMs are affected.

Clear RW for 32-bit vCPUs. The vCPU's features come from the host, and on a CPU without AArch32 EL1 a cleared RW would make EL2 switch registers that are UNDEFINED there. Clear it only when the system has AArch32 EL1, the same check system_supported_vcpu_features() makes on the host.

Fixes b56680de9c648 ("KVM: arm64: Initialize trap register values in hyp in pKVM")
---
 arch/arm64/kvm/hyp/nvhe/pkvm.c | 8 ++++++++
 1 file changed, 8 insertions(+)
arch/arm64/kvm/hyp/nvhe/pkvm.c +8 -0
index 459bd9eb7e4bc..affc9595fda20 100644
@@ -54,6 +54,14 @@ static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu)
 	else
 		vcpu->arch.hcr_el2 |= HCR_TID2;
 
+	/*
+	 * At EL2, vcpu_el1_is_32bit() reads HCR_EL2.RW, and EL2 switches the
+	 * *32_EL2 registers when it returns true; they're UNDEFINED without AArch32 EL1.
+	 */
+	if (vcpu_has_feature(vcpu, KVM_ARM_VCPU_EL1_32BIT) &&
+	    cpus_have_final_cap(ARM64_HAS_32BIT_EL1))
+		vcpu->arch.hcr_el2 &= ~HCR_RW;
+
 	if (vcpu_has_ptrauth(vcpu))
 		vcpu->arch.hcr_el2 |= (HCR_API | HCR_APK);
 
Signature · 1 line
2.39.5
maintainer
#4 lore raw

Re: [PATCH v1 2/4] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs

Quoted 7 lines · Fuad Tabba

In pKVM, KVM_RUN on a vCPU created with KVM_ARM_VCPU_EL1_32BIT fails with KVM_EXIT_FAIL_ENTRY. pkvm_vcpu_reset_hcr(), pKVM's EL2 counterpart of vcpu_set_hcr(), never clears HCR_EL2.RW, so the first ERET into the vCPU is an illegal exception return. Protected VMs are AArch64-only, so only non-protected VMs are affected.

Quoted 5 lines

Clear RW for 32-bit vCPUs. The vCPU's features come from the host, and on a CPU without AArch32 EL1 a cleared RW would make EL2 switch registers that are UNDEFINED there. Clear it only when the system has AArch32 EL1, the same check system_supported_vcpu_features() makes on the host.

Apologies from repainting the proverbial bike shed, but I find the way the above is written very hard to understand, because you are describing minute aspects of the code without giving the big picture upfront.

I'd rather see something like:

"pKVM maintains its own copy of a per-vcpu HCR_EL2. On initialisation of the hypervisor's private vcpu structure, HCR_EL2.RW is set to 1 unconditionally. However, nothing forbids userspace to create a non-protected, AArch32 guest. Since HCR_EL2.RW==1, entering the guest fails with an IL exception.

Make sure HCR_EL2.RW is cleared when the vcpu is AArch32 at EL1, and that the HW actually supports this."

which describes the problem without the reader having to dig into code. It makes matching the description and the code very easy. At least for me, YMMV...

Quoted 13 lines
Fixes b56680de9c648 ("KVM: arm64: Initialize trap register values in hyp in pKVM")
---
 arch/arm64/kvm/hyp/nvhe/pkvm.c | 8 ++++++++
 1 file changed, 8 insertions(+)
arch/arm64/kvm/hyp/nvhe/pkvm.c +0 -0
index 459bd9eb7e4bc..affc9595fda20 100644

arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu) · line 60

Quoted 7 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +4 -0
@@ -54,6 +54,14 @@ static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu)
 	else
 		vcpu->arch.hcr_el2 |= HCR_TID2;
 
+	/*
+	 * At EL2, vcpu_el1_is_32bit() reads HCR_EL2.RW, and EL2 switches the
+	 * *32_EL2 registers when it returns true; they're UNDEFINED without AArch32 EL1.
+	 */

That's another example: yes, this is all true. But how relevant is it? Are you clearing RW just for the sake of vcpu_el1_is_32bit() to work and prevent an UNDEF? No. It is so that 32bit guests do work. The above really is only paraphrasing the ARM ARM.

What you don't describe is that if ARM64_HAS_32BIT_EL1 is not implemented, you let the vcpu walk to the cliff with RW==1, and rely on the CPU generating an IL exception. For me, this is the important piece of information that needs to be captured.

arch/arm64/kvm/hyp/nvhe/pkvm.c static void pkvm_vcpu_reset_hcr(struct kvm_vcpu *vcpu) · line 67

Quoted 7 lines
arch/arm64/kvm/hyp/nvhe/pkvm.c +4 -0
+	if (vcpu_has_feature(vcpu, KVM_ARM_VCPU_EL1_32BIT) &&
+	    cpus_have_final_cap(ARM64_HAS_32BIT_EL1))
+		vcpu->arch.hcr_el2 &= ~HCR_RW;
+
 	if (vcpu_has_ptrauth(vcpu))
 		vcpu->arch.hcr_el2 |= (HCR_API | HCR_APK);
 

Other than that, the patch looks great! :)

Thanks,

	M.
Signature · 1 line
Jazz isn't dead. It just smells funny.
author reviewer
#5 lore raw

Re: [PATCH v1 2/4] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs

Hi Marc,

On Fri, 25 Sep 2026 16:01:35 +0100, Marc Zyngier <[email protected]> wrote:
[...]

Quoted 4 lines

Apologies from repainting the proverbial bike shed, but I find the way the above is written very hard to understand, because you are describing minute aspects of the code without giving the big picture upfront.

Writing commit messages and comments is hard! :) I'm working on it...

Quoted 9 lines

I'd rather see something like:

"pKVM maintains its own copy of a per-vcpu HCR_EL2. On initialisation of the hypervisor's private vcpu structure, HCR_EL2.RW is set to 1 unconditionally. However, nothing forbids userspace to create a non-protected, AArch32 guest. Since HCR_EL2.RW==1, entering the guest fails with an IL exception.

Quoted 6 lines

Make sure HCR_EL2.RW is cleared when the vcpu is AArch32 at EL1, and that the HW actually supports this."

which describes the problem without the reader having to dig into code. It makes matching the description and the code very easy. At least for me, YMMV...

Much clearer, I'll use that for v2, and give the other messages in the series the same treatment.

[...]

Quoted 9 lines

That's another example: yes, this is all true. But how relevant is it? Are you clearing RW just for the sake of vcpu_el1_is_32bit() to work and prevent an UNDEF? No. It is so that 32bit guests do work. The above really is only paraphrasing the ARM ARM.

What you don't describe is that if ARM64_HAS_32BIT_EL1 is not implemented, you let the vcpu walk to the cliff with RW==1, and rely on the CPU generating an IL exception. For me, this is the important piece of information that needs to be captured.

The comment should say what happens without AArch32 EL1: RW stays set and the entry fails with the IL exception, rather than EL2 switching registers that are UNDEFINED there. I'll rewrite it for v2.

[...]

Quoted 1 line

Other than that, the patch looks great! :)

Sashiko didn't complain... :D

Cheers,
/fuad

author reviewer
#6 lore raw

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

In reply to Fuad Tabba

In pKVM, a non-protected VM gets only TWI, TWE and VSE from the HCR_EL2 the host computes for it. EL2 sets the rest in pkvm_vcpu_reset_hcr(), which covers only part of vcpu_set_hcr(). On a CPU with MTE the VM can then read GMID_EL1, on one without FGT it can execute a TLBI OS its ID registers hide, and it never gets the host's TVM, VI or VF.

Use the host's HCR_EL2 on every entry instead, except for the bits EL2 owns. The other bits only control what the VM's own execution traps on and which virtual exceptions are pending for it. The host computes them from the vCPU's features, ID registers and flags, which EL2 already takes from the host for a non-protected VM, as it takes MDCR_EL2, HCRX_EL2 and the fine-grained traps.

ATA, an owned bit, stays clear, as pKVM doesn't support MTE for any guest. TID2 and TID4 move to pvm_init_traps_hcr(), since EL2 now sets them only for a protected VM. A protected VM's HCR_EL2 is unchanged.

Fixes b56680de9c648 ("KVM: arm64: Initialize trap register values in hyp in pKVM")
---
 arch/arm64/include/asm/kvm_arm.h       |  1 +
 arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 13 +++++++++++++
 arch/arm64/kvm/hyp/nvhe/hyp-main.c     |  7 ++++---
 arch/arm64/kvm/hyp/nvhe/pkvm.c         | 18 ++++++++----------
 4 files changed, 26 insertions(+), 13 deletions(-)
arch/arm64/include/asm/kvm_arm.h +1 -0
index 4bfbd827c5aa7..8d187650e463e 100644
@@ -30,6 +30,7 @@
 #define HCR_AMVOFFEN	__HCR(AMVOFFEN)
 #define HCR_TICAB	__HCR(TICAB)
 #define HCR_TID4	__HCR(TID4)
+#define HCR_GPF		__HCR(GPF)
 #define HCR_FIEN	__HCR(FIEN)
 #define HCR_FWB		__HCR(FWB)
 #define HCR_NV2		__HCR(NV2)
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +13 -0
index c904647d2f760..75b1122db4c91 100644
@@ -12,6 +12,19 @@
 #include <nvhe/gfp.h>
 #include <nvhe/spinlock.h>
 
+/*
+ * HCR_EL2 bits EL2 owns for a non-protected VM, whatever the host sets: those
+ * that restrict the guest, configure EL2 or what it switches (E2H, RW), or
+ * enable state EL2 doesn't switch or support. RES0 is included, so a bit comes
+ * from the host only once arch/arm64/tools/sysreg describes it.
+ */
+#define PKVM_HCR_EL2_OWNED	((HCR_GUEST_FLAGS & ~(HCR_TWI | HCR_TWE)) | HCR_BSU |	\
+				 HCR_E2H | HCR_TGE | HCR_TEA | HCR_GPF | HCR_TERR |	\
+				 HCR_FWB | HCR_DC | HCR_ID | HCR_CD | HCR_NV |		\
+				 HCR_NV1 | HCR_NV2 | HCR_API | HCR_APK | HCR_ATA |	\
+				 HCR_DCT | HCR_FIEN | HCR_AMVOFFEN | HCR_ENSCXT |	\
+				 HCR_EL2_RES0)
+
 /*
  * Holds the relevant data for maintaining the vcpu state completely at hyp.
  */
arch/arm64/kvm/hyp/nvhe/hyp-main.c +4 -3
index 9a3b92e626adb..dec99d5bbee78 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 = HCR_TWI | HCR_TWE | HCR_VSE;
 
 	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_OWNED;
 	} 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 affc9595fda20..2cf704036871b 100644
@@ -30,6 +30,7 @@ unsigned int kvm_host_sve_max_vl;
  */
 static DEFINE_PER_CPU(struct pkvm_hyp_vcpu *, loaded_hyp_vcpu);
 
+/* A non-protected VM keeps only the PKVM_HCR_EL2_OWNED bits of the HCR_EL2 set here. */
 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;
-
 	/*
 	 * At EL2, vcpu_el1_is_32bit() reads HCR_EL2.RW, and EL2 switches the
 	 * *32_EL2 registers when it returns true; they're UNDEFINED without AArch32 EL1.
@@ -64,9 +58,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)
@@ -84,6 +75,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_TID4;
+	else
+		val |= HCR_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
maintainer
#7 lore raw

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

Quoted 14 lines · Fuad Tabba

In pKVM, a non-protected VM gets only TWI, TWE and VSE from the HCR_EL2 the host computes for it. EL2 sets the rest in pkvm_vcpu_reset_hcr(), which covers only part of vcpu_set_hcr(). On a CPU with MTE the VM can then read GMID_EL1, on one without FGT it can execute a TLBI OS its ID registers hide, and it never gets the host's TVM, VI or VF.

Use the host's HCR_EL2 on every entry instead, except for the bits EL2 owns. The other bits only control what the VM's own execution traps on and which virtual exceptions are pending for it. The host computes them from the vCPU's features, ID registers and flags, which EL2 already takes from the host for a non-protected VM, as it takes MDCR_EL2, HCRX_EL2 and the fine-grained traps.

Quoted 3 lines

ATA, an owned bit, stays clear, as pKVM doesn't support MTE for any guest. TID2 and TID4 move to pvm_init_traps_hcr(), since EL2 now sets them only for a protected VM. A protected VM's HCR_EL2 is unchanged.

But the host does set these bits for non-protected VMs. What is going to honor these traps?

No mention of why you are adding HCR_EL2_GPF here?

Quoted 33 lines
Fixes b56680de9c648 ("KVM: arm64: Initialize trap register values in hyp in pKVM")
---
 arch/arm64/include/asm/kvm_arm.h       |  1 +
 arch/arm64/kvm/hyp/include/nvhe/pkvm.h | 13 +++++++++++++
 arch/arm64/kvm/hyp/nvhe/hyp-main.c     |  7 ++++---
 arch/arm64/kvm/hyp/nvhe/pkvm.c         | 18 ++++++++----------
 4 files changed, 26 insertions(+), 13 deletions(-)
arch/arm64/include/asm/kvm_arm.h +1 -0
index 4bfbd827c5aa7..8d187650e463e 100644
@@ -30,6 +30,7 @@
 #define HCR_AMVOFFEN	__HCR(AMVOFFEN)
 #define HCR_TICAB	__HCR(TICAB)
 #define HCR_TID4	__HCR(TID4)
+#define HCR_GPF		__HCR(GPF)
 #define HCR_FIEN	__HCR(FIEN)
 #define HCR_FWB		__HCR(FWB)
 #define HCR_NV2		__HCR(NV2)
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +3 -0
index c904647d2f760..75b1122db4c91 100644
@@ -12,6 +12,19 @@
 #include <nvhe/gfp.h>
 #include <nvhe/spinlock.h>
 
+/*
+ * HCR_EL2 bits EL2 owns for a non-protected VM, whatever the host sets: those
+ * that restrict the guest, configure EL2 or what it switches (E2H, RW), or

arch/arm64/kvm/hyp/include/nvhe/pkvm.h line 27

Quoted 10 lines
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +10 -0
+ * enable state EL2 doesn't switch or support. RES0 is included, so a bit comes
+ * from the host only once arch/arm64/tools/sysreg describes it.
+ */
+#define PKVM_HCR_EL2_OWNED	((HCR_GUEST_FLAGS & ~(HCR_TWI | HCR_TWE)) | HCR_BSU |	\
+				 HCR_E2H | HCR_TGE | HCR_TEA | HCR_GPF | HCR_TERR |	\
+				 HCR_FWB | HCR_DC | HCR_ID | HCR_CD | HCR_NV |		\
+				 HCR_NV1 | HCR_NV2 | HCR_API | HCR_APK | HCR_ATA |	\
+				 HCR_DCT | HCR_FIEN | HCR_AMVOFFEN | HCR_ENSCXT |	\
+				 HCR_EL2_RES0)
+

The name of the macro doesn't indicate that this only applies to protected VM.

Also, please don't add new uses of the compat HCR macros. I really want to remove them (probably post -rc1).

Quoted 16 lines
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +0 -0
 /*
  * Holds the relevant data for maintaining the vcpu state completely at hyp.
  */
arch/arm64/kvm/hyp/nvhe/hyp-main.c +1 -0
index 9a3b92e626adb..dec99d5bbee78 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 = HCR_TWI | HCR_TWE | HCR_VSE;
 
 	fpsimd_sve_flush();
 	flush_debug_state(hyp_vcpu);

arch/arm64/kvm/hyp/nvhe/hyp-main.c static void flush_hyp_vcpu(struct pkvm_hyp_vcpu *hyp_vcpu) · line 232

Quoted 4 lines
arch/arm64/kvm/hyp/nvhe/hyp-main.c +1 -0
@@ -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_OWNED;

This feels fragile. You start with a restrictive set (TWI, TWE, VSE), and then drop it all. At this point, I have no idea what you are letting in.

It would be better to express things in a consistent way:

- either the bits that are controlled by the host for either protected and non-protected guests,

- or the bits that are controlled by the hypervisor for either cases.

Here, you're mixing both, and that's confusing.

Thanks,

	M.
Signature · 1 line
Jazz isn't dead. It just smells funny.
author reviewer
#8 lore raw

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

Hi Marc,

On Sun, 27 Sep 2026 10:15:07 +0100, Marc Zyngier <[email protected]> wrote:
[...]

Quoted 6 lines
Quoted 3 lines

ATA, an owned bit, stays clear, as pKVM doesn't support MTE for any guest. TID2 and TID4 move to pvm_init_traps_hcr(), since EL2 now sets them only for a protected VM. A protected VM's HCR_EL2 is unchanged.

But the host does set these bits for non-protected VMs. What is going to honor these traps?

The host, as it does today: the traps of a non-protected VM that the hypervisor doesn't handle itself are forwarded to the host. What changes is where the bits come from: for a non-protected VM they come from the host's HCR_EL2 with the rest, and the hypervisor only sets them itself for a protected VM. I'll rewrite the message to make that clear.

Quoted 2 lines

No mention of why you are adding HCR_EL2_GPF here?

It was only there to name the bit in the mask below. It's gone in v2.

[...]

Quoted 11 lines

arch/arm64/kvm/hyp/include/nvhe/pkvm.h line 27

Quoted 7 lines
arch/arm64/kvm/hyp/include/nvhe/pkvm.h +7 -0
+#define PKVM_HCR_EL2_OWNED        ((HCR_GUEST_FLAGS & ~(HCR_TWI | HCR_TWE)) | HCR_BSU |        \
+                                 HCR_E2H | HCR_TGE | HCR_TEA | HCR_GPF | HCR_TERR |        \
+                                 HCR_FWB | HCR_DC | HCR_ID | HCR_CD | HCR_NV |                \
+                                 HCR_NV1 | HCR_NV2 | HCR_API | HCR_APK | HCR_ATA |        \
+                                 HCR_DCT | HCR_FIEN | HCR_AMVOFFEN | HCR_ENSCXT |        \
+                                 HCR_EL2_RES0)
+

The name of the macro doesn't indicate that this only applies to protected VM.

Quoted 2 lines

Also, please don't add new uses of the compat HCR macros. I really want to remove them (probably post -rc1).

Will do: the masks in v2 are named per VM type and use the HCR_EL2_* names.

[...]

Quoted 4 lines

This feels fragile. You start with a restrictive set (TWI, TWE, VSE), and then drop it all. At this point, I have no idea what you are letting in.

Quoted 8 lines

It would be better to express things in a consistent way:

- either the bits that are controlled by the host for either protected and non-protected guests,

- or the bits that are controlled by the hypervisor for either cases.

Here, you're mixing both, and that's confusing.

Ack. I'll go with the bits the host controls, one set per VM type:
TWI, TWE and VSE for a protected VM, and those plus the per-VM traps and virtual interrupts for a non-protected one. The flush then applies the same mask logic to both.

Cheers,
/fuad

author reviewer
#9 lore raw

PATCH 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. In pKVM, a hidden TLBI OS executed on a CPU without FGT until "KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM".

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