From e079ef0b1c40c7a4a1be0c1ac5dbff95685c3beb Mon Sep 17 00:00:00 2001 From: Marc Zyngier Date: Tue, 29 Sep 2026 10:35:44 +0100 Subject: [PATCH 1/8] KVM: arm64: vgic: Allow last_lr_irq to be NULL when LRs are not overflowing last_lr_irq is always populated when there is any interrupt populated in the AP list. Not only this is not necessary (it is only useful when we completely fill the LRs), but this is in the way of further fixes. Make sure last_lr_irq is kept to NULL when we LRs are not completely full. Fixes: 6da5e537f5afe ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail") Reviewed-by: Fuad Tabba Tested-by: Fuad Tabba Signed-off-by: Marc Zyngier Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20260929093548.3598547-4-maz@kernel.org Signed-off-by: Oliver Upton --- arch/arm64/kvm/vgic/vgic-v2.c | 6 ++++-- arch/arm64/kvm/vgic/vgic-v3.c | 6 ++++-- arch/arm64/kvm/vgic/vgic.c | 8 +++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/arch/arm64/kvm/vgic/vgic-v2.c b/arch/arm64/kvm/vgic/vgic-v2.c index 7182f63fc938..70cc53ba810a 100644 --- a/arch/arm64/kvm/vgic/vgic-v2.c +++ b/arch/arm64/kvm/vgic/vgic-v2.c @@ -122,6 +122,10 @@ void vgic_v2_fold_lr_state(struct kvm_vcpu *vcpu) for (int lr = 0; lr < vgic_cpu->vgic_v2.used_lrs; lr++) vgic_v2_fold_lr(vcpu, cpuif->vgic_lr[lr]); + cpuif->used_lrs = 0; + if (!irq) + return; + /* See the GICv3 equivalent for the EOIcount handling rationale */ list_for_each_entry_continue(irq, &vgic_cpu->ap_list_head, ap_list) { u32 lr; @@ -144,8 +148,6 @@ void vgic_v2_fold_lr_state(struct kvm_vcpu *vcpu) vgic_v2_fold_lr(vcpu, lr); eoicount--; } - - cpuif->used_lrs = 0; } void vgic_v2_deactivate(struct kvm_vcpu *vcpu, u32 val) diff --git a/arch/arm64/kvm/vgic/vgic-v3.c b/arch/arm64/kvm/vgic/vgic-v3.c index 726e20a1da6e..346bacb3198f 100644 --- a/arch/arm64/kvm/vgic/vgic-v3.c +++ b/arch/arm64/kvm/vgic/vgic-v3.c @@ -155,6 +155,10 @@ void vgic_v3_fold_lr_state(struct kvm_vcpu *vcpu) for (int lr = 0; lr < cpuif->used_lrs; lr++) vgic_v3_fold_lr(vcpu, cpuif->vgic_lr[lr]); + cpuif->used_lrs = 0; + if (!irq) + return; + /* * EOIMode=0: use EOIcount to emulate deactivation. We are * guaranteed to deactivate in reverse order of the activation, so @@ -188,8 +192,6 @@ void vgic_v3_fold_lr_state(struct kvm_vcpu *vcpu) vgic_v3_fold_lr(vcpu, lr); eoicount--; } - - cpuif->used_lrs = 0; } void vgic_v3_deactivate(struct kvm_vcpu *vcpu, u64 val) diff --git a/arch/arm64/kvm/vgic/vgic.c b/arch/arm64/kvm/vgic/vgic.c index b25303d9919f..425503e0c582 100644 --- a/arch/arm64/kvm/vgic/vgic.c +++ b/arch/arm64/kvm/vgic/vgic.c @@ -866,9 +866,6 @@ static void vgic_fold_state(struct kvm_vcpu *vcpu) return; } - if (!*host_data_ptr(last_lr_irq)) - return; - if (kvm_vgic_global_state.type == VGIC_V2) vgic_v2_fold_lr_state(vcpu); else @@ -1021,11 +1018,12 @@ static void vgic_flush_lr_state(struct kvm_vcpu *vcpu) scoped_guard(raw_spinlock, &irq->irq_lock) { if (likely(vgic_target_oracle(irq) == vcpu)) { vgic_populate_lr(vcpu, irq, count++); - *host_data_ptr(last_lr_irq) = irq; + if (count == kvm_vgic_global_state.nr_lr) + *host_data_ptr(last_lr_irq) = irq; } } - if (count == kvm_vgic_global_state.nr_lr) + if (*host_data_ptr(last_lr_irq)) break; } From 19bc5d79ece613f242b5ec03921a3b465321bfed Mon Sep 17 00:00:00 2001 From: Marc Zyngier Date: Tue, 29 Sep 2026 10:35:45 +0100 Subject: [PATCH 2/8] KVM: arm64: vgic: Take a refcount on IRQs referenced by last_lr_irq Referencing the last interrupt inserted in an LR is rather fragile, as this interrupt can vanish if a concurrently unmapped LPI. Solve this by bumping up the refcount on the interrupt when populating last_lr_irq, and drop it at vgic_prune_ap_list() time, when LPIs are being reclaimed. Fixes: 6da5e537f5afe ("KVM: arm64: vgic: Pick EOIcount deactivations from AP-list tail") Reported-by: Yuchao Zhang Reviewed-by: Fuad Tabba Tested-by: Fuad Tabba Signed-off-by: Marc Zyngier Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20260929093548.3598547-5-maz@kernel.org Signed-off-by: Oliver Upton --- arch/arm64/kvm/vgic/vgic.c | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/arch/arm64/kvm/vgic/vgic.c b/arch/arm64/kvm/vgic/vgic.c index 425503e0c582..5cf5a1ef86cd 100644 --- a/arch/arm64/kvm/vgic/vgic.c +++ b/arch/arm64/kvm/vgic/vgic.c @@ -853,6 +853,17 @@ static void vgic_prune_ap_list(struct kvm_vcpu *vcpu) goto retry; } + /* + * Fix the last_lr_irq refcount which was obtained while + * populating the LRs. This can also result in the LPI being + * deleted. + */ + irq = *host_data_ptr(last_lr_irq); + if (irq) { + deleted_lpis |= vgic_put_irq_norelease(vcpu->kvm, irq); + *host_data_ptr(last_lr_irq) = NULL; + } + raw_spin_unlock(&vgic_cpu->ap_list_lock); if (unlikely(deleted_lpis)) @@ -1018,8 +1029,10 @@ static void vgic_flush_lr_state(struct kvm_vcpu *vcpu) scoped_guard(raw_spinlock, &irq->irq_lock) { if (likely(vgic_target_oracle(irq) == vcpu)) { vgic_populate_lr(vcpu, irq, count++); - if (count == kvm_vgic_global_state.nr_lr) + if (count == kvm_vgic_global_state.nr_lr) { + vgic_get_irq_ref(irq); *host_data_ptr(last_lr_irq) = irq; + } } } From 751f4641560be8568dcc5af400c0e55a283abf51 Mon Sep 17 00:00:00 2001 From: Marc Zyngier Date: Tue, 29 Sep 2026 10:35:47 +0100 Subject: [PATCH 3/8] KVM: arm64: vgic-its: Fix MOVALL handling of source redistributor The MOVALL command moves the pending state of all LPIs targeting a given redistributor to another one. However, we seem to have lost the filtering on the source RD, which means we move all LPIs to the target. Not quite what the spec mandates. Hack update_affinity() to take an optional source vcpu that is used as a filter when non-NULL, restoring the filtering that was performed by vgic_copy_lpi_list() back in the days. Fixes: 11f4f8f3e6e06 ("KVM: arm64: vgic-its: Walk LPI xarray in vgic_its_cmd_handle_movall()") Signed-off-by: Marc Zyngier Cc: stable@vger.kernel.org Link: https://patch.msgid.link/20260929093548.3598547-7-maz@kernel.org Signed-off-by: Oliver Upton --- arch/arm64/kvm/vgic/vgic-its.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/arch/arm64/kvm/vgic/vgic-its.c b/arch/arm64/kvm/vgic/vgic-its.c index 0904ae850c35..c10a11373e10 100644 --- a/arch/arm64/kvm/vgic/vgic-its.c +++ b/arch/arm64/kvm/vgic/vgic-its.c @@ -319,12 +319,16 @@ static int update_lpi_config(struct kvm *kvm, struct vgic_irq *irq, return ret; } -static int update_affinity(struct vgic_irq *irq, struct kvm_vcpu *vcpu) +static int update_affinity(struct vgic_irq *irq, + struct kvm_vcpu *from_vcpu, struct kvm_vcpu *vcpu) { struct its_vlpi_map map; int ret; guard(raw_spinlock_irqsave)(&irq->irq_lock); + if (from_vcpu && irq->target_vcpu != from_vcpu) + return 0; + irq->target_vcpu = vcpu; if (!irq->hw) @@ -362,7 +366,7 @@ static void update_affinity_ite(struct kvm *kvm, struct its_ite *ite) return; vcpu = collection_to_vcpu(kvm, ite->collection); - update_affinity(ite->irq, vcpu); + update_affinity(ite->irq, NULL, vcpu); } /* @@ -856,7 +860,7 @@ static int vgic_its_cmd_handle_movi(struct kvm *kvm, struct vgic_its *its, vgic_its_invalidate_cache(its); - return update_affinity(ite->irq, vcpu); + return update_affinity(ite->irq, NULL, vcpu); } static bool __is_visible_gfn_locked(struct vgic_its *its, gpa_t gpa) @@ -1383,7 +1387,7 @@ static int vgic_its_cmd_handle_movall(struct kvm *kvm, struct vgic_its *its, if (!irq) continue; - update_affinity(irq, vcpu2); + update_affinity(irq, vcpu1, vcpu2); vgic_put_irq(kvm, irq); } From 4bdd2b3a708c1feb701cb8b62feb8ad2fd77bb07 Mon Sep 17 00:00:00 2001 From: Fuad Tabba Date: Tue, 29 Sep 2026 10:00:28 +0100 Subject: [PATCH 4/8] KVM: arm64: Apply the fine-grained UNDEFs without FEAT_FGT 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. Fixes: f5a5a406b4b8b ("KVM: arm64: Propagate and handle Fine-Grained UNDEF bits") Cc: stable@vger.kernel.org Signed-off-by: Fuad Tabba Reviewed-by: Wei-Lin Chang Link: https://patch.msgid.link/20260929090031.3829185-2-fuad.tabba@linux.dev [oliver: Take Wei-Lin's suggested changelog] Signed-off-by: Oliver Upton --- arch/arm64/kvm/emulate-nested.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/arch/arm64/kvm/emulate-nested.c b/arch/arm64/kvm/emulate-nested.c index 3806ff0920fe..f59d89d04eca 100644 --- a/arch/arm64/kvm/emulate-nested.c +++ b/arch/arm64/kvm/emulate-nested.c @@ -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); From 80ae56184b57176b908cd6a7ececf274a7e4b3c7 Mon Sep 17 00:00:00 2001 From: Fuad Tabba Date: Tue, 29 Sep 2026 10:00:29 +0100 Subject: [PATCH 5/8] KVM: arm64: Clear HCR_EL2.RW for 32-bit non-protected vCPUs 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 Link: https://patch.msgid.link/20260929090031.3829185-3-fuad.tabba@linux.dev Signed-off-by: Oliver Upton --- 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 bb3e0dc0676e..cd09d8888202 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); From 04447b95c62bef2b3591486c66244c49591a3dbd Mon Sep 17 00:00:00 2001 From: Fuad Tabba Date: Tue, 29 Sep 2026 10:00:30 +0100 Subject: [PATCH 6/8] KVM: arm64: Use the host's HCR_EL2 for non-protected VMs in pKVM 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 Link: https://patch.msgid.link/20260929090031.3829185-4-fuad.tabba@linux.dev Signed-off-by: Oliver Upton --- 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 c904647d2f76..5c050f21066a 100644 --- a/arch/arm64/kvm/hyp/include/nvhe/pkvm.h +++ b/arch/arm64/kvm/hyp/include/nvhe/pkvm.h @@ -12,6 +12,15 @@ #include #include +/* + * 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 9a3b92e626ad..ac64a036b0a9 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 cd09d8888202..6290c4b62659 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); From afb4334fb52c7b416bbc9c4b166f6806509f734b Mon Sep 17 00:00:00 2001 From: Fuad Tabba Date: Tue, 29 Sep 2026 10:00:31 +0100 Subject: [PATCH 7/8] KVM: arm64: selftests: Check a feature hidden in an ID register is UNDEF 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 Link: https://patch.msgid.link/20260929090031.3829185-5-fuad.tabba@linux.dev Signed-off-by: Oliver Upton --- 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 6a1482e3a286..51b98f47044f 100644 --- a/tools/testing/selftests/kvm/Makefile.kvm +++ b/tools/testing/selftests/kvm/Makefile.kvm @@ -195,6 +195,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 000000000000..194d746e7605 --- /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 + */ +#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(); +} From 71cc2c67fb8f8d5aa8154eb482e9846d2214f11b Mon Sep 17 00:00:00 2001 From: Mostafa Saleh Date: Tue, 29 Sep 2026 20:02:09 +0000 Subject: [PATCH 8/8] KVM: arm64: Use stage-1 leaf size for VM_PFNMAP When commit 2aa53d68cee6 ("KVM: arm64: Try stage2 block mapping for host device MMIO") added VM_PFNMAP support to get_vma_page_shift(), stage-1 page tables did not support huge PFNMAP (as in VFIO-PCI vfio_pci_mmap_huge_fault()) and transparent_hugepage_adjust() was unsafe for MMIO as it dereferenced struct page. Since commit 6011cf68c885 ("KVM: arm64: Walk userspace page tables to compute the THP mapping size"), transparent_hugepage_adjust() instead walks the host stage-1 page tables via get_user_mapping_size() without touching struct page. Meanwhile, commit 3e509c9b03f9 ("mm/arm64: support large pfn mappings") enabled stage-1 huge PFNMAP. So. we can drop the VMA-based VM_PFNMAP size calculation in get_vma_page_shift() and let transparent_hugepage_adjust() derive the stage-2 mapping size directly from the populated stage-1 leaf for non-cacheable mappings as well. Assisted-by: LLM Cc: stable@vger.kernel.org Fixes: 2aa53d68cee6 ("KVM: arm64: Try stage2 block mapping for host device MMIO") Reviewed-by: Marc Zyngier Signed-off-by: Mostafa Saleh Signed-off-by: Oliver Upton --- arch/arm64/kvm/mmu.c | 47 ++++++-------------------------------------- 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/arch/arm64/kvm/mmu.c b/arch/arm64/kvm/mmu.c index 2d44cd6a5aed..bf6d6526639f 100644 --- a/arch/arm64/kvm/mmu.c +++ b/arch/arm64/kvm/mmu.c @@ -1468,32 +1468,11 @@ transparent_hugepage_adjust(struct kvm *kvm, struct kvm_memory_slot *memslot, return PAGE_SIZE; } -static int get_vma_page_shift(struct vm_area_struct *vma, unsigned long hva) +static int get_vma_page_shift(struct vm_area_struct *vma) { - unsigned long pa; - - if (is_vm_hugetlb_page(vma) && !(vma->vm_flags & VM_PFNMAP)) + if (is_vm_hugetlb_page(vma)) return huge_page_shift(hstate_vma(vma)); - if (!(vma->vm_flags & VM_PFNMAP)) - return PAGE_SHIFT; - - VM_BUG_ON(is_vm_hugetlb_page(vma)); - - pa = (vma->vm_pgoff << PAGE_SHIFT) + (hva - vma->vm_start); - -#ifndef __PAGETABLE_PMD_FOLDED - if ((hva & (PUD_SIZE - 1)) == (pa & (PUD_SIZE - 1)) && - ALIGN_DOWN(hva, PUD_SIZE) >= vma->vm_start && - ALIGN(hva, PUD_SIZE) <= vma->vm_end) - return PUD_SHIFT; -#endif - - if ((hva & (PMD_SIZE - 1)) == (pa & (PMD_SIZE - 1)) && - ALIGN_DOWN(hva, PMD_SIZE) >= vma->vm_start && - ALIGN(hva, PMD_SIZE) <= vma->vm_end) - return PMD_SHIFT; - return PAGE_SHIFT; } @@ -1794,7 +1773,7 @@ static short kvm_s2_resolve_vma_size(const struct kvm_s2_fault_desc *s2fd, vma_shift = PAGE_SHIFT; } else { s2vi->max_map_size = PUD_SIZE; - vma_shift = get_vma_page_shift(vma, s2fd->hva); + vma_shift = get_vma_page_shift(vma); } switch (vma_shift) { @@ -1952,16 +1931,6 @@ static int kvm_s2_fault_pin_pfn(const struct kvm_s2_fault_desc *s2fd, return -EFAULT; } } else { - /* - * If the page was identified as device early by looking at - * the VMA flags, vma_pagesize is already representing the - * largest quantity we can map. If instead it was mapped - * via __kvm_faultin_pfn(), vma_pagesize is set to PAGE_SIZE - * and must not be upgraded. - * - * In both cases, we don't let transparent_hugepage_adjust() - * change things at the last minute. - */ s2vi->map_non_cacheable = true; } @@ -2051,10 +2020,10 @@ static int kvm_s2_fault_map(const struct kvm_s2_fault_desc *s2fd, /* * If we are not forced to use page mapping, check if we are - * backed by a THP and thus use block mapping if possible. + * backed by a huge stage-1 mapping and thus use block mapping if + * possible. */ - if (mapping_size == PAGE_SIZE && - !(s2vi->max_map_size == PAGE_SIZE || s2vi->map_non_cacheable)) { + if (mapping_size == PAGE_SIZE && s2vi->max_map_size != PAGE_SIZE) { if (perm_fault_granule > PAGE_SIZE) { mapping_size = perm_fault_granule; } else { @@ -2135,10 +2104,6 @@ static int user_mem_abort(const struct kvm_s2_fault_desc *s2fd) return ret; } - /* - * Let's check if we will get back a huge page backed by hugetlbfs, or - * get block mapping for device MMIO region. - */ ret = kvm_s2_fault_pin_pfn(s2fd, &s2vi); if (ret != 1) return ret;