[RFC PATCH 10/14] iommufd/selftest: Add mock-domain IOVA queries

From: Fred Griffoul

Date: Tue Oct 27 2026 - 12:02:38 EST


From: Fred Griffoul <fgriffo@xxxxxxxxxxxx>

Tests of guest_memfd mappings cannot know in advance which frame an
IOVA reaches, since a revoke can unmap or replace it, and
IOMMU_TEST_OP_MD_CHECK_MAP only compares a range against a user buffer.

Add IOMMU_TEST_OP_MD_CHECK_MAPPED, which checks that a range of a mock
domain is all mapped or all unmapped, and IOMMU_TEST_OP_MD_IOVA_TO_PHYS,
which returns the physical address behind an IOVA. They hold
domains_rwsem for writing, so that a userspace unmap cannot free a
page-table level during the walk; they are not safe against a
concurrent guest_memfd update or dma-buf revoke, which the selftests do
not do.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Fred Griffoul <fgriffo@xxxxxxxxxxxx>
---
drivers/iommu/iommufd/iommufd_test.h | 18 ++++
drivers/iommu/iommufd/selftest.c | 125 +++++++++++++++++++++++++++
2 files changed, 143 insertions(+)

diff --git a/drivers/iommu/iommufd/iommufd_test.h b/drivers/iommu/iommufd/iommufd_test.h
index 52b78cbcc920..3fffcb2b20d7 100644
--- a/drivers/iommu/iommufd/iommufd_test.h
+++ b/drivers/iommu/iommufd/iommufd_test.h
@@ -31,6 +31,8 @@ enum {
IOMMU_TEST_OP_PASID_CHECK_HWPT,
IOMMU_TEST_OP_DMABUF_GET,
IOMMU_TEST_OP_DMABUF_REVOKE,
+ IOMMU_TEST_OP_MD_CHECK_MAPPED,
+ IOMMU_TEST_OP_MD_IOVA_TO_PHYS,
};

enum {
@@ -193,6 +195,22 @@ struct iommu_test_cmd {
__s32 dmabuf_fd;
__u32 revoked;
} dmabuf_revoke;
+ struct {
+ /*
+ * 1: every page in [iova, iova+length) must be mapped;
+ * 0: none of them may be. Any page in the other state
+ * fails with -ENOENT. Any other value, or a non-zero
+ * __reserved, is -EINVAL.
+ */
+ __u32 mapped;
+ __u32 __reserved;
+ __aligned_u64 iova;
+ __aligned_u64 length;
+ } check_mapped;
+ struct {
+ __aligned_u64 iova;
+ __aligned_u64 out_phys; /* 0 if unmapped */
+ } iova_to_phys;
};
__u32 last;
};
diff --git a/drivers/iommu/iommufd/selftest.c b/drivers/iommu/iommufd/selftest.c
index ee706f18f7e9..378d7c46327e 100644
--- a/drivers/iommu/iommufd/selftest.c
+++ b/drivers/iommu/iommufd/selftest.c
@@ -2068,6 +2068,122 @@ static int iommufd_test_dmabuf_get(struct iommufd_ucmd *ucmd,
return rc;
}

