[PATCH] iommu/amd: Flush stale TLB entries when freeing a nested domain ID

From: Shuai Xue

Date: Wed Sep 09 2026 - 04:29:48 EST


A nested domain uses its hdom_id as the domain ID tag for the stage-2
translations programmed into the DTE. When the last user of a
gdom_id -> hdom_id mapping goes away, nested_domain_free() returns
the hdom_id to the global pool without invalidating the TLB entries
that were cached under that ID while devices were attached.

Once the hdom_id is handed out to a different domain, those stale
entries become reachable through the new owner of the ID, breaking
DMA isolation between the two domains.

Flush all TLB entries tagged with the hdom_id on the IOMMU of the
viommu before returning the ID to the pool, the same way
set_dte_entry() flushes the old domain ID it replaces right after
calling amd_iommu_update_dte(). This is safe at this point: the
IOMMU core guarantees that a domain is detached from all devices
before it is freed, and the mapping's refcount counts nested domain
objects sharing the mapping rather than devices, so once it drops to
zero no device translates through the hdom_id anymore.

Fixes: 103f4e7c8544 ("iommu/amd: Add support for nested domain attach/detach")
Cc: stable@xxxxxxxxxxxxxxx
Assisted-by: Qoder:Qwen-3.8-MAX-Preview
Signed-off-by: Shuai Xue <xueshuai@xxxxxxxxxxxxxxxxx>
---
drivers/iommu/amd/amd_iommu.h | 1 +
drivers/iommu/amd/iommu.c | 2 +-
drivers/iommu/amd/nested.c | 9 +++++++++
3 files changed, 11 insertions(+), 1 deletion(-)

diff --git a/drivers/iommu/amd/amd_iommu.h b/drivers/iommu/amd/amd_iommu.h
index a2fe804b038b..97883db687ec 100644
--- a/drivers/iommu/amd/amd_iommu.h
+++ b/drivers/iommu/amd/amd_iommu.h
@@ -89,6 +89,7 @@ int amd_iommu_complete_ppr(struct device *dev, u32 pasid, int status, int tag);
* This function flushes all internal caches of
* the IOMMU used by this driver.
*/
+void amd_iommu_flush_tlb_domid(struct amd_iommu *iommu, u32 dom_id);
void amd_iommu_flush_all_caches(struct amd_iommu *iommu);
void amd_iommu_domain_flush_pages(struct protection_domain *domain,
u64 address, u64 last, u32 flags);
diff --git a/drivers/iommu/amd/iommu.c b/drivers/iommu/amd/iommu.c
index 4dc306a4b5c6..15bc9ddd93f4 100644
--- a/drivers/iommu/amd/iommu.c
+++ b/drivers/iommu/amd/iommu.c
@@ -1576,7 +1576,7 @@ static void amd_iommu_flush_tlb_all(struct amd_iommu *iommu)
iommu_completion_wait(iommu);
}

-static void amd_iommu_flush_tlb_domid(struct amd_iommu *iommu, u32 dom_id)
+void amd_iommu_flush_tlb_domid(struct amd_iommu *iommu, u32 dom_id)
{
struct iommu_cmd cmd;

diff --git a/drivers/iommu/amd/nested.c b/drivers/iommu/amd/nested.c
index 63b53b29e029..d92ee06edb4c 100644
--- a/drivers/iommu/amd/nested.c
+++ b/drivers/iommu/amd/nested.c
@@ -265,6 +265,8 @@ static void nested_domain_free(struct iommu_domain *dom)
struct guest_domain_mapping_info *curr;
struct nested_domain *ndom __free(kfree) = to_ndomain(dom);
struct amd_iommu_viommu *aviommu = ndom->viommu;
+ struct amd_iommu *iommu = container_of(aviommu->core.iommu_dev,
+ struct amd_iommu, iommu);

xa_lock_irqsave(&aviommu->gdomid_array, irqflags);

@@ -288,6 +290,13 @@ static void nested_domain_free(struct iommu_domain *dom)
pr_debug("%s: Free gdom_id=%#x, hdom_id=%#x\n",
__func__, ndom->gdom_id, curr->hdom_id);

+ /*
+ * Flush any TLB entries still tagged with this hdom_id before
+ * returning it to the global pool, so that a future owner of the
+ * ID can not hit stale stage-2 translations.
+ */
+ amd_iommu_flush_tlb_domid(iommu, curr->hdom_id);
+
amd_iommu_pdom_id_free(ndom->gdom_info->hdom_id);
kfree(curr);
}
--
2.39.3