[PATCH v10 08/13] iommu/arm-smmu-v3-kexec: Add ASID/VMID reservation helpers
From: Nicolin Chen
Date: Sun Aug 30 2026 - 19:19:35 EST
An adopted stream table keeps translating in-flight DMAs, so the SMMU also
keeps caching the TLB entries tagged with the previous kernel's ASIDs and
VMIDs. Any kexec flavor retaining those translations must reserve all the
in-use IDs, so that this kernel cannot ever give any of its own domains an
overlapping ID that would alias the previous kernel's cached TLB entries.
Add the reservation helpers to arm-smmu-v3-kexec.c:
- arm_smmu_kexec_scan_and_resv_ids()
- arm_smmu_kexec_unresv_ids()
At adoption time, arm_smmu_kexec_scan_and_resv_ids() runs a one-off scan:
walking the stream table set up in the strtab_cfg and every CD table behind
an S1 STE, it reserves the ASIDs in the asid xarray and the S2VMIDs in the
vmid ida. The scan memremaps each table transiently, which is a necessity
even for a live update: the ID reservation must be complete by the end of
the SMMU probe itself, before this kernel starts allocating any ID for its
own domains, yet the CD tables are only claimed when their masters get to
re-probe, long after that point. Thus, the scan cannot rely on any claimed
mapping and must map each table by itself.
The scan walks untrusted tables, yet every loop is strictly index-bounded:
the iteration counts derive from the log2size and s1cdmax fields, which are
validated against this kernel's own sid_bits and ssid_bits, so a corrupted
table cannot extend the walk.
The ASID space is per SMMU instance and the scan runs before its own SMMU
registers with the IOMMU core, i.e. before any domain of that SMMU can hold
an ASID. So it reserves via xa_reserve(), and a failing scan just empties
the xarray and the ida, since they hold nothing else at that point.
Assisted-by: Claude:claude-fable-5
Signed-off-by: Nicolin Chen <nicolinc@xxxxxxxxxx>
---
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h | 2 +
.../iommu/arm/arm-smmu-v3/arm-smmu-v3-kexec.c | 235 ++++++++++++++++++
2 files changed, 237 insertions(+)
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
index 1bcf6cb2ebb3c..64927c54f8b6b 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
@@ -1264,6 +1264,8 @@ int arm_smmu_kexec_check_strtab_l1_desc(struct arm_smmu_device *smmu,
int arm_smmu_kexec_check_ste_cdtab(struct arm_smmu_device *smmu, u64 ste0,
phys_addr_t *cdtab, u32 *s1fmt,
u32 *max_contexts);
+int arm_smmu_kexec_scan_and_resv_ids(struct arm_smmu_device *smmu);
+void arm_smmu_kexec_unresv_ids(struct arm_smmu_device *smmu);
#endif /* CONFIG_ARM_SMMU_V3_KEXEC */
#ifdef CONFIG_CRASH_DUMP
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-kexec.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-kexec.c
index 700f63c7972e9..8c53cd7570070 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-kexec.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-kexec.c
@@ -221,3 +221,238 @@ int arm_smmu_kexec_check_ste_cdtab(struct arm_smmu_device *smmu, u64 ste0,
*max_contexts = 1U << s1cdmax;
return 0;
}
+
+static int arm_smmu_kexec_resv_asid(struct arm_smmu_device *smmu, u32 asid)
+{
+ /* A valid CD never has ASID 0; both kernels share the same HW limit */
+ if (!asid || asid >= 1UL << smmu->asid_bits)
+ return -EINVAL;
+
+ guard(mutex)(&arm_smmu_asid_lock);
+
+ /*
+ * The scan runs before this SMMU registers with the IOMMU core, so no
+ * domain of its own holds an ASID yet, while xa_reserve() does nothing
+ * if the entry is there, covering a domain's ASID that many CDs share.
+ */
+ return xa_reserve(&smmu->asid_map, asid, GFP_KERNEL);
+}
+
+static int arm_smmu_kexec_resv_vmid(struct arm_smmu_device *smmu, u32 vmid)
+{
+ int ret;
+
+ /* A translating STE never has VMID 0, which is reserved for bypass */
+ if (!vmid || vmid >= 1UL << smmu->vmid_bits)
+ return -EINVAL;
+
+ ret = ida_alloc_range(&smmu->vmid_map, vmid, vmid, GFP_KERNEL);
+ if (ret < 0 && ret != -ENOSPC) /* -ENOSPC means already reserved */
+ return ret;
+ return 0;
+}
+
+static int arm_smmu_kexec_resv_cd_asids(struct arm_smmu_device *smmu,
+ struct arm_smmu_cd *cds, u32 num_cds)
+{
+ int ret = 0;
+ u32 i;
+
+ for (i = 0; i < num_cds; i++) {
+ u64 val = le64_to_cpu(cds[i].data[0]);
+ u32 asid = FIELD_GET(CTXDESC_CD_0_ASID, val);
+
+ if (!(val & CTXDESC_CD_0_V))
+ continue;
+ ret = arm_smmu_kexec_resv_asid(smmu, asid);
+ if (ret)
+ break;
+ }
+ return ret;
+}
+
+/*
+ * Reserve the ASIDs of all the valid CDs of an S1 STE in the previous kernel's
+ * CD tables. The CD tables are transiently memremapped for the scan.
+ */
+static int arm_smmu_kexec_resv_s1_asids(struct arm_smmu_device *smmu, u64 ste0)
+{
+ struct arm_smmu_cdtab_l1 *l1tab;
+ u32 num_l1_ents, num_cds, i;
+ u32 max_contexts, s1fmt;
+ phys_addr_t cdtab;
+ int ret;
+
+ ret = arm_smmu_kexec_check_ste_cdtab(smmu, ste0, &cdtab, &s1fmt,
+ &max_contexts);
+ if (ret)
+ return ret;
+
+ if (s1fmt == STRTAB_STE_0_S1FMT_LINEAR) {
+ struct arm_smmu_cd *cds;
+
+ cds = memremap(cdtab, max_contexts * sizeof(*cds), MEMREMAP_WB);
+ if (!cds)
+ return -ENOMEM;
+ ret = arm_smmu_kexec_resv_cd_asids(smmu, cds, max_contexts);
+ memunmap(cds);
+ return ret;
+ }
+
+ num_l1_ents = DIV_ROUND_UP(max_contexts, CTXDESC_L2_ENTRIES);
+ l1tab = memremap(cdtab, num_l1_ents * sizeof(*l1tab), MEMREMAP_WB);
+ if (!l1tab)
+ return -ENOMEM;
+
+ /* max_contexts being under a full leaf makes the only leaf partial */
+ num_cds = min_t(u32, max_contexts, CTXDESC_L2_ENTRIES);
+
+ /* Aliased L2 tables cannot extend the walk; they only repeat a scan */
+ for (i = 0; i < num_l1_ents; i++) {
+ u64 l1_desc = le64_to_cpu(l1tab[i].l2ptr);
+ phys_addr_t l2_base = l1_desc & CTXDESC_L1_DESC_L2PTR_MASK;
+ struct arm_smmu_cdtab_l2 *l2;
+
+ if (!(l1_desc & CTXDESC_L1_DESC_V))
+ continue;
+
+ /*
+ * A valid descriptor never carries a null pointer. Also, an L2
+ * table is always 64KB-aligned, so an unaligned pointer would
+ * make this kernel read a different table.
+ */
+ if (!l2_base || !IS_ALIGNED(l2_base, sizeof(*l2))) {
+ ret = -EINVAL;
+ break;
+ }
+
+ l2 = memremap(l2_base, num_cds * sizeof(*l2->cds), MEMREMAP_WB);
+ if (!l2) {
+ ret = -ENOMEM;
+ break;
+ }
+ ret = arm_smmu_kexec_resv_cd_asids(smmu, l2->cds, num_cds);
+ memunmap(l2);
+ if (ret)
+ break;
+ }
+ memunmap(l1tab);
+ return ret;
+}
+
+static int arm_smmu_kexec_resv_ste_ids(struct arm_smmu_device *smmu,
+ struct arm_smmu_ste *ste)
+{
+ u32 vmid = FIELD_GET(STRTAB_STE_2_S2VMID, le64_to_cpu(ste->data[2]));
+ u64 ste0 = le64_to_cpu(ste->data[0]);
+
+ if (!(ste0 & STRTAB_STE_0_V))
+ return 0;
+
+ switch (FIELD_GET(STRTAB_STE_0_CFG, ste0)) {
+ case STRTAB_STE_0_CFG_ABORT:
+ case STRTAB_STE_0_CFG_BYPASS:
+ return 0;
+ case STRTAB_STE_0_CFG_S1_TRANS:
+ return arm_smmu_kexec_resv_s1_asids(smmu, ste0);
+ case STRTAB_STE_0_CFG_NESTED:
+ /*
+ * A guest-owned CD table is in the IPA space, unreachable. Its
+ * ASIDs are only tagged with the S2VMID reserved below, so they
+ * cannot alias this kernel's VMID-0 or EL2 S1 domains.
+ */
+ fallthrough;
+ case STRTAB_STE_0_CFG_S2_TRANS:
+ return arm_smmu_kexec_resv_vmid(smmu, vmid);
+ default:
+ return -EINVAL;
+ }
+}
+
+/**
+ * arm_smmu_kexec_scan_and_resv_ids() - Reserve a stream table's in-use IDs
+ * @smmu: SMMU device of this kernel, with an adopted or restored strtab_cfg
+ *
+ * Scan the stream table set up in the strtab_cfg and every CD table behind an
+ * S1 STE, reserving all of the in-use ASIDs and VMIDs. A failing scan rolls
+ * back through arm_smmu_kexec_unresv_ids().
+ *
+ * Note that the scan selects the linear or 2-level walk per this kernel's own
+ * ARM_SMMU_FEAT_2_LVL_STRTAB, so the caller must have matched the feature bit
+ * to the format of the adopted stream table in the strtab_cfg.
+ *
+ * Return: 0 on success, -EINVAL on any malformed table entry, or -ENOMEM on a
+ * memory shortage
+ */
+int arm_smmu_kexec_scan_and_resv_ids(struct arm_smmu_device *smmu)
+{
+ struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
+ int ret = 0;
+ u32 i, j;
+
+ if (!(smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)) {
+ for (i = 0; i < cfg->linear.num_ents; i++) {
+ ret = arm_smmu_kexec_resv_ste_ids(
+ smmu, &cfg->linear.table[i]);
+ if (ret)
+ return ret;
+ }
+ return 0;
+ }
+
+ /* Aliased L2 tables cannot extend the scan; they only repeat a scan */
+ for (i = 0; i < cfg->l2.num_l1_ents; i++) {
+ u64 l1_desc = le64_to_cpu(cfg->l2.l1tab[i].l2ptr);
+ struct arm_smmu_strtab_l2 *l2;
+ phys_addr_t base;
+
+ ret = arm_smmu_kexec_check_strtab_l1_desc(smmu, l1_desc, i,
+ &base);
+ if (ret == 1)
+ continue;
+ if (ret)
+ return ret;
+
+ /*
+ * This kernel will map the previous kernel's L2 tables lazily
+ * or not at all. Here, take a transient view for this scan.
+ */
+ l2 = memremap(base, sizeof(*l2), MEMREMAP_WB);
+ if (!l2)
+ return -ENOMEM;
+ for (j = 0; j < ARRAY_SIZE(l2->stes); j++) {
+ ret = arm_smmu_kexec_resv_ste_ids(smmu, &l2->stes[j]);
+ if (ret)
+ break;
+ }
+ memunmap(l2);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/**
+ * arm_smmu_kexec_unresv_ids() - Release the IDs that a failing scan reserved
+ * @smmu: SMMU device of this kernel that failed its reservation scan
+ *
+ * Undo the reservations of a failing arm_smmu_kexec_scan_and_resv_ids() call,
+ * for a caller that falls back to a full reset.
+ *
+ * That reset flushes the whole TLB, so the previous kernel's IDs no longer need
+ * any protection. A scan that fails halfway would otherwise keep a good share
+ * of an 8-bit ASID or VMID space reserved for nothing.
+ */
+void arm_smmu_kexec_unresv_ids(struct arm_smmu_device *smmu)
+{
+ /*
+ * Emptying both maps releases exactly this scan's IDs, as no domain of
+ * this SMMU can hold one until it registers with the IOMMU core, later
+ * in the probe. Both stay initialized and usable for the full reset.
+ */
+ mutex_lock(&arm_smmu_asid_lock);
+ xa_destroy(&smmu->asid_map);
+ mutex_unlock(&arm_smmu_asid_lock);
+
+ ida_destroy(&smmu->vmid_map);
+}
--
2.43.0