[PATCH rc v4 1/5] iommu/arm-smmu-v3: Add arm_smmu_kdump_adopt_strtab() for kdump

Nicolin Chen
2026-04-29T07:22:06Z

When transitioning to a kdump kernel, the primary kernel might have crashed
while endpoint devices were actively bus-mastering DMA. Currently, the SMMU
driver aggressively resets the hardware during probe by clearing CR0_SMMUEN
and setting the Global Bypass Attribute (GBPA) to ABORT.

In a kdump scenario, this aggressive reset is highly destructive:
a) If GBPA is set to ABORT, in-flight DMA will be aborted, generating fatal
PCIe AER or SErrors that may panic the kdump kernel
b) If GBPA is set to BYPASS, in-flight DMA targeting some IOVAs will bypass
the SMMU and corrupt the physical memory at those 1:1 mapped IOVAs.

To safely absorb in-flight DMAs, a kdump kernel will have to leave SMMUEN=1
intact and avoid modifying STRTAB_BASE, allowing HW to continue translating
in-flight DMAs reusing the crashed kernel's page tables until the endpoint
device drivers probe and quiesce their respective hardware.

However, the ARM SMMUv3 architecture specification states that updating the
SMMU_STRTAB_BASE register while SMMUEN == 1 is UNPREDICTABLE or ignored.

This leaves a kdump kernel no choice but to adopt the stream table from the
crashed kernel.

Introduce ARM_SMMU_OPT_KDUMP_ADOPT and adopt functions memremapping all the
stream tables extracted from STRTAB_BASE and STRTAB_BASE_CFG.

Note that the adoption of the crashed kernel's stream table follows certain
strict rules, since the old stream table might be compromised. Thus, apply
a series of validations against the values read from the registers. If any
address or size doesn't pass the test, it means the stream table cannot be
trusted, so toss it entirely. To avoid OOM due to a deeply corrupted stream
table, the memremap for l2 tables is done on the kdump kernel's demand.

The new option will be set in a following change.

Fixes: b63b3439b856 ("iommu/arm-smmu-v3: Abort all transactions if SMMU is enabled in kdump kernel")
Cc: stable@vger.kernel.org # v6.12+
Suggested-by: Jason Gunthorpe <jgg@nvidia.com>
Signed-off-by: Nicolin Chen <nicolinc@nvidia.com>
---
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h | 1 +
drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c | 304 +++++++++++++++++++-
2 files changed, 302 insertions(+), 3 deletions(-)

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 ef42df4753ec4..cd60b692c3901 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
@@ -861,6 +861,7 @@ struct arm_smmu_device {
#define ARM_SMMU_OPT_MSIPOLL (1 << 2)
#define ARM_SMMU_OPT_CMDQ_FORCE_SYNC (1 << 3)
#define ARM_SMMU_OPT_TEGRA241_CMDQV (1 << 4)
+#define ARM_SMMU_OPT_KDUMP_ADOPT (1 << 5)
u32 options;

struct arm_smmu_cmdq cmdq;
diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
index e8d7dbe495f03..fbc0fa6f182c6 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
@@ -14,6 +14,7 @@
#include <linux/bitops.h>
#include <linux/crash_dump.h>
#include <linux/delay.h>
+#include <linux/dma-direct.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/io-pgtable.h>
@@ -2040,16 +2041,110 @@ static void arm_smmu_init_initial_stes(struct arm_smmu_ste *strtab,
}
}

