Subject: [RFC PATCH v2 3/5] iommu/arm-smmu-v3: Add Stream Table Entry display to debugfs

From: Qinxin Xia <[email-masked]>
Date: Sat, 28 Mar 2026 18:17:04 +0800

Add Stream Table Entry (STE) display functionality to debugfs.
This allow inspecting STE contents for each device stream including:
- STE validity and configuration
- Stage 1 and Stage 2 context pointers
- Raw STE data

/sys/kernel/debug/iommu/arm_smmu_v3/smmu0/stream_table/
└── <sid>/
    └─── ste

Signed-off-by: Qinxin Xia <[email-masked]>
---
 .../arm/arm-smmu-v3/arm-smmu-v3-debugfs.c     | 224 +++++++++++++++++-
 drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c   |  18 +-
 drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h   |  17 ++
 3 files changed, 256 insertions(+), 3 deletions(-)

diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-debugfs.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-debugfs.c
index cfd296aebc9f..70623b480d64 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-debugfs.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-debugfs.c
@@ -6,7 +6,10 @@
  * /sys/kernel/debug/iommu/arm_smmu_v3/
  * └── smmu<ioaddr>/
  *     ├── capabilities    # SMMU feature capabilities and configuration
- *     └── registers	   # SMMU Key registers
+ *     ├── registers	   # SMMU Key registers
+ *     └── stream_table
+ *	   └─── <sid>/                                # Stream ID 0
+ *	       └── ste                                # Stream Table Entry
  *
  * The capabilities file provides detailed information about:
  * - translation stage support (Stage1/Stage2)
@@ -17,6 +20,11 @@
  * - CR0, CR1, CR2 control registers
  * - Command and Event queue pointers
  *
+ * The STE Information Displayed:
+ * - STE validity and configuration
+ * - Stage 1 and Stage 2 context pointers
+ * - Raw STE data
+ *
  * Copyright (C) 2026 HiSilicon Limited.
  * Author: Qinxin Xia <[email-masked]>
  */
@@ -162,6 +170,218 @@ static const struct file_operations smmu_debugfs_registers_fops = {
 	.release = smmu_debugfs_registers_release,
 };
 
