diff options
| author | Wei-Lin Chang <weilin.chang@arm.com> | 2026-04-14 01:03:34 +0100 |
|---|---|---|
| committer | Marc Zyngier <maz@kernel.org> | 2026-05-28 10:20:36 +0100 |
| commit | 8853566dfbab1a255ae72676ab5ec43e1631ddb7 (patch) | |
| tree | f6877e279767443a357c557b39fc549501d96afc /arch | |
| parent | b154da8288add1f6fb958797d0b3462800f9fc77 (diff) | |
| download | linux-next-history-8853566dfbab1a255ae72676ab5ec43e1631ddb7.tar.gz | |
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>
Diffstat (limited to 'arch')
| -rw-r--r-- | arch/arm64/kvm/at.c | 52 | ||||
| -rw-r--r-- | arch/arm64/kvm/nested.c | 100 |
2 files changed, 122 insertions, 30 deletions
diff --git a/arch/arm64/kvm/at.c b/arch/arm64/kvm/at.c index 6ebcf65b4ffaa..60d51e98ccb00 100644 --- a/arch/arm64/kvm/at.c +++ b/arch/arm64/kvm/at.c @@ -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); diff --git a/arch/arm64/kvm/nested.c b/arch/arm64/kvm/nested.c index bc95e43c54dd7..3204b3ef60ddd 100644 --- a/arch/arm64/kvm/nested.c +++ b/arch/arm64/kvm/nested.c @@ -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 bool has_tgran_2(u64 mmfr0, unsigned int shift) +{ + 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 unsigned int fallback_tgran2_shift(u64 mmfr0) +{ + 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(u64 vtcr) +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: - return 12; + shift = 12; + break; case VTCR_EL2_TG0_16K: - return 14; + shift = 14; + break; case VTCR_EL2_TG0_64K: - default: /* IMPDEF: treat any other value as 64k */ - return 16; + /* 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(u64 vtcr) +static size_t vtcr_to_tg0_pgsize(struct kvm *kvm, u64 vtcr) { - return BIT(vtcr_to_tg0_pgshift(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; } |