+/*
+ * Adopting the crashed kernel's stream table has risks: the physical addresses
+ * read from ARM_SMMU_STRTAB_BASE / L1 descriptors may be corrupted. Reject any
+ * range that overlaps the kdump kernel's critical regions.
+ */
+static bool arm_smmu_kdump_phys_is_corrupted(phys_addr_t base, size_t size)
+{
+ /* Must NOT overlap kdump kernel's own RAM -- silent corruption */
+ if (region_intersects(base, size, IORESOURCE_SYSTEM_RAM,
+ IORES_DESC_NONE) != REGION_DISJOINT)
+ return true;
+
+ /*
+ * Must NOT overlap any MMIO region -- fatal SError
+ *
+ * Note that a false positive is possible on platforms that register the
+ * reserved-memory regions where the crashed kernel's stream table might
+ * legitimately reside. The cost of a false reject will be a fallback to
+ * full reset (recoverable), while a missed MMIO mapping will be fatal.
+ */
+ if (region_intersects(base, size, IORESOURCE_MEM, IORES_DESC_NONE) !=
+ REGION_DISJOINT)
+ return true;
+
+ /*
+ * Note: physical holes are absent from iomem_resource, so a corrupted
+ * address pointing into one will not be caught here. Closing that gap
+ * requires a firmware memory map and is left as a future improvement.
+ */
+ return false;
+}
+
+static int arm_smmu_kdump_adopt_l2_strtab(struct arm_smmu_device *smmu, u32 sid,
+ u32 l1_idx, u64 l2_dma, u32 span,
+ struct arm_smmu_strtab_l2 **l2table)
+{
+ phys_addr_t base = dma_to_phys(smmu->dev, l2_dma);
+ struct arm_smmu_strtab_l2 *table;
+ size_t size;
+
+ /*
+ * Only a coherent SMMU is supported at this moment. For a non-coherent
+ * SMMU that wants to support ARM_SMMU_OPT_KDUMP_ADOPT, try MEMREMAP_WC.
+ */
+ if (WARN_ON(!(smmu->features & ARM_SMMU_FEAT_COHERENCY)))
+ return -EOPNOTSUPP;
+
+ /*
+ * Retest the memremap inputs in case the L1 descriptor was overwritten
+ * since adopt. Reject this master's insert; panic or SMMU-disable would
+ * either lose the vmcore or cascade aborts. Do not try to fix it, as it
+ * would break all other SIDs in the same bus (PCI case). The corruption
+ * blast radius is already bounded to that bus range.
+ */
+ if (span != STRTAB_SPLIT + 1) {
+ dev_err(smmu->dev,
+ "kdump: L1[%u] span %u changed since adopt (was %u)\n",
+ l1_idx, span, STRTAB_SPLIT + 1);
+ return -EINVAL;
+ }
+
+ size = (1UL << (span - 1)) * sizeof(struct arm_smmu_ste);
+ if (arm_smmu_kdump_phys_is_corrupted(base, size)) {
+ dev_err(smmu->dev,
+ "kdump: L1[%u] now points at a corrupt range\n",
+ l1_idx);
+ return -EINVAL;
+ }
+
+ table = devm_memremap(smmu->dev, base, size, MEMREMAP_WB);
+ if (IS_ERR(table)) {
+ dev_err(smmu->dev,
+ "kdump: failed to adopt l2 stream table for SID %u\n",
+ sid);
+ return PTR_ERR(table);
+ }
+
+ *l2table = table;
+ return 0;
+}
+
static int arm_smmu_init_l2_strtab(struct arm_smmu_device *smmu, u32 sid)
{
dma_addr_t l2ptr_dma;
struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
struct arm_smmu_strtab_l2 **l2table;
+ u32 l1_idx = arm_smmu_strtab_l1_idx(sid);

- l2table = &cfg->l2.l2ptrs[arm_smmu_strtab_l1_idx(sid)];
+ l2table = &cfg->l2.l2ptrs[l1_idx];
if (*l2table)
return 0;

+ /* Deferred adoption of the crashed kernel's L2 table */
+ if (smmu->options & ARM_SMMU_OPT_KDUMP_ADOPT) {
+ /* L1 entry is shared with the SMMU and possibly rogue DMA */
+ u64 l2ptr = le64_to_cpu(READ_ONCE(cfg->l2.l1tab[l1_idx].l2ptr));
+ dma_addr_t l2_dma = l2ptr & STRTAB_L1_DESC_L2PTR_MASK;
+ u32 span = FIELD_GET(STRTAB_L1_DESC_SPAN, l2ptr);
+
+ if (span && l2_dma)
+ return arm_smmu_kdump_adopt_l2_strtab(
+ smmu, sid, l1_idx, l2_dma, span, l2table);
+ }
+
*l2table = dmam_alloc_coherent(smmu->dev, sizeof(**l2table),
&l2ptr_dma, GFP_KERNEL);
if (!*l2table) {
@@ -2061,8 +2156,7 @@ static int arm_smmu_init_l2_strtab(struct arm_smmu_device *smmu, u32 sid)

arm_smmu_init_initial_stes((*l2table)->stes,
ARRAY_SIZE((*l2table)->stes));
- arm_smmu_write_strtab_l1_desc(&cfg->l2.l1tab[arm_smmu_strtab_l1_idx(sid)],
- l2ptr_dma);
+ arm_smmu_write_strtab_l1_desc(&cfg->l2.l1tab[l1_idx], l2ptr_dma);
return 0;
}

@@ -4556,10 +4650,213 @@ static int arm_smmu_init_strtab_linear(struct arm_smmu_device *smmu)
return 0;
}