+/*
+ * True if [iova, iova + length) lies inside the domain's aperture. An IOVA
+ * that the domain cannot map would read as unmapped, so the queries refuse
+ * it rather than report it as such.
+ */
+static bool mock_domain_covers(struct mock_iommu_domain *mock,
+ unsigned long iova, size_t length)
+{
+ struct iommu_domain_geometry *geo = &mock->domain.geometry;
+ unsigned long last;
+
+ if (check_add_overflow(iova, length - 1, &last))
+ return false;
+ return iova >= geo->aperture_start && last <= geo->aperture_end;
+}
+
+/*
+ * iova_to_phys() walks the page table, so it must not run while an unmap
+ * frees a level of it. A userspace unmap holds the IOAS domains_rwsem for
+ * reading while it unmaps the domains, so holding it for writing keeps such
+ * unmaps away. A guest_memfd update, e.g. on a conversion or a punch hole,
+ * and a dma-buf revoke unmap under their pages mutex only, so tests must not
+ * run these queries while one is in progress. A range can cross several
+ * areas and holes, so the queries take one IOAS-wide lock rather than the
+ * pages mutex of each area.
+ */
+static struct rw_semaphore *
+mock_domain_unmap_lock(struct iommufd_hw_pagetable *hwpt)
+{
+ return &to_hwpt_paging(hwpt)->ioas->iopt.domains_rwsem;
+}
+
+/*
+ * Report the physical address the mock domain resolves @iova to, or 0 if
+ * it is unmapped. Lets a test check that two IOVAs share one frame, or that
+ * an IOVA moved to another frame, without knowing the frames in advance.
+ * Not safe against a concurrent guest_memfd update or dma-buf revoke; see
+ * mock_domain_unmap_lock().
+ */
+static int iommufd_test_md_iova_to_phys(struct iommufd_ucmd *ucmd,
+ unsigned int mockpt_id,
+ unsigned long iova)
+{
+ struct iommu_test_cmd *cmd = ucmd->cmd;
+ struct iommufd_hw_pagetable *hwpt;
+ struct mock_iommu_domain *mock;
+ unsigned int page_size;
+ int rc;
+
+ hwpt = get_md_pagetable(ucmd, mockpt_id, &mock);
+ if (IS_ERR(hwpt))
+ return PTR_ERR(hwpt);
+
+ page_size = 1 << __ffs(mock->domain.pgsize_bitmap);
+ if (iova % page_size || !mock_domain_covers(mock, iova, page_size)) {
+ rc = -EINVAL;
+ goto out_put;
+ }
+ down_write(mock_domain_unmap_lock(hwpt));
+ cmd->iova_to_phys.out_phys =
+ mock->domain.ops->iova_to_phys(&mock->domain, iova);
+ up_write(mock_domain_unmap_lock(hwpt));
+ rc = iommufd_ucmd_respond(ucmd, sizeof(*cmd));
+out_put:
+ iommufd_put_object(ucmd->ictx, &hwpt->obj);
+ return rc;
+}
+
+/*
+ * Check that [@iova, @iova + @length) is fully mapped or fully unmapped. Not
+ * safe against a concurrent guest_memfd update or dma-buf revoke; see
+ * mock_domain_unmap_lock().
+ */
+static int iommufd_test_md_check_mapped(struct iommufd_ucmd *ucmd,
+ unsigned int mockpt_id,
+ unsigned long iova, size_t length,
+ u32 mapped, u32 reserved)
+{
+ struct iommufd_hw_pagetable *hwpt;
+ struct mock_iommu_domain *mock;
+ unsigned int page_size;
+ int rc = 0;
+
+ if (mapped > 1 || reserved)
+ return -EINVAL;
+
+ hwpt = get_md_pagetable(ucmd, mockpt_id, &mock);
+ if (IS_ERR(hwpt))
+ return PTR_ERR(hwpt);
+
+ page_size = 1 << __ffs(mock->domain.pgsize_bitmap);
+ if (iova % page_size || length % page_size || !length ||
+ !mock_domain_covers(mock, iova, length)) {
+ rc = -EINVAL;
+ goto out_put;
+ }
+
+ down_write(mock_domain_unmap_lock(hwpt));
+ for (; length; length -= page_size, iova += page_size) {
+ bool is_mapped =
+ mock->domain.ops->iova_to_phys(&mock->domain, iova) != 0;
+
+ if (is_mapped != mapped) {
+ rc = -ENOENT;
+ break;
+ }
+ /* A huge-page mock domain spans 2^38 bytes. */
+ cond_resched();
+ }
+ up_write(mock_domain_unmap_lock(hwpt));
+
+out_put:
+ iommufd_put_object(ucmd->ictx, &hwpt->obj);
+ return rc;
+}
+
static int iommufd_test_dmabuf_revoke(struct iommufd_ucmd *ucmd, int fd,
bool revoked)
{
@@ -2180,6 +2296,15 @@ int iommufd_test(struct iommufd_ucmd *ucmd)
return iommufd_test_dmabuf_revoke(ucmd,
cmd->dmabuf_revoke.dmabuf_fd,
cmd->dmabuf_revoke.revoked);
+ case IOMMU_TEST_OP_MD_CHECK_MAPPED:
+ return iommufd_test_md_check_mapped(ucmd, cmd->id,
+ cmd->check_mapped.iova,
+ cmd->check_mapped.length,
+ cmd->check_mapped.mapped,
+ cmd->check_mapped.__reserved);
+ case IOMMU_TEST_OP_MD_IOVA_TO_PHYS:
+ return iommufd_test_md_iova_to_phys(ucmd, cmd->id,
+ cmd->iova_to_phys.iova);
default:
return -EOPNOTSUPP;
}
--
2.47.3