+/**
+ * smmu_debugfs_ste_show() - Dump STE details to seq_file
+ * @seq: seq_file to write to
+ *
+ * Errors are reported via seq_puts, the function always returns 0
+ */
+static int smmu_debugfs_ste_show(struct seq_file *seq, void *unused)
+{
+	struct ste_context *ctx = seq->private;
+	struct arm_smmu_master *master = ctx->master;
+	struct arm_smmu_device *smmu;
+	struct arm_smmu_ste *ste;
+	u32 sid, cfg;
+	int i;
+
+	if (!master) {
+		seq_puts(seq, "No SMMU master data\n");
+		return 0;
+	}
+
+	smmu = master->smmu;
+	scoped_guard(mutex, &smmu->streams_mutex) {
+		sid = ctx->sid;
+
+		if (!arm_smmu_sid_in_range(smmu, sid)) {
+			seq_printf(seq, "Invalid Stream ID: %u (max %u)\n",
+				   sid, (1 << smmu->sid_bits) - 1);
+			return 0;
+		}
+
+		ste = arm_smmu_get_step_for_sid(smmu, sid);
+		if (!ste) {
+			seq_printf(seq, "STE not available for SID %u\n", sid);
+			return 0;
+		}
+
+		seq_printf(seq, "STE for Stream ID %u\n", sid);
+		seq_printf(seq, "  Valid: %s\n",
+			   le64_to_cpu(ste->data[0]) & STRTAB_STE_0_V ? "Yes" : "No");
+
+		seq_puts(seq, "  Config: ");
+
+		cfg = FIELD_GET(STRTAB_STE_0_CFG, le64_to_cpu(ste->data[0]));
+
+		switch (cfg) {
+		case STRTAB_STE_0_CFG_BYPASS:
+			seq_puts(seq, "BYPASS\n");
+			break;
+		case STRTAB_STE_0_CFG_S1_TRANS:
+			seq_puts(seq, "only S1_TRANS\n");
+			break;
+		case STRTAB_STE_0_CFG_S2_TRANS:
+			seq_puts(seq, "only S2_TRANS\n");
+			break;
+		case STRTAB_STE_0_CFG_NESTED:
+			seq_puts(seq, "S1+S2_TRANS\n");
+			break;
+		case STRTAB_STE_0_CFG_ABORT:
+			seq_puts(seq, "ABORT\n");
+			break;
+		default:
+			seq_puts(seq, "UNKNOWN\n");
+		}
+
+		if (le64_to_cpu(ste->data[0]) & STRTAB_STE_0_CFG_S1_TRANS) {
+			seq_printf(seq, "  S1ContextPtr: 0x%016llx\n",
+				   le64_to_cpu(ste->data[1]) & STRTAB_STE_0_S1CTXPTR_MASK);
+		}
+
+		if (le64_to_cpu(ste->data[0]) & STRTAB_STE_0_CFG_S2_TRANS) {
+			seq_printf(seq, "  S2ContextPtr: 0x%016llx\n",
+				   le64_to_cpu(ste->data[3]) & STRTAB_STE_3_S2TTB_MASK);
+		}
+
+		/* Display raw STE data */
+		seq_puts(seq, "  Raw Data:\n");
+		for (i = 0; i < STRTAB_STE_DWORDS; i++)
+			seq_printf(seq, "    STE[%d]: 0x%016llx\n", i,
+				   le64_to_cpu(ste->data[i]));
+	}
+		return 0;
+}
+
+static int smmu_debugfs_ste_open(struct inode *inode, struct file *file)
+{
+	struct ste_context *ctx = inode->i_private;
+
+	if (!ctx || !get_device(ctx->master->dev))
+		return -ENODEV;
+
+	return single_open(file, smmu_debugfs_ste_show, ctx);
+}
+
+static int smmu_debugfs_ste_release(struct inode *inode, struct file *file)
+{
+	struct seq_file *seq = file->private_data;
+	struct ste_context *ctx = seq->private;
+
+	single_release(inode, file);
+	if (ctx)
+		put_device(ctx->master->dev);
+	return 0;
+}
+
+static const struct file_operations smmu_debugfs_ste_fops = {
+	.owner   = THIS_MODULE,
+	.open    = smmu_debugfs_ste_open,
+	.read    = seq_read,
+	.llseek  = seq_lseek,
+	.release = smmu_debugfs_ste_release,
+};
+
+/**
+ * arm_smmu_debugfs_create_stream_table() - Create debugfs entries for stream table
+ * @dev: device to create entries for
+ * @smmu: SMMU device
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int arm_smmu_debugfs_create_stream_table(struct device *dev,
+					 struct arm_smmu_device *smmu)
+{
+	struct dentry *stream_dir, *dev_dir;
+	struct arm_smmu_master *master;
+	struct ste_context *ctx;
+	char name[64];
+	u32 sid;
+	int i;
+
+	scoped_guard(mutex, &arm_smmu_debugfs_lock) {
+		if (!smmu->debugfs->stream_dir) {
+			stream_dir = debugfs_create_dir("stream_table",
+							smmu->debugfs->smmu_dir);
+			if (!stream_dir)
+				return -ENOMEM;
+
+			smmu->debugfs->stream_dir = stream_dir;
+		} else {
+			stream_dir = smmu->debugfs->stream_dir;
+		}
+	}
+
+	master = dev_iommu_priv_get(dev);
+	if (!master || !master->num_streams)
+		return -ENODEV;
+
+	for (i = 0; i < master->num_streams; i++) {
+		sid = master->streams[i].id;
+		snprintf(name, sizeof(name), "%u", sid);
+		dev_dir = debugfs_create_dir(name, stream_dir);
+		if (!dev_dir)
+			continue;
+
+		/* Create STE file */
+		ctx = kzalloc_obj(*ctx);
+		ctx->master = master;
+		ctx->sid = sid;
+		spin_lock(&smmu->debugfs->stream_lock);
+		list_add_tail(&ctx->node, &smmu->debugfs->stream_list);
+		spin_unlock(&smmu->debugfs->stream_lock);
+		debugfs_create_file("ste", 0444, dev_dir, ctx,
+				    &smmu_debugfs_ste_fops);
+
+	}
+
+	return 0;
+}
+
+/**
+ * arm_smmu_debugfs_remove_stream_table() - Remove debugfs entries for stream table
+ * @dev: device to remove entries for
+ * @smmu: SMMU device
+ *
+ * This function removes the debugfs directories created by
+ * arm_smmu_debugfs_create_stream_table().
+ */
+void arm_smmu_debugfs_remove_stream_table(struct device *dev,
+				      struct arm_smmu_device *smmu)
+{
+	struct dentry *stream_dir, *dev_dir;
+	struct arm_smmu_master *master;
+	struct ste_context *ctx, *tmp;
+	char name[64];
+	int i;
+
+	/* Check if stream_table directory exists */
+	if (!smmu->debugfs || !smmu->debugfs->stream_dir)
+		return;
+
+	stream_dir = smmu->debugfs->stream_dir;
+	master = dev_iommu_priv_get(dev);
+	if (!master)
+		return;
+
+	/* Remove directories for each stream ID */
+	for (i = 0; i < master->num_streams; i++) {
+		snprintf(name, sizeof(name), "%u", master->streams[i].id);
+		dev_dir = debugfs_lookup(name, stream_dir);
+		debugfs_remove_recursive(dev_dir);
+	}
+
+	/* Free stream context */
+	spin_lock(&smmu->debugfs->stream_lock);
+	list_for_each_entry_safe(ctx, tmp, &smmu->debugfs->stream_list, node) {
+		if (ctx->master->dev == dev) {
+			list_del(&ctx->node);
+			kfree(ctx);
+		}
+	}
+	spin_unlock(&smmu->debugfs->stream_lock);
+}
+
 /**
  * arm_smmu_debugfs_setup() - Initialize debugfs for SMMU device
  * @smmu: SMMU device to setup debugfs for
@@ -201,6 +421,8 @@ int arm_smmu_debugfs_setup(struct arm_smmu_device *smmu, const char *name)
 	}
 
 	debugfs->smmu_dir = smmu_dir;
+	INIT_LIST_HEAD(&debugfs->stream_list);
+	spin_lock_init(&debugfs->stream_lock);
 	smmu->debugfs = debugfs;
 
 	/* Create capabilities file */
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 cbb3fccc501b..6be132b2f3f0 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c
@@ -2654,7 +2654,10 @@ static int arm_smmu_domain_finalise(struct arm_smmu_domain *smmu_domain,
 	return 0;
 }
 
