[PATCH v2] KVM: arm64: Enable S1PIE for hVHE
Marc Zyngier
maz at kernel.org
Wed Sep 9 02:44:26 PDT 2026
On Tue, 08 Sep 2026 20:40:47 +0100,
Mark Brown <broonie at kernel.org> wrote:
>
> When FEAT_S1PIE (stage 1 permission indirection) is supported we
> currently enable and use it in the hypervisor when running in VHE mode
> but not when running in nVHE or hVHE mode. While systems with
> FEAT_S1PIE would normally use VHE users can configure them for nVHE or
Missing comma after VHE.
> hVHE. Enable FEAT_S1PIE with hVHE only, hVHE is used for protected VMs
> but there is no real use case for nVHE mode on hardware with this
> feature.
That's not the reason. The reason is that there is no nVHE-only
hardware with PIE, and that on VHE-capable HW, nVHE and hVHE are
strictly equivalent. Therefore there is no need to add support for HW
that does not exist.
>
> AP[1] is one of the bits used to encode the indirected permissions.
> Since for hVHE this is always 0 we only configure the subset of
> indirected permissions that the system is expected to use.
>
> With permission indirection read and write permissions must be encoded
> in the bits used by S1PIE, set DBM for writable mappings. Only do this
> when using S1PIE, the hypervisor does not otherwise use DBM so no
> existing hypervisor code sets that bit. Since the meaning is assigned
> via S1PIE this does not actually enable DBM, the mappings we configure
> just grant write permission.
>
> In order to enable S1PIE we also need to configure TCR2_EL2 which is
> currently only done in __finalise_el2 which is VHE only, do so when the
> register is present. When running in nVHE we leave TCR2_EL2.PIE
> disabled. This ensures we have an explicit configuration for TCR2_EL2
> when it is present in the system.
>
> For simplicity we unconditionally initialise PIR_EL2 and PIRE0_EL2 if
> FEAT_S1PIE is present, this will have no effect in nVHE mode since we
> set TCR2_EL2.PIE to 0.
>
> This should have no practical impact other than causing any unexpected
> encodings to map to no permissions instead of their default
> meanings.
What default meanings?
> It will mean that the configuration is closer to that in VHE mode, and
> will be required for future work enabling features like D128 and GCS
> which are only available via indirection.
>
> Signed-off-by: Mark Brown <broonie at kernel.org>
> ---
> Changes in v2:
> - Rebase onto v7.3-rc2.
> - Don't bother cleaning up TCR2_EL2 on hypervisor exit, and squash the
> handling into the S1PIE patch.
> - Remove support for nVHE mode.
> - Link to v1: https://patch.msgid.link/20260904-kvm-arm64-nvhe-pie-v1-0-29d59f245e6c@kernel.org
> ---
> arch/arm64/include/asm/kvm_arm.h | 19 +++++++++++++++++++
> arch/arm64/include/asm/kvm_asm.h | 1 +
> arch/arm64/include/asm/kvm_pgtable.h | 1 +
> arch/arm64/kernel/asm-offsets.c | 1 +
> arch/arm64/kvm/arm.c | 8 +++++++-
> arch/arm64/kvm/hyp/nvhe/hyp-init.S | 16 ++++++++++++++--
> arch/arm64/kvm/hyp/pgtable.c | 8 ++++++++
> 7 files changed, 51 insertions(+), 3 deletions(-)
>
> diff --git a/arch/arm64/include/asm/kvm_arm.h b/arch/arm64/include/asm/kvm_arm.h
> index 4bfbd827c5aa..eecac91d41e4 100644
> --- a/arch/arm64/include/asm/kvm_arm.h
> +++ b/arch/arm64/include/asm/kvm_arm.h
> @@ -345,4 +345,23 @@
> #define VCPU_RESET_PSTATE_SVC (PSR_AA32_MODE_SVC | PSR_AA32_A_BIT | \
> PSR_AA32_I_BIT | PSR_AA32_F_BIT)
>
> +/*
> + * Permission indirection configuration for the hVHE hypervisor when
> + * we have FEAT_S1PIE. Like the host kernel we configure a mapping
> + * mostly equivalent to the non-PIE meanings of the bits so the
> + * page table manipulation code needs minimal updates for PIE.
> + *
> + * These mappings are minimal with only things used from the
> + * hVHE hypervisor, nVHE is not supported. Write permission is
> + * controlled via DBM.
They are controlled by PIIndex[1]. Yes, this is the same bit. No, this
doesn't mean the same thing. When PIE is enabled, DBM is in the PIE
register.
> + */
> +
> +#define KVM_HYP_PIR_IDX(uxn, pxn, dbm, ap1) (((uxn) << 3) | ((pxn) << 2) | \
> + ((dbm) << 1) | (ap1))
> +
This is not what these bits are called. They are just PIIndex[] bits.
> +#define KVM_HVHE_PIR_EL2 ( \
> + PIRx_ELx_PERM_PREP(KVM_HYP_PIR_IDX(0, 0, 0, 0), PIE_RX) | \
> + PIRx_ELx_PERM_PREP(KVM_HYP_PIR_IDX(1, 1, 0, 0), PIE_R) | \
> + PIRx_ELx_PERM_PREP(KVM_HYP_PIR_IDX(1, 1, 1, 0), PIE_RW))
> +
> #endif /* __ARM64_KVM_ARM_H__ */
> diff --git a/arch/arm64/include/asm/kvm_asm.h b/arch/arm64/include/asm/kvm_asm.h
> index e5b92ac09e69..eb796436d6eb 100644
> --- a/arch/arm64/include/asm/kvm_asm.h
> +++ b/arch/arm64/include/asm/kvm_asm.h
> @@ -208,6 +208,7 @@ extern void *__vhe_undefined_symbol;
> struct kvm_nvhe_init_params {
> unsigned long mair_el2;
> unsigned long tcr_el2;
> + unsigned long tcr2_el2;
> unsigned long tpidr_el2;
> unsigned long stack_hyp_va;
> unsigned long stack_pa;
> diff --git a/arch/arm64/include/asm/kvm_pgtable.h b/arch/arm64/include/asm/kvm_pgtable.h
> index 41a8687938eb..7b1b7ab3e88e 100644
> --- a/arch/arm64/include/asm/kvm_pgtable.h
> +++ b/arch/arm64/include/asm/kvm_pgtable.h
> @@ -93,6 +93,7 @@ typedef u64 kvm_pte_t;
>
> #define KVM_PTE_LEAF_ATTR_HI_S2_XN GENMASK(54, 53)
>
> +#define KVM_PTE_LEAF_ATTR_HI_S1_DBM BIT(51)
> #define KVM_PTE_LEAF_ATTR_HI_S1_GP BIT(50)
>
> #define KVM_PTE_LEAF_ATTR_S2_PERMS (KVM_PTE_LEAF_ATTR_LO_S2_S2AP_R | \
> diff --git a/arch/arm64/kernel/asm-offsets.c b/arch/arm64/kernel/asm-offsets.c
> index 9c853ed3ceab..baffe58015d6 100644
> --- a/arch/arm64/kernel/asm-offsets.c
> +++ b/arch/arm64/kernel/asm-offsets.c
> @@ -118,6 +118,7 @@ int main(void)
> DEFINE(HOST_DATA_CONTEXT, offsetof(struct kvm_host_data, host_ctxt));
> DEFINE(NVHE_INIT_MAIR_EL2, offsetof(struct kvm_nvhe_init_params, mair_el2));
> DEFINE(NVHE_INIT_TCR_EL2, offsetof(struct kvm_nvhe_init_params, tcr_el2));
> + DEFINE(NVHE_INIT_TCR2_EL2, offsetof(struct kvm_nvhe_init_params, tcr2_el2));
> DEFINE(NVHE_INIT_TPIDR_EL2, offsetof(struct kvm_nvhe_init_params, tpidr_el2));
> DEFINE(NVHE_INIT_STACK_HYP_VA, offsetof(struct kvm_nvhe_init_params, stack_hyp_va));
> DEFINE(NVHE_INIT_PGD_PA, offsetof(struct kvm_nvhe_init_params, pgd_pa));
> diff --git a/arch/arm64/kvm/arm.c b/arch/arm64/kvm/arm.c
> index 8b080804bc90..607a6f808b1c 100644
> --- a/arch/arm64/kvm/arm.c
> +++ b/arch/arm64/kvm/arm.c
> @@ -2158,7 +2158,7 @@ static int kvm_init_vector_slots(void)
> static void __init cpu_prepare_hyp_mode(int cpu, u32 hyp_va_bits)
> {
> struct kvm_nvhe_init_params *params = per_cpu_ptr_nvhe_sym(kvm_init_params, cpu);
> - unsigned long tcr;
> + unsigned long tcr, tcr2;
>
> /*
> * Calculate the raw per-cpu offset without a translation from the
> @@ -2186,6 +2186,12 @@ static void __init cpu_prepare_hyp_mode(int cpu, u32 hyp_va_bits)
> tcr |= TCR_T0SZ(hyp_va_bits);
> params->tcr_el2 = tcr;
>
> + tcr2 = 0;
> + if (cpus_have_final_cap(ARM64_HAS_S1PIE) &&
> + cpus_have_final_cap(ARM64_KVM_HVHE))
> + tcr2 |= TCR2_EL2_PIE;
> + params->tcr2_el2 = tcr2;
> +
> params->pgd_pa = kvm_mmu_get_httbr();
> if (is_protected_kvm_enabled())
> params->hcr_el2 = HCR_HOST_NVHE_PROTECTED_FLAGS;
> diff --git a/arch/arm64/kvm/hyp/nvhe/hyp-init.S b/arch/arm64/kvm/hyp/nvhe/hyp-init.S
> index 0b3e0b28dfc7..cd5b75c8776f 100644
> --- a/arch/arm64/kvm/hyp/nvhe/hyp-init.S
> +++ b/arch/arm64/kvm/hyp/nvhe/hyp-init.S
> @@ -137,8 +137,20 @@ alternative_if ARM64_HAS_CNP
> alternative_else_nop_endif
> msr ttbr0_el2, x2
>
> - ldr x0, [x0, #NVHE_INIT_TCR_EL2]
> - msr tcr_el2, x0
> + ldr x1, [x0, #NVHE_INIT_TCR_EL2]
> + msr tcr_el2, x1
> +
> +alternative_if ARM64_HAS_S1PIE
> + /* S1PIE is only enabled with TCR2_EL2.PIE if we are running hVHE */
> + mov_q x1, KVM_HVHE_PIR_EL2
> + msr REG_PIR_EL2, x1
> + msr REG_PIRE0_EL2, xzr
> +alternative_else_nop_endif
> +
> +alternative_if ARM64_HAS_TCR2
> + ldr x1, [x0, #NVHE_INIT_TCR2_EL2]
> + msr REG_TCR2_EL2, x1
> +alternative_else_nop_endif
S1PIE implies TCR2. Why the additional alternatives?
>
> isb
>
> diff --git a/arch/arm64/kvm/hyp/pgtable.c b/arch/arm64/kvm/hyp/pgtable.c
> index b74dd5ce1efd..5276c2874fe0 100644
> --- a/arch/arm64/kvm/hyp/pgtable.c
> +++ b/arch/arm64/kvm/hyp/pgtable.c
> @@ -349,6 +349,14 @@ static int hyp_set_prot_attr(enum kvm_pgtable_prot prot, kvm_pte_t *ptep)
>
> if (system_supports_bti_kernel())
> attr |= KVM_PTE_LEAF_ATTR_HI_S1_GP;
> + } else if (cpus_have_final_cap(ARM64_HAS_S1PIE) &&
> + cpus_have_final_cap(ARM64_KVM_HVHE) &&
> + (prot & KVM_PGTABLE_PROT_W)) {
> + /*
> + * When using S1PIE for hVHE set DBM for writable
> + * mappings since AP[2] is ineffective.
It's not ineffective. It doesn't exist! See R_JJNHR.
This is totally obfuscating what is really going on, which is really
simple. We have 4 permission index bits in the PTE, and the both the
code and comments should reflect that.
> + */
> + attr |= KVM_PTE_LEAF_ATTR_HI_S1_DBM;
So this sets PIIndex[1]. But It isn't obvious how this takes us from
R(0x8) to RW(0xC). Where is PIIndex[3] coming from? It is supplied by
the code following through the UXN/PXN bbits, but it is mind-boggling
that we should rely on something deals with non-execution to control
the *READ* permission.
TBH, I think this is completely going the wrong way. Why can't we
write this as a discrete enumeration of the permission combination we
support (all 3 of them), and map that to the correct index?
M.
--
Without deviation from the norm, progress is not possible.
More information about the linux-arm-kernel
mailing list