* [PATCH 1/4] iommufd/iova_bitmap: Clip recorded ranges to the bitmap
2026-09-25 16:11 [PATCH 0/4] iommufd: Keep IOMMU_HWPT_GET_DIRTY_BITMAP within the user bitmap Andrea Parri
@ 2026-09-25 16:11 ` Andrea Parri
2026-09-25 16:11 ` [PATCH 2/4] iommufd/selftest: Test dirty bitmap recording past the query Andrea Parri
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Andrea Parri @ 2026-09-25 16:11 UTC (permalink / raw)
To: Jason Gunthorpe, Kevin Tian
Cc: Andrea Parri, Joerg Roedel, Will Deacon, Robin Murphy,
Shuah Khan, Joao Martins, Alex Williamson, Yishai Hadas, iommu,
linux-kernel, linux-kselftest, stable
IOMMU_HWPT_GET_DIRTY_BITMAP can set bits in the caller's memory past the
end of the bitmap. When the queried range ends inside a dirty huge
IOPTE, the generic page-table dirty code records the whole entry, and
iova_bitmap_set() then sets every bit up to the end of the pinned page.
iova_bitmap_mapped_range() checks a range only against the pages
currently pinned, and iova_bitmap_mapped_length() caps only the pinning
window, so neither limits the write. A range that starts inside the
bitmap but ends past it passes iova_bitmap_advance_to() on its start,
and the write loop runs to the end of the last pinned page.
Clip the range to [bitmap->iova, bitmap->iova + bitmap->length - 1]
before computing cur_bit and last_bit. A range that starts before the
bitmap now has its in-range part recorded; the unsigned underflow in
iova_bitmap_advance_to() previously rejected it whole.
The clip runs before any pinning or indexing, so for a validated query
range cur_bit and last_bit can no longer address outside the bitmap. It
only narrows the requested range; the bitmap bounds are fixed at
allocation.
Fixes: 58ccf0190d19 ("vfio: Add an IOVA bitmap support")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
drivers/iommu/iommufd/iova_bitmap.c | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/drivers/iommu/iommufd/iova_bitmap.c b/drivers/iommu/iommufd/iova_bitmap.c
index dac3e657d498d..44826ae330c4b 100644
--- a/drivers/iommu/iommufd/iova_bitmap.c
+++ b/drivers/iommu/iommufd/iova_bitmap.c
@@ -388,6 +388,30 @@ int iova_bitmap_for_each(struct iova_bitmap *bitmap, void *opaque,
}
EXPORT_SYMBOL_NS_GPL(iova_bitmap_for_each, "IOMMUFD");
+/*
+ * Trim [@iova..@iova+@length-1] to the IOVA range covered by @bitmap.
+ * Dirty trackers may report more than was asked for, for instance a whole
+ * huge IOPTE of which only a part was queried.
+ */
+static bool iova_bitmap_clip(struct iova_bitmap *bitmap, unsigned long *iova,
+ size_t *length)
+{
+ unsigned long first = max(*iova, bitmap->iova);
+ unsigned long last = bitmap->iova + bitmap->length - 1;
+ unsigned long end;
+
+ if (!*length)
+ return false;
+ if (!check_add_overflow(*iova, *length - 1, &end))
+ last = min(last, end);
+ if (first > last)
+ return false;
+
+ *iova = first;
+ *length = last - first + 1;
+ return true;
+}
+
/**
* iova_bitmap_set() - Records an IOVA range in bitmap
* @bitmap: IOVA bitmap
@@ -404,6 +428,9 @@ void iova_bitmap_set(struct iova_bitmap *bitmap,
struct iova_bitmap_map *mapped = &bitmap->mapped;
unsigned long cur_bit, last_bit, last_page_idx;
+ if (!iova_bitmap_clip(bitmap, &iova, &length))
+ return;
+
update_indexes:
if (unlikely(!iova_bitmap_mapped_range(mapped, iova, length))) {
/*
--
2.53.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 2/4] iommufd/selftest: Test dirty bitmap recording past the query
2026-09-25 16:11 [PATCH 0/4] iommufd: Keep IOMMU_HWPT_GET_DIRTY_BITMAP within the user bitmap Andrea Parri
2026-09-25 16:11 ` [PATCH 1/4] iommufd/iova_bitmap: Clip recorded ranges to the bitmap Andrea Parri
@ 2026-09-25 16:11 ` Andrea Parri
2026-09-25 16:11 ` [PATCH 3/4] iommufd: Reject a zero-length dirty bitmap request Andrea Parri
2026-09-25 16:11 ` [PATCH 4/4] iommufd/selftest: Test " Andrea Parri
3 siblings, 0 replies; 5+ messages in thread
From: Andrea Parri @ 2026-09-25 16:11 UTC (permalink / raw)
To: Jason Gunthorpe, Kevin Tian
Cc: Andrea Parri, Joerg Roedel, Will Deacon, Robin Murphy,
Shuah Khan, Joao Martins, Alex Williamson, Yishai Hadas, iommu,
linux-kernel, linux-kselftest
Add a dirty-tracking test that dirties a huge IOPTE and queries only part
of it, with a guard placed after an exactly sized bitmap.
The test maps hugetlb-backed memory, allocates a MOCK_IOMMUPT_HUGE
dirty-tracking HWPT, and dirties the first 1M IOPTE. It then queries a
128K window at the mock 2K page size, so the request covers 64 bits while
the tracker records 512. A guard follows the one-u64 bitmap in the same
pinned page.
Without the clip in iova_bitmap_set(), the kernel sets 56 bytes past the
bitmap and the guard check fails:
# iommufd.c:2470:get_dirty_bitmap_overrun:Expected 0xaaaaaaaaaaaaaaaaULL (12297829382473034410) == bitmap[bitmap_words + i] (18446744073709551615)
# FAIL iommufd_dirty_tracking.domain_dirty64M_huge.get_dirty_bitmap_overrun
With the fix the guard stays intact. The test runs only on the *_huge
variants and skips the rest. Tested on x86-64 under virtme-ng with
CONFIG_IOMMUFD_TEST=y and hugepages=128 on the kernel command line; the
whole iommufd_dirty_tracking fixture reports PASSED: 63/63, the seven
skips being this test on the non-huge variants.
Assisted-by: LLM
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
tools/testing/selftests/iommu/iommufd.c | 49 +++++++++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/tools/testing/selftests/iommu/iommufd.c b/tools/testing/selftests/iommu/iommufd.c
index 44193d171ba98..47d861b3bdd7b 100644
--- a/tools/testing/selftests/iommu/iommufd.c
+++ b/tools/testing/selftests/iommu/iommufd.c
@@ -2423,6 +2423,55 @@ TEST_F(iommufd_dirty_tracking, get_dirty_bitmap_no_clear)
test_ioctl_destroy(hwpt_id);
}
+TEST_F(iommufd_dirty_tracking, get_dirty_bitmap_overrun)
+{
+ uint32_t ioas_id = self->ioas_id;
+ uint32_t hwpt_id;
+ size_t nbits = 64;
+ size_t query_len = nbits * self->page_size;
+ __u64 *bitmap = self->bitmap;
+ size_t bitmap_words =
+ DIV_ROUND_UP(nbits, BITS_PER_BYTE * sizeof(*bitmap));
+ __u64 set = 1;
+ __u64 nr_dirty;
+ size_t i;
+
+ if (!variant->hugepages)
+ SKIP(return, "test requires a huge IOPTE");
+
+ /* The canary sits in the same pinned page, right after the bitmap. */
+ memset(bitmap, 0, (bitmap_words + 8) * sizeof(*bitmap));
+ for (i = 0; i < 8; i++)
+ bitmap[bitmap_words + i] = 0xaaaaaaaaaaaaaaaaULL;
+
+ test_ioctl_ioas_map_fixed_id(ioas_id, self->buffer,
+ variant->buffer_size, MOCK_APERTURE_START);
+ test_cmd_hwpt_alloc_iommupt(self->idev_id, ioas_id,
+ IOMMU_HWPT_ALLOC_DIRTY_TRACKING,
+ MOCK_IOMMUPT_HUGE, &hwpt_id);
+ test_cmd_set_dirty_tracking(hwpt_id, true);
+
+ /* Dirty the whole first huge IOPTE */
+ test_cmd_mock_domain_set_dirty(self->fd, hwpt_id, MOCK_HUGE_PAGE_SIZE,
+ MOCK_APERTURE_START, MOCK_HUGE_PAGE_SIZE,
+ &set, &nr_dirty);
+ ASSERT_EQ(1, nr_dirty);
+
+ /* Query only part of it */
+ test_cmd_get_dirty_bitmap(self->fd, hwpt_id, query_len,
+ MOCK_APERTURE_START, self->page_size, bitmap,
+ 0);
+
+ /* The queried window is dirty */
+ ASSERT_EQ(~0ULL, bitmap[0]);
+
+ /* Bits past the queried bitmap must not be set */
+ for (i = 0; i < 8; i++)
+ ASSERT_EQ(0xaaaaaaaaaaaaaaaaULL, bitmap[bitmap_words + i]);
+
+ test_ioctl_destroy(hwpt_id);
+}
+
/* VFIO compatibility IOCTLs */
TEST_F(iommufd, simple_ioctls)
--
2.53.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 3/4] iommufd: Reject a zero-length dirty bitmap request
2026-09-25 16:11 [PATCH 0/4] iommufd: Keep IOMMU_HWPT_GET_DIRTY_BITMAP within the user bitmap Andrea Parri
2026-09-25 16:11 ` [PATCH 1/4] iommufd/iova_bitmap: Clip recorded ranges to the bitmap Andrea Parri
2026-09-25 16:11 ` [PATCH 2/4] iommufd/selftest: Test dirty bitmap recording past the query Andrea Parri
@ 2026-09-25 16:11 ` Andrea Parri
2026-09-25 16:11 ` [PATCH 4/4] iommufd/selftest: Test " Andrea Parri
3 siblings, 0 replies; 5+ messages in thread
From: Andrea Parri @ 2026-09-25 16:11 UTC (permalink / raw)
To: Jason Gunthorpe, Kevin Tian
Cc: Andrea Parri, Joerg Roedel, Will Deacon, Robin Murphy,
Shuah Khan, Joao Martins, Alex Williamson, Yishai Hadas, iommu,
linux-kernel, linux-kselftest, stable
iommufd_check_iova_range() subtracts one from bitmap->length before it
checks the length, so a zero-length request wraps to SIZE_MAX. With
iova = 0 the wrapped value passes the overflow and alignment checks, and
the request reaches iova_bitmap_alloc() with length 0.
iova_bitmap_alloc() then sizes the bitmap from a length - 1 of SIZE_MAX,
so mapped_total_index is effectively unbounded. The scan uses
last_iova = ULONG_MAX, which spans the whole IOAS when an area is mapped
at IOVA 0. Reject a zero length before the subtraction.
Fixes: b9a60d6f850e ("iommufd: Add IOMMU_HWPT_GET_DIRTY_BITMAP")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
drivers/iommu/iommufd/io_pagetable.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/drivers/iommu/iommufd/io_pagetable.c b/drivers/iommu/iommufd/io_pagetable.c
index 4e447ce74cf6c..283ab372e8da8 100644
--- a/drivers/iommu/iommufd/io_pagetable.c
+++ b/drivers/iommu/iommufd/io_pagetable.c
@@ -605,6 +605,9 @@ int iommufd_check_iova_range(struct io_pagetable *iopt,
size_t iommu_pgsize = iopt->iova_alignment;
u64 last_iova;
+ if (!bitmap->length)
+ return -EINVAL;
+
if (check_add_overflow(bitmap->iova, bitmap->length - 1, &last_iova))
return -EOVERFLOW;
--
2.53.0
^ permalink raw reply [flat|nested] 5+ messages in thread* [PATCH 4/4] iommufd/selftest: Test a zero-length dirty bitmap request
2026-09-25 16:11 [PATCH 0/4] iommufd: Keep IOMMU_HWPT_GET_DIRTY_BITMAP within the user bitmap Andrea Parri
` (2 preceding siblings ...)
2026-09-25 16:11 ` [PATCH 3/4] iommufd: Reject a zero-length dirty bitmap request Andrea Parri
@ 2026-09-25 16:11 ` Andrea Parri
3 siblings, 0 replies; 5+ messages in thread
From: Andrea Parri @ 2026-09-25 16:11 UTC (permalink / raw)
To: Jason Gunthorpe, Kevin Tian
Cc: Andrea Parri, Joerg Roedel, Will Deacon, Robin Murphy,
Shuah Khan, Joao Martins, Alex Williamson, Yishai Hadas, iommu,
linux-kernel, linux-kselftest
Add a GET_DIRTY_BITMAP case that passes iova = 0 with a zero length and
expects -EINVAL. iova = 0 is the value that lets the zero length wrap
into an accepted range; without the previous patch the request is
accepted and returns success.
The case runs on every variant of the dirty-tracking fixture. Tested on
x86-64 under virtme-ng with CONFIG_IOMMUFD_TEST=y.
Assisted-by: LLM
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
tools/testing/selftests/iommu/iommufd.c | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/tools/testing/selftests/iommu/iommufd.c b/tools/testing/selftests/iommu/iommufd.c
index 47d861b3bdd7b..2b3c9e9c69ca7 100644
--- a/tools/testing/selftests/iommu/iommufd.c
+++ b/tools/testing/selftests/iommu/iommufd.c
@@ -2472,6 +2472,25 @@ TEST_F(iommufd_dirty_tracking, get_dirty_bitmap_overrun)
test_ioctl_destroy(hwpt_id);
}
+TEST_F(iommufd_dirty_tracking, get_dirty_bitmap_zero_length)
+{
+ uint32_t ioas_id = self->ioas_id;
+ uint32_t hwpt_id;
+
+ test_cmd_hwpt_alloc_iommupt(self->idev_id, ioas_id,
+ IOMMU_HWPT_ALLOC_DIRTY_TRACKING,
+ MOCK_IOMMUPT_DEFAULT, &hwpt_id);
+ test_cmd_set_dirty_tracking(hwpt_id, true);
+
+ /* Only iova 0 wraps the zero length into an accepted range. */
+ EXPECT_ERRNO(EINVAL,
+ _test_cmd_get_dirty_bitmap(self->fd, hwpt_id, 0, 0,
+ self->page_size, self->bitmap,
+ 0));
+
+ test_ioctl_destroy(hwpt_id);
+}
+
/* VFIO compatibility IOCTLs */
TEST_F(iommufd, simple_ioctls)
--
2.53.0
^ permalink raw reply [flat|nested] 5+ messages in thread