* [PATCH 1/2] iommupt: fix full IOTLB flush from a zero length unmap
2026-11-05 12:12 [PATCH 0/2] iommupt: fix zero-length unmap causing a full IOTLB flush YeeLi
@ 2026-11-05 12:12 ` YeeLi
2026-11-09 6:59 ` Baolu Lu
2026-11-05 12:12 ` [PATCH 2/2] iommupt: add a KUnit test for unmapping an empty range YeeLi
1 sibling, 1 reply; 4+ messages in thread
From: YeeLi @ 2026-11-05 12:12 UTC (permalink / raw)
To: joro, will; +Cc: robin.murphy, jgg, iommu
From: yeeli <seven.yi.lee@gmail.com>
unmap_range() hands gather_range_pending() the number of bytes it
actually unmapped, not the requested length. That count is 0 whenever
the range holds no mappings, so an unmap that removes nothing reaches
the gather code with len == 0.
The gather helpers assume a real range: both
iommu_iotlb_gather_add_range() and iommu_iotlb_gather_is_disjoint()
compute end = iova + len - 1. For len == 0 this wraps to iova - 1.
The problem can be reproduced by attaching an empty paging domain to an
Intel VT-d device and unmapping IOVA 0:
domain = iommu_paging_domain_alloc(dev);
iommu_attach_device(domain, dev);
iommu_unmap(domain, 0, PAGE_SIZE); /* nothing is mapped */
The unmap returns 0, but the gather becomes [0, ULONG_MAX] and the
iotlb_sync() that follows asks the driver to invalidate everything.
Captured with the cache_tag_flush_range and qi_submit tracepoints
enabled:
cache_tag_flush_range: dmar0 dmar0[0] type iotlb did 12 [0x0-0xffffffffffffffff] addr 0x0 mask 0x34
qi_submit: p_iotlb_inv dmar0: 0xc0026 0x0 0x0 0x0
With a non-zero IOVA the wrapped end widens whatever range has already
been gathered instead.
Skip the gather bookkeeping when nothing was unmapped. An untouched
gather keeps start == ULONG_MAX and end == 0, which iommu_iotlb_sync()
already ignores.
Fixes: 7c53f4238aa8 ("iommupt: Add unmap_pages op")
Signed-off-by: yeeli <seven.yi.lee@gmail.com>
---
drivers/iommu/generic_pt/iommu_pt.h | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/drivers/iommu/generic_pt/iommu_pt.h b/drivers/iommu/generic_pt/iommu_pt.h
index 837d835e4fad..566cae4bc217 100644
--- a/drivers/iommu/generic_pt/iommu_pt.h
+++ b/drivers/iommu/generic_pt/iommu_pt.h
@@ -83,8 +83,13 @@ static void gather_range_pending(struct iommupt_pending_gather *pending,
* For NO_GAPS the user gets to pick if flushing all or doing micro
* flushes is better for their work load by choosing DMA vs DMA-FQ
* operation. Drivers should also see shadow_on_flush.
+ *
+ * A zero length unmap (len == 0) means nothing was unmapped and must
+ * not create a gather range: iova + len - 1 wraps around, and at IOVA 0
+ * the resulting degenerate range [0, ULONG_MAX] would turn into a full
+ * IOTLB flush.
*/
- if (!iommu_iotlb_gather_queued(iotlb_gather)) {
+ if (len && !iommu_iotlb_gather_queued(iotlb_gather)) {
if (pt_feature(common, PT_FEAT_FLUSH_RANGE_NO_GAPS) &&
iommu_iotlb_gather_is_disjoint(iotlb_gather, iova, len)) {
iommu_iotlb_sync(&iommu_table->domain, iotlb_gather);
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread* [PATCH 2/2] iommupt: add a KUnit test for unmapping an empty range
2026-11-05 12:12 [PATCH 0/2] iommupt: fix zero-length unmap causing a full IOTLB flush YeeLi
2026-11-05 12:12 ` [PATCH 1/2] iommupt: fix full IOTLB flush from a zero length unmap YeeLi
@ 2026-11-05 12:12 ` YeeLi
1 sibling, 0 replies; 4+ messages in thread
From: YeeLi @ 2026-11-05 12:12 UTC (permalink / raw)
To: joro, will; +Cc: robin.murphy, jgg, iommu
From: yeeli <seven.yi.lee@gmail.com>
Unmapping a range that holds no mappings must not record anything in
the IOTLB gather. Otherwise iova + len - 1 underflows and the flush
covers the whole domain, or widens a range that was already gathered.
Check that the gather stays empty for a hole at IOVA 0 and at a
non-zero IOVA. Without the previous fix the first case turns the gather
into [0, ULONG_MAX] and the second one flips it to [iova, iova - 1].
Signed-off-by: yeeli <seven.yi.lee@gmail.com>
---
drivers/iommu/generic_pt/kunit_iommu_pt.h | 31 +++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/drivers/iommu/generic_pt/kunit_iommu_pt.h b/drivers/iommu/generic_pt/kunit_iommu_pt.h
index 9fc71db74234..59a9386f1474 100644
--- a/drivers/iommu/generic_pt/kunit_iommu_pt.h
+++ b/drivers/iommu/generic_pt/kunit_iommu_pt.h
@@ -427,6 +427,36 @@ static void test_mixed(struct kunit *test)
check_iova(test, start, oa, len);
}
+/*
+ * Unmapping a range that holds no mappings must not record anything in
+ * the IOTLB gather. Otherwise iova + len - 1 underflows and the flush
+ * covers the whole domain, or widens a range that was already gathered.
+ */
+static void test_unmap_hole(struct kunit *test)
+{
+ struct kunit_iommu_priv *priv = test->priv;
+ struct iommu_iotlb_gather gather;
+
+ /* A hole at IOVA 0 must not turn the gather into [0, ULONG_MAX]. */
+ iommu_iotlb_gather_init(&gather);
+ KUNIT_ASSERT_EQ(
+ test,
+ iommu_unmap_fast(&priv->domain, 0, priv->smallest_pgsz, &gather),
+ 0);
+ KUNIT_EXPECT_EQ(test, gather.start, ULONG_MAX);
+ KUNIT_EXPECT_EQ(test, gather.end, 0);
+
+ /* Neither may a hole at a non-zero IOVA widen the gather. */
+ iommu_iotlb_gather_init(&gather);
+ KUNIT_ASSERT_EQ(
+ test,
+ iommu_unmap_fast(&priv->domain, priv->smallest_pgsz,
+ priv->smallest_pgsz, &gather),
+ 0);
+ KUNIT_EXPECT_EQ(test, gather.start, ULONG_MAX);
+ KUNIT_EXPECT_EQ(test, gather.end, 0);
+}
+
static struct kunit_case iommu_test_cases[] = {
KUNIT_CASE_FMT(test_increase_level),
KUNIT_CASE_FMT(test_map_simple),
@@ -435,6 +465,7 @@ static struct kunit_case iommu_test_cases[] = {
KUNIT_CASE_FMT(test_random_map),
KUNIT_CASE_FMT(test_pgsize_boundary),
KUNIT_CASE_FMT(test_mixed),
+ KUNIT_CASE_FMT(test_unmap_hole),
{},
};
--
2.34.1
^ permalink raw reply related [flat|nested] 4+ messages in thread