+static int arm_smmu_kdump_adopt_strtab_2lvl(struct arm_smmu_device *smmu,
+ u32 cfg_reg, dma_addr_t dma)
+{
+ u32 log2size = FIELD_GET(STRTAB_BASE_CFG_LOG2SIZE, cfg_reg);
+ u32 split = FIELD_GET(STRTAB_BASE_CFG_SPLIT, cfg_reg);
+ struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
+ phys_addr_t base;
+ u32 num_l1_ents;
+ size_t size;
+ int i;
+
+ /*
+ * Only a coherent SMMU is supported at this moment. For a non-coherent
+ * SMMU that wants to support ARM_SMMU_OPT_KDUMP_ADOPT, try MEMREMAP_WC.
+ */
+ if (WARN_ON(!(smmu->features & ARM_SMMU_FEAT_COHERENCY)))
+ return -EOPNOTSUPP;
+
+ if (log2size < split || log2size > smmu->sid_bits) {
+ dev_err(smmu->dev, "kdump: log2size %u out of range [%u, %u]\n",
+ log2size, split, smmu->sid_bits);
+ return -EINVAL;
+ }
+ if (split != STRTAB_SPLIT) {
+ dev_err(smmu->dev,
+ "kdump: unsupported STRTAB_SPLIT %u (expected %u)\n",
+ split, STRTAB_SPLIT);
+ return -EINVAL;
+ }
+
+ num_l1_ents = 1U << (log2size - split);
+ if (num_l1_ents > STRTAB_MAX_L1_ENTRIES) {
+ dev_err(smmu->dev, "kdump: l1 entries %u exceeds max %u\n",
+ num_l1_ents, STRTAB_MAX_L1_ENTRIES);
+ return -EINVAL;
+ }
+
+ cfg->l2.l1_dma = dma;
+ cfg->l2.num_l1_ents = num_l1_ents;
+
+ base = dma_to_phys(smmu->dev, dma);
+ size = num_l1_ents * sizeof(struct arm_smmu_strtab_l1);
+ if (arm_smmu_kdump_phys_is_corrupted(base, size)) {
+ dev_err(smmu->dev, "kdump: l1 stream table is corrupted\n");
+ return -EINVAL;
+ }
+
+ cfg->l2.l1tab = devm_memremap(smmu->dev, base, size, MEMREMAP_WB);
+ if (IS_ERR(cfg->l2.l1tab))
+ return PTR_ERR(cfg->l2.l1tab);
+
+ cfg->l2.l2ptrs = devm_kcalloc(smmu->dev, num_l1_ents,
+ sizeof(*cfg->l2.l2ptrs), GFP_KERNEL);
+ if (!cfg->l2.l2ptrs)
+ return -ENOMEM;
+
+ for (i = 0; i < num_l1_ents; i++) {
+ u64 l2ptr = le64_to_cpu(cfg->l2.l1tab[i].l2ptr);
+ dma_addr_t l2_dma = l2ptr & STRTAB_L1_DESC_L2PTR_MASK;
+ u32 span = FIELD_GET(STRTAB_L1_DESC_SPAN, l2ptr);
+
+ if (!span || !l2_dma)
+ continue;
+
+ if (span != STRTAB_SPLIT + 1) {
+ dev_err(smmu->dev,
+ "kdump: L1[%u] unsupported span %u (vs %u)\n",
+ i, span, STRTAB_SPLIT + 1);
+ return -EINVAL;
+ }
+
+ base = dma_to_phys(smmu->dev, l2_dma);
+ size = (1UL << (span - 1)) * sizeof(struct arm_smmu_ste);
+ if (arm_smmu_kdump_phys_is_corrupted(base, size)) {
+ dev_err(smmu->dev,
+ "kdump: l2 stream table is corrupted\n");
+ return -EINVAL;
+ }
+
+ /*
+ * If the crashed kernel's l1 descriptors are deeply corrupted,
+ * blindly memremapping every l2 table here could lead to OOM.
+ *
+ * Defer the l2 memremap to arm_smmu_init_l2_strtab(), so peak
+ * memory is bounded by the kdump kernel's actual demand.
+ */
+ }
+
+ return 0;
+}
+
+static int arm_smmu_kdump_adopt_strtab_linear(struct arm_smmu_device *smmu,
+ u32 cfg_reg, dma_addr_t dma)
+{
+ u32 log2size = FIELD_GET(STRTAB_BASE_CFG_LOG2SIZE, cfg_reg);
+ struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
+ unsigned int max_log2size;
+ phys_addr_t base;
+ size_t size;
+
+ /*
+ * Only a coherent SMMU is supported at this moment. For a non-coherent
+ * SMMU that wants to support ARM_SMMU_OPT_KDUMP_ADOPT, try MEMREMAP_WC.
+ */
+ if (WARN_ON(!(smmu->features & ARM_SMMU_FEAT_COHERENCY)))
+ return -EOPNOTSUPP;
+
+ /* Cap the size at what the kdump kernel itself would have allocated */
+ if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)
+ max_log2size =
+ ilog2(STRTAB_MAX_L1_ENTRIES * STRTAB_NUM_L2_STES);
+ else
+ max_log2size = smmu->sid_bits;
+
+ /* cfg->linear.num_ents is unsigned int, so cap log2size at 31 */
+ max_log2size = min(max_log2size, 31U);
+ if (log2size > max_log2size) {
+ dev_err(smmu->dev, "kdump: unsupported log2size %u (> %u)\n",
+ log2size, max_log2size);
+ return -EINVAL;
+ }
+
+ /*
+ * We might end up with a num_ents != sid_bits, which is fine. In the
+ * ARM_SMMU_OPT_KDUMP_ADOPT case, arm_smmu_write_strtab() is bypassed.
+ */
+ cfg->linear.num_ents = 1U << log2size;
+ cfg->linear.ste_dma = dma;
+
+ base = dma_to_phys(smmu->dev, dma);
+ size = cfg->linear.num_ents * sizeof(struct arm_smmu_ste);
+ if (arm_smmu_kdump_phys_is_corrupted(base, size)) {
+ dev_err(smmu->dev, "kdump: stream table is corrupted\n");
+ return -EINVAL;
+ }
+
+ cfg->linear.table = devm_memremap(smmu->dev, base, size, MEMREMAP_WB);
+ if (IS_ERR(cfg->linear.table))
+ return PTR_ERR(cfg->linear.table);
+ return 0;
+}
+
+static void arm_smmu_kdump_adopt_cleanup(struct arm_smmu_device *smmu, u32 fmt)
+{
+ struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
+
+ if (fmt == STRTAB_BASE_CFG_FMT_2LVL) {
+ if (cfg->l2.l2ptrs)
+ devm_kfree(smmu->dev, cfg->l2.l2ptrs);
+ if (!IS_ERR_OR_NULL(cfg->l2.l1tab))
+ devm_memunmap(smmu->dev, cfg->l2.l1tab);
+ } else if (fmt == STRTAB_BASE_CFG_FMT_LINEAR) {
+ if (!IS_ERR_OR_NULL(cfg->linear.table))
+ devm_memunmap(smmu->dev, cfg->linear.table);
+ }
+}
+
+static int arm_smmu_kdump_adopt_strtab(struct arm_smmu_device *smmu)
+{
+ u32 cfg_reg = readl_relaxed(smmu->base + ARM_SMMU_STRTAB_BASE_CFG);
+ u64 base_reg = readq_relaxed(smmu->base + ARM_SMMU_STRTAB_BASE);
+ u32 fmt = FIELD_GET(STRTAB_BASE_CFG_FMT, cfg_reg);
+ dma_addr_t dma = base_reg & STRTAB_BASE_ADDR_MASK;
+ int ret;
+
+ dev_info(smmu->dev, "kdump: adopting crashed kernel's stream table\n");
+
+ if (fmt == STRTAB_BASE_CFG_FMT_2LVL) {
+ /*
+ * Both kernels run on the same hardware, so it's impossible for
+ * kdump kernel to see the support for linear stream table only.
+ */
+ if (WARN_ON(!(smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)))
+ ret = -EINVAL;
+ else
+ ret = arm_smmu_kdump_adopt_strtab_2lvl(smmu, cfg_reg,
+ dma);
+ } else if (fmt == STRTAB_BASE_CFG_FMT_LINEAR) {
+ /*
+ * In case that the old kernel for some reason used the linear
+ * format, enforce the same format to match the adopted table.
+ */
+ ret = arm_smmu_kdump_adopt_strtab_linear(smmu, cfg_reg, dma);
+ if (!ret)
+ smmu->features &= ~ARM_SMMU_FEAT_2_LVL_STRTAB;
+ } else {
+ dev_err(smmu->dev, "kdump: invalid STRTAB format %u\n", fmt);
+ ret = -EINVAL;
+ }
+
+ if (ret) {
+ dev_warn(smmu->dev, "kdump: falling back to full reset\n");
+ arm_smmu_kdump_adopt_cleanup(smmu, fmt);
+ smmu->options &= ~ARM_SMMU_OPT_KDUMP_ADOPT;
+ memset(&smmu->strtab_cfg, 0, sizeof(smmu->strtab_cfg));
+ }
+ return ret;
+}
+
static int arm_smmu_init_strtab(struct arm_smmu_device *smmu)
{
int ret;

+ if ((smmu->options & ARM_SMMU_OPT_KDUMP_ADOPT) &&
+ !arm_smmu_kdump_adopt_strtab(smmu))
+ goto out;
+
if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)
ret = arm_smmu_init_strtab_2lvl(smmu);
else
@@ -4567,6 +4864,7 @@ static int arm_smmu_init_strtab(struct arm_smmu_device *smmu)
if (ret)
return ret;

+out:
ida_init(&smmu->vmid_map);

return 0;
--
2.43.0