KVM: arm64: Fallback to a supported value for unsupported guest TGx

When KVM derives the translation granule for emulated stage-1 and
stage-2 walks, it decodes TCR/VTCR.TGx and treats the granule as-is.
This is wrong when the guest programs a granule size that is not
advertised in the guest's ID_AA64MMFR0_EL1.TGRAN* fields.
Architecturally, such a value must be treated as an implemented granule
size. Choose an available one while prioritizing PAGE_SIZE.

Signed-off-by: Wei-Lin Chang <weilin.chang@arm.com>
Link: https://patch.msgid.link/20260414000334.3947257-5-weilin.chang@arm.com
[maz: minor tidying up]
Signed-off-by: Marc Zyngier <maz@kernel.org>
This commit is contained in:
Wei-Lin Chang 2026-04-14 01:03:34 +01:00 committed by Marc Zyngier
parent b154da8288
commit 8853566dfb
2 changed files with 128 additions and 36 deletions

View File

@ -136,6 +136,30 @@ static void compute_s1poe(struct kvm_vcpu *vcpu, struct s1_walk_info *wi)
wi->e0poe = (wi->regime != TR_EL2) && (val & TCR2_EL1_E0POE);
}
#define _has_tgran(__r, __sz) \
({ \
u64 _s1, _mmfr0 = __r; \
\
_s1 = SYS_FIELD_GET(ID_AA64MMFR0_EL1, \
TGRAN##__sz, _mmfr0); \
\
_s1 != ID_AA64MMFR0_EL1_TGRAN##__sz##_NI; \
})
static bool has_tgran(u64 mmfr0, unsigned int shift)
{
switch (shift) {
case 12:
return _has_tgran(mmfr0, 4);
case 14:
return _has_tgran(mmfr0, 16);
case 16:
return _has_tgran(mmfr0, 64);
default:
BUG();
}
}
static unsigned int tcr_to_tg0_pgshift(u64 tcr)
{
u64 tg0 = tcr & TCR_TG0_MASK;
@ -166,8 +190,23 @@ static unsigned int tcr_to_tg1_pgshift(u64 tcr)
}
}
static unsigned int tcr_tg_pgshift(u64 tcr, bool upper_range)
static unsigned int fallback_tgran_shift(u64 mmfr0)
{
if (has_tgran(mmfr0, PAGE_SHIFT))
return PAGE_SHIFT;
else if (has_tgran(mmfr0, 12))
return 12;
else if (has_tgran(mmfr0, 14))
return 14;
else if (has_tgran(mmfr0, 16))
return 16;
else /* Should be unreacheable */
return PAGE_SHIFT;
}
static unsigned int tcr_tg_pgshift(struct kvm *kvm, u64 tcr, bool upper_range)
{
u64 mmfr0 = kvm_read_vm_id_reg(kvm, SYS_ID_AA64MMFR0_EL1);
unsigned int shift;
/* Someone was silly enough to encode TG0/TG1 differently */
@ -176,6 +215,15 @@ static unsigned int tcr_tg_pgshift(u64 tcr, bool upper_range)
else
shift = tcr_to_tg0_pgshift(tcr);
/*
* If TGx is programmed to an unimplemented value (not advertised in
* ID_AA64MMFR0_EL1), we should treat it as if an implemented value is
* written, as per the architecture. Choose an available one while
* prioritizing PAGE_SIZE.
*/
if (!has_tgran(mmfr0, shift))
return fallback_tgran_shift(mmfr0);
return shift;
}
@ -223,7 +271,7 @@ static int setup_s1_walk(struct kvm_vcpu *vcpu, struct s1_walk_info *wi,
else
wi->txsz = FIELD_GET(TCR_T0SZ_MASK, tcr);
wi->pgshift = tcr_tg_pgshift(tcr, upper_range);
wi->pgshift = tcr_tg_pgshift(vcpu->kvm, tcr, upper_range);
wi->pa52bit = has_52bit_pa(vcpu, wi, tcr);
ia_bits = get_ia_size(wi);

View File

@ -378,25 +378,84 @@ static int walk_nested_s2_pgd(struct kvm_vcpu *vcpu, phys_addr_t ipa,
return 0;
}
#define _has_tgran_2(__r, __sz) \
({ \
u64 _s1, _s2, _mmfr0 = __r; \
\
_s2 = SYS_FIELD_GET(ID_AA64MMFR0_EL1, \
TGRAN##__sz##_2, _mmfr0); \
\
_s1 = SYS_FIELD_GET(ID_AA64MMFR0_EL1, \
TGRAN##__sz, _mmfr0); \
\
((_s2 != ID_AA64MMFR0_EL1_TGRAN##__sz##_2_NI && \
_s2 != ID_AA64MMFR0_EL1_TGRAN##__sz##_2_TGRAN##__sz) || \
(_s2 == ID_AA64MMFR0_EL1_TGRAN##__sz##_2_TGRAN##__sz && \
_s1 != ID_AA64MMFR0_EL1_TGRAN##__sz##_NI)); \
})
static unsigned int vtcr_to_tg0_pgshift(u64 vtcr)
static bool has_tgran_2(u64 mmfr0, unsigned int shift)
{
u64 tg0 = FIELD_GET(VTCR_EL2_TG0_MASK, vtcr);
switch (tg0) {
case VTCR_EL2_TG0_4K:
return 12;
case VTCR_EL2_TG0_16K:
return 14;
case VTCR_EL2_TG0_64K:
default: /* IMPDEF: treat any other value as 64k */
return 16;
switch (shift) {
case 12:
return _has_tgran_2(mmfr0, 4);
case 14:
return _has_tgran_2(mmfr0, 16);
case 16:
return _has_tgran_2(mmfr0, 64);
default:
BUG();
}
}
static size_t vtcr_to_tg0_pgsize(u64 vtcr)
static unsigned int fallback_tgran2_shift(u64 mmfr0)
{
return BIT(vtcr_to_tg0_pgshift(vtcr));
if (has_tgran_2(mmfr0, PAGE_SHIFT))
return PAGE_SHIFT;
else if (has_tgran_2(mmfr0, 12))
return 12;
else if (has_tgran_2(mmfr0, 14))
return 14;
else if (has_tgran_2(mmfr0, 16))
return 16;
else
return PAGE_SHIFT;
}
static unsigned int vtcr_to_tg0_pgshift(struct kvm *kvm, u64 vtcr)
{
u64 tg0 = FIELD_GET(VTCR_EL2_TG0_MASK, vtcr);
u64 mmfr0 = kvm_read_vm_id_reg(kvm, SYS_ID_AA64MMFR0_EL1);
unsigned int shift;
switch (tg0) {
case VTCR_EL2_TG0_4K:
shift = 12;
break;
case VTCR_EL2_TG0_16K:
shift = 14;
break;
case VTCR_EL2_TG0_64K:
/* IMPDEF: treat any other value as 64k, subject to fallback */
default:
shift = 16;
}
/*
* If TGx is programmed to an unimplemented value (not advertised in
* ID_AA64MMFR0_EL1), we should treat it as if an implemented value is
* written, as per the architecture. Choose an available one while
* prioritizing PAGE_SIZE.
*/
if (!has_tgran_2(mmfr0, shift))
return fallback_tgran2_shift(mmfr0);
return shift;
}
static size_t vtcr_to_tg0_pgsize(struct kvm *kvm, u64 vtcr)
{
return BIT(vtcr_to_tg0_pgshift(kvm, vtcr));
}
static void setup_s2_walk(struct kvm_vcpu *vcpu, struct s2_walk_info *wi)
@ -405,7 +464,7 @@ static void setup_s2_walk(struct kvm_vcpu *vcpu, struct s2_walk_info *wi)
wi->baddr = vcpu_read_sys_reg(vcpu, VTTBR_EL2);
wi->t0sz = vtcr & VTCR_EL2_T0SZ_MASK;
wi->pgshift = vtcr_to_tg0_pgshift(vtcr);
wi->pgshift = vtcr_to_tg0_pgshift(vcpu->kvm, vtcr);
wi->sl = FIELD_GET(VTCR_EL2_SL0_MASK, vtcr);
/* Global limit for now, should eventually be per-VM */
wi->max_oa_bits = min(get_kvm_ipa_limit(),
@ -521,10 +580,10 @@ static u8 pgshift_level_to_ttl(u16 shift, u8 level)
*/
static u8 get_guest_mapping_ttl(struct kvm_s2_mmu *mmu, u64 addr)
{
size_t tg0_size = vtcr_to_tg0_pgsize(kvm_s2_mmu_to_kvm(mmu), mmu->tlb_vtcr);
u64 tmp, sz = 0;
kvm_pte_t pte;
u8 ttl, level;
size_t tg0_size = vtcr_to_tg0_pgsize(mmu->tlb_vtcr);
lockdep_assert_held_write(&kvm_s2_mmu_to_kvm(mmu)->mmu_lock);
@ -608,7 +667,7 @@ unsigned long compute_tlb_inval_range(struct kvm_s2_mmu *mmu, u64 val)
if (!max_size) {
/* Compute the maximum extent of the invalidation */
switch (vtcr_to_tg0_pgsize(mmu->tlb_vtcr)) {
switch (vtcr_to_tg0_pgsize(kvm, mmu->tlb_vtcr)) {
case SZ_4K:
max_size = SZ_1G;
break;
@ -1508,21 +1567,6 @@ static void kvm_map_l1_vncr(struct kvm_vcpu *vcpu)
}
}
#define has_tgran_2(__r, __sz) \
({ \
u64 _s1, _s2, _mmfr0 = __r; \
\
_s2 = SYS_FIELD_GET(ID_AA64MMFR0_EL1, \
TGRAN##__sz##_2, _mmfr0); \
\
_s1 = SYS_FIELD_GET(ID_AA64MMFR0_EL1, \
TGRAN##__sz, _mmfr0); \
\
((_s2 != ID_AA64MMFR0_EL1_TGRAN##__sz##_2_NI && \
_s2 != ID_AA64MMFR0_EL1_TGRAN##__sz##_2_TGRAN##__sz) || \
(_s2 == ID_AA64MMFR0_EL1_TGRAN##__sz##_2_TGRAN##__sz && \
_s1 != ID_AA64MMFR0_EL1_TGRAN##__sz##_NI)); \
})
/*
* Our emulated CPU doesn't support all the possible features. For the
* sake of simplicity (and probably mental sanity), wipe out a number
@ -1609,15 +1653,15 @@ u64 limit_nv_id_reg(struct kvm *kvm, u32 reg, u64 val)
*/
switch (PAGE_SIZE) {
case SZ_4K:
if (has_tgran_2(orig_val, 4))
if (_has_tgran_2(orig_val, 4))
val |= SYS_FIELD_PREP_ENUM(ID_AA64MMFR0_EL1, TGRAN4_2, IMP);
fallthrough;
case SZ_16K:
if (has_tgran_2(orig_val, 16))
if (_has_tgran_2(orig_val, 16))
val |= SYS_FIELD_PREP_ENUM(ID_AA64MMFR0_EL1, TGRAN16_2, IMP);
fallthrough;
case SZ_64K:
if (has_tgran_2(orig_val, 64))
if (_has_tgran_2(orig_val, 64))
val |= SYS_FIELD_PREP_ENUM(ID_AA64MMFR0_EL1, TGRAN64_2, IMP);
break;
}