-static struct arm_smmu_ste *
+#ifndef CONFIG_ARM_SMMU_V3_DEBUGFS
+static
+#endif
+struct arm_smmu_ste *
 arm_smmu_get_step_for_sid(struct arm_smmu_device *smmu, u32 sid)
 {
 	struct arm_smmu_strtab_cfg *cfg = &smmu->strtab_cfg;
@@ -3489,7 +3492,10 @@ struct arm_smmu_device *arm_smmu_get_by_fwnode(struct fwnode_handle *fwnode)
 	return dev ? dev_get_drvdata(dev) : NULL;
 }
 
-static bool arm_smmu_sid_in_range(struct arm_smmu_device *smmu, u32 sid)
+#ifndef CONFIG_ARM_SMMU_V3_DEBUGFS
+static
+#endif
+bool arm_smmu_sid_in_range(struct arm_smmu_device *smmu, u32 sid)
 {
 	if (smmu->features & ARM_SMMU_FEAT_2_LVL_STRTAB)
 		return arm_smmu_strtab_l1_idx(sid) < smmu->strtab_cfg.l2.num_l1_ents;
@@ -3635,6 +3641,10 @@ static struct iommu_device *arm_smmu_probe_device(struct device *dev)
 		pci_prepare_ats(to_pci_dev(dev), stu);
 	}
 
+#ifdef CONFIG_ARM_SMMU_V3_DEBUGFS
+	arm_smmu_debugfs_create_stream_table(dev, smmu);
+#endif
+
 	return &smmu->iommu;
 
 err_free_master:
@@ -3648,10 +3658,14 @@ static void arm_smmu_release_device(struct device *dev)
 
 	WARN_ON(master->iopf_refcount);
 
+#ifdef CONFIG_ARM_SMMU_V3_DEBUGFS
+	arm_smmu_debugfs_remove_stream_table(dev, master->smmu);
+#endif
 	arm_smmu_disable_pasid(master);
 	arm_smmu_remove_master(master);
 	if (arm_smmu_cdtab_allocated(&master->cd_table))
 		arm_smmu_free_cd_tables(master);
+
 	kfree(master);
 }
 
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 a54d72fb9e07..d9ea803a2e4e 100644
--- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
+++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.h
@@ -734,13 +734,30 @@ struct arm_smmu_impl_ops {
 };
 
 #ifdef CONFIG_ARM_SMMU_V3_DEBUGFS
+struct ste_context {
+	struct arm_smmu_master		*master;
+	u32				sid;
+	struct list_head		node;
+};
+
 struct arm_smmu_debugfs {
+	struct list_head		stream_list;
+	spinlock_t			stream_lock;
 	struct dentry			*smmu_dir;
+	struct dentry			*stream_dir;
 	/* Reserved for future extensions */
 };
 
 int arm_smmu_debugfs_setup(struct arm_smmu_device *smmu, const char *name);
 void arm_smmu_debugfs_remove(struct arm_smmu_device *smmu);
+int arm_smmu_debugfs_create_stream_table(struct device *dev,
+					 struct arm_smmu_device *smmu);
+void arm_smmu_debugfs_remove_stream_table(struct device *dev,
+					  struct arm_smmu_device *smmu);
+
+struct arm_smmu_ste *
+arm_smmu_get_step_for_sid(struct arm_smmu_device *smmu, u32 sid);
+bool arm_smmu_sid_in_range(struct arm_smmu_device *smmu, u32 sid);
 #endif
 
 /* An SMMUv3 instance */
-- 
2.33.0