]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
KVM: arm64: Fallback to a supported value for unsupported guest TGx
authorWei-Lin Chang <weilin.chang@arm.com>
Tue, 14 Apr 2026 00:03:34 +0000 (01:03 +0100)
committerMarc Zyngier <maz@kernel.org>
Thu, 28 May 2026 09:20:36 +0000 (10:20 +0100)
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>
arch/arm64/kvm/at.c
arch/arm64/kvm/nested.c

index 6ebcf65b4ffaa2a87b733cb4112daf8307a58a68..60d51e98ccb000e8b6ca3eda88d622911f9da528 100644 (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);
index bc95e43c54dd769a82f8c9c8656958f65b8cc33c..3204b3ef60ddd95f145dcf9dc6d3b5e60f61fd86 100644 (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 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;
                }