[PATCH rc v1 1/4] iommu/arm-smmu-v3: Add arm_smmu_adopt_strtab() for kdump

Nicolin Chen
2026-04-09T19:47:32Z

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 DMA, the kdump kernel must leave SMMUEN=1 intact
and avoid modifying STRTAB_BASE. This allows HW to continue translating in-
flight DMA using 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 and arm_smmu_adopt_strtab() that does memremap
on all the stream tables extracted from STRTAB_BASE and STRTAB_BASE_CFG.

The option will be set in arm_smmu_device_hw_probe().

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 | 99 ++++++++++++++++++++-
2 files changed, 99 insertions(+), 1 deletion(-)

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..74950d98ba09f 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 (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 f6901c5437edc..8a1de3a67f78c 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
@@ -4553,11 +4553,108 @@ static int arm_smmu_init_strtab_linear(struct arm_smmu_device *smmu)
return 0;
}

+static int arm_smmu_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;
+ u32 num_l1_ents;
+ int i;
+
+ if (log2size < split) {
+ dev_err(smmu->dev, "kdump: invalid log2size %u < split %u\n",
+ log2size, split);
+ 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 = 1 << (log2size - split);
+ cfg->l2.l1_dma = dma;
+ cfg->l2.num_l1_ents = num_l1_ents;
+ cfg->l2.l1tab = devm_memremap(
+ smmu->dev, dma, num_l1_ents * sizeof(struct arm_smmu_strtab_l1),
+ MEMREMAP_WB);
+ if (!cfg->l2.l1tab)
+ return -ENOMEM;
+
+ 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);
+ u32 span = FIELD_GET(STRTAB_L1_DESC_SPAN, l2ptr);
+ dma_addr_t l2_dma = l2ptr & STRTAB_L1_DESC_L2PTR_MASK;
+
+ if (span && l2_dma) {
+ cfg->l2.l2ptrs[i] = devm_memremap(
+ smmu->dev, l2_dma,
+ sizeof(struct arm_smmu_strtab_l2), MEMREMAP_WB);
+ if (!cfg->l2.l2ptrs[i])
+ return -ENOMEM;
+ }
+ }
+
+ return 0;
+}
+
+static int arm_smmu_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;
+
+ cfg->linear.ste_dma = dma;
+ cfg->linear.num_ents = 1 << log2size;
+ cfg->linear.table = devm_memremap(smmu->dev, dma,
+ cfg->linear.num_ents *
+ sizeof(struct arm_smmu_ste),
+ MEMREMAP_WB);
+ if (!cfg->linear.table)
+ return -ENOMEM;
+ return 0;
+}
+
+static int arm_smmu_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) {
+ /* Enforce 2-level feature flag to match the adopted table */
+ smmu->features |= ARM_SMMU_FEAT_2_LVL_STRTAB;
+ ret = arm_smmu_adopt_strtab_2lvl(smmu, cfg_reg, dma);
+ } else if (fmt == STRTAB_BASE_CFG_FMT_LINEAR) {
+ /* Force linear feature flag to match the adopted table */
+ smmu->features &= ~ARM_SMMU_FEAT_2_LVL_STRTAB;
+ ret = arm_smmu_adopt_strtab_linear(smmu, cfg_reg, dma);
+ } else {
+ dev_err(smmu->dev, "kdump: invalid STRTAB format %u\n", fmt);
+ ret = -EINVAL;
+ }
+ return ret;
+}
+
static int arm_smmu_init_strtab(struct arm_smmu_device *smmu)
{
int ret;

- if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)
+ if (smmu->options & ARM_SMMU_OPT_KDUMP)
+ ret = arm_smmu_adopt_strtab(smmu);
+ else if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)
ret = arm_smmu_init_strtab_2lvl(smmu);
else
ret = arm_smmu_init_strtab_linear(smmu);
--
2.43.0