* [PATCH 0/2] iommufd: Keep dmabuf MMIO attributes in domains added later
@ 2026-10-05 14:37 Andrea Parri
2026-10-05 14:37 ` [PATCH 1/2] iommufd: Keep dmabuf PFNs MMIO when filling another domain Andrea Parri
2026-10-05 14:37 ` [PATCH 2/2] iommufd/selftest: Check dmabuf MMIO mappings filled from " Andrea Parri
0 siblings, 2 replies; 3+ messages in thread
From: Andrea Parri @ 2026-10-05 14:37 UTC (permalink / raw)
To: Jason Gunthorpe, Kevin Tian, Shuah Khan
Cc: Andrea Parri, Joerg Roedel, Will Deacon, Robin Murphy,
Nicolin Chen, iommu, linux-kselftest, linux-kernel
A VFIO PCI dmabuf mapped into an IOAS gets IOMMU_MMIO only in the domains
that exist when it is mapped. A domain attached afterwards, or an
IOMMU_IOAS_COPY of the area, reads the PFNs back from the existing
domain and maps the BAR as cacheable CPU memory. Patch 1 always takes
dmabuf PFNs from the recorded phys range instead.
Sashiko first reported this on patch 6 of David Woodhouse's RFC "KVM:
Allow alternative providers of guest_memfd backed by PFNMAP memory",
which picks the batch kind in pfn_reader_fill_dmabuf() from the
exporter's memory type. This fix does not depend on that series and
combines with it: with both, a domain added later gets the kind the
exporter reports.
The mock page table has no memory type bits, so the existing selftests
cannot see the difference. Patch 2 makes the mock record, per domain,
the pages mapped with IOMMU_MMIO, adds IOMMU_TEST_OP_MD_CHECK_MMIO, and
checks the domain filled later in two new tests, dmabuf_mmio_new_domain
and dmabuf_mmio_copy.
Tested on x86-64 under virtme-ng with CONFIG_IOMMUFD_TEST=y. Without
patch 1 both new tests fail on the second domain in all three mock
domain variants; in dmabuf_mmio_new_domain, a temporary print in
batch_to_domain() showed that domain mapped with IOMMU_READ |
IOMMU_WRITE | IOMMU_CACHE, where the first got IOMMU_READ | IOMMU_WRITE |
IOMMU_MMIO. With patch 1 the dmabuf tests pass.
Not tested on hardware that uses the memory type bits (ARM SMMUv3, AMD
with SME, RISC-V).
Andrea Parri (2):
iommufd: Keep dmabuf PFNs MMIO when filling another domain
iommufd/selftest: Check dmabuf MMIO mappings filled from another domain
drivers/iommu/iommufd/iommufd_test.h | 5 ++
drivers/iommu/iommufd/pages.c | 13 ++-
drivers/iommu/iommufd/selftest.c | 90 +++++++++++++++++++
tools/testing/selftests/iommu/iommufd.c | 51 +++++++++++
tools/testing/selftests/iommu/iommufd_utils.h | 13 +++
5 files changed, 168 insertions(+), 4 deletions(-)
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 1/2] iommufd: Keep dmabuf PFNs MMIO when filling another domain
2026-10-05 14:37 [PATCH 0/2] iommufd: Keep dmabuf MMIO attributes in domains added later Andrea Parri
@ 2026-10-05 14:37 ` Andrea Parri
2026-10-05 14:37 ` [PATCH 2/2] iommufd/selftest: Check dmabuf MMIO mappings filled from " Andrea Parri
1 sibling, 0 replies; 3+ messages in thread
From: Andrea Parri @ 2026-10-05 14:37 UTC (permalink / raw)
To: Jason Gunthorpe, Kevin Tian
Cc: Andrea Parri, Joerg Roedel, Will Deacon, Robin Murphy,
Nicolin Chen, iommu, linux-kernel, Sashiko, stable
A VFIO PCI dmabuf mapped into an IOAS is mapped with IOMMU_MMIO only in
the domains present when it is mapped. A domain added later, or a copy
of the area into another IOAS, maps the same BAR as cacheable CPU
memory: IOMMU_CACHE set and IOMMU_MMIO clear.
Once the area is in pages->domains_itree, pfn_reader_fill_span() reads
its PFNs back from the storage domain through batch_from_domain(), which
adds them with batch_add_pfn() as BATCH_CPU_MEMORY. Only a hole in the
span reaches pfn_reader_fill_dmabuf() and gets BATCH_MMIO, so
batch_to_domain() programs the later domain with the wrong prot.
The effect depends on the page table format. io-pgtable-arm maps the BAR
as Normal cacheable instead of Device memory, the AMDv1 and x86_64
generic_pt formats set the SME C-bit on it when the tables are encrypted,
and the RISC-V format with Svpbmt maps it as normal memory instead of IO.
Read dmabuf PFNs from the recorded phys for every span, before the
xarray and domain paths. The phys range is always available, and the
read-back from a domain only serves to avoid re-pinning user pages.
Sashiko pointed this out while reviewing an RFC that plumbs the dma-buf
memory type through pfn_reader_fill_dmabuf().
Fixes: 74014a4b55f5 ("iommufd: Have pfn_reader process DMABUF iopt_pages")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Link: https://lore.kernel.org/all/20260716154123.32DC01F000E9@smtp.kernel.org/
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
drivers/iommu/iommufd/pages.c | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/drivers/iommu/iommufd/pages.c b/drivers/iommu/iommufd/pages.c
index 404f31d8f7291..2f6153108add7 100644
--- a/drivers/iommu/iommufd/pages.c
+++ b/drivers/iommu/iommufd/pages.c
@@ -1178,6 +1178,15 @@ static int pfn_reader_fill_span(struct pfn_reader *pfns)
WARN_ON(span->last_used < start_index))
return -EINVAL;
+ /*
+ * Always read a dmabuf from its phys, even where a domain already
+ * maps it: a domain can only report CPU memory PFNs, losing
+ * BATCH_MMIO. last_hole aliases last_used, so this covers any span.
+ */
+ if (iopt_is_dmabuf(pfns->pages))
+ return pfn_reader_fill_dmabuf(&pfns->dmabuf, &pfns->batch,
+ start_index, span->last_hole);
+
if (span->is_used == 1) {
batch_from_xarray(&pfns->batch, &pfns->pages->pinned_pfns,
start_index, span->last_used);
@@ -1201,10 +1210,6 @@ static int pfn_reader_fill_span(struct pfn_reader *pfns)
return 0;
}
- if (iopt_is_dmabuf(pfns->pages))
- return pfn_reader_fill_dmabuf(&pfns->dmabuf, &pfns->batch,
- start_index, span->last_hole);
-
user = &pfns->user;
if (start_index >= user->upages_end) {
rc = pfn_reader_user_pin(user, pfns->pages, start_index,
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH 2/2] iommufd/selftest: Check dmabuf MMIO mappings filled from another domain
2026-10-05 14:37 [PATCH 0/2] iommufd: Keep dmabuf MMIO attributes in domains added later Andrea Parri
2026-10-05 14:37 ` [PATCH 1/2] iommufd: Keep dmabuf PFNs MMIO when filling another domain Andrea Parri
@ 2026-10-05 14:37 ` Andrea Parri
1 sibling, 0 replies; 3+ messages in thread
From: Andrea Parri @ 2026-10-05 14:37 UTC (permalink / raw)
To: Jason Gunthorpe, Kevin Tian, Shuah Khan
Cc: Andrea Parri, Joerg Roedel, Will Deacon, Robin Murphy,
Nicolin Chen, iommu, linux-kselftest, linux-kernel
Add tests that a dmabuf keeps IOMMU_MMIO in a domain that is filled
from an already mapped area: a second domain added to the IOAS after
the dmabuf has been mapped, and the domain of another IOAS the dmabuf
is copied into.
The mock page table has no memory type bits, so a mapping made with
IOMMU_MMIO cannot be told apart from one made with IOMMU_CACHE. Wrap the
mock's generic_pt map_range and unmap_range ops to record, per domain,
the pages mapped with IOMMU_MMIO, and add IOMMU_TEST_OP_MD_CHECK_MMIO to
check that a range is recorded.
dmabuf_mmio_new_domain maps a mock dmabuf, checks the existing domain,
allocates a second HWPT on the same IOAS and checks it too.
dmabuf_mmio_copy maps a mock dmabuf, copies it with IOMMU_IOAS_COPY into
a second IOAS that has its own mock domain, and checks that domain.
Without the previous patch the checks on the second domain fail in the
three mock domain variants; with it all checks pass.
Assisted-by: LLM
Signed-off-by: Andrea Parri <parri.andrea@gmail.com>
---
drivers/iommu/iommufd/iommufd_test.h | 5 ++
drivers/iommu/iommufd/selftest.c | 90 +++++++++++++++++++
tools/testing/selftests/iommu/iommufd.c | 51 +++++++++++
tools/testing/selftests/iommu/iommufd_utils.h | 13 +++
4 files changed, 159 insertions(+)
diff --git a/drivers/iommu/iommufd/iommufd_test.h b/drivers/iommu/iommufd/iommufd_test.h
index 52b78cbcc920d..3c8111dfb928a 100644
--- a/drivers/iommu/iommufd/iommufd_test.h
+++ b/drivers/iommu/iommufd/iommufd_test.h
@@ -31,6 +31,7 @@ enum {
IOMMU_TEST_OP_PASID_CHECK_HWPT,
IOMMU_TEST_OP_DMABUF_GET,
IOMMU_TEST_OP_DMABUF_REVOKE,
+ IOMMU_TEST_OP_MD_CHECK_MMIO,
};
enum {
@@ -109,6 +110,10 @@ struct iommu_test_cmd {
__aligned_u64 length;
__aligned_u64 uptr;
} check_map;
+ struct {
+ __aligned_u64 iova;
+ __aligned_u64 length;
+ } check_mmio;
struct {
__aligned_u64 length;
__aligned_u64 uptr;
diff --git a/drivers/iommu/iommufd/selftest.c b/drivers/iommu/iommufd/selftest.c
index 25387969ab753..e700fd9250540 100644
--- a/drivers/iommu/iommufd/selftest.c
+++ b/drivers/iommu/iommufd/selftest.c
@@ -120,6 +120,10 @@ struct mock_iommu_domain {
struct pt_iommu_amdv1 amdv1;
};
unsigned long flags;
+ /* map_range/unmap_range wrappers record IOMMU_MMIO pages */
+ const struct pt_iommu_ops *pt_ops;
+ struct pt_iommu_ops rec_ops;
+ struct xarray mmio_pages;
};
PT_IOMMU_CHECK_DOMAIN(struct mock_iommu_domain, iommu, domain);
PT_IOMMU_CHECK_DOMAIN(struct mock_iommu_domain, amdv1.iommu, domain);
@@ -392,6 +396,7 @@ static void mock_domain_free(struct iommu_domain *domain)
struct mock_iommu_domain *mock = to_mock_domain(domain);
pt_iommu_deinit(&mock->iommu);
+ xa_destroy(&mock->mmio_pages);
kfree(mock);
}
@@ -423,6 +428,51 @@ static const struct iommu_dirty_ops amdv1_mock_dirty_ops = {
.set_dirty_tracking = mock_domain_set_dirty_tracking,
};
+static int mock_map_range(struct pt_iommu *iommu_table, dma_addr_t iova,
+ phys_addr_t paddr, dma_addr_t len, unsigned int prot,
+ gfp_t gfp, size_t *mapped)
+{
+ struct mock_iommu_domain *mock =
+ container_of(iommu_table, struct mock_iommu_domain, iommu);
+ unsigned long index;
+ int rc;
+
+ rc = mock->pt_ops->map_range(iommu_table, iova, paddr, len, prot, gfp,
+ mapped);
+ if (rc || !(prot & IOMMU_MMIO))
+ return rc;
+
+ for (index = iova / MOCK_PAGE_SIZE;
+ index <= (iova + len - 1) / MOCK_PAGE_SIZE; index++) {
+ rc = xa_err(xa_store(&mock->mmio_pages, index, xa_mk_value(1),
+ gfp));
+ if (rc)
+ return rc;
+ }
+ return 0;
+}
+
+static size_t mock_unmap_range(struct pt_iommu *iommu_table, dma_addr_t iova,
+ dma_addr_t len,
+ struct iommu_iotlb_gather *iotlb_gather)
+{
+ struct mock_iommu_domain *mock =
+ container_of(iommu_table, struct mock_iommu_domain, iommu);
+ unsigned long index;
+ size_t unmapped;
+ void *entry;
+
+ unmapped = mock->pt_ops->unmap_range(iommu_table, iova, len,
+ iotlb_gather);
+ if (!unmapped)
+ return 0;
+
+ xa_for_each_range(&mock->mmio_pages, index, entry, iova / MOCK_PAGE_SIZE,
+ (iova + unmapped - 1) / MOCK_PAGE_SIZE)
+ xa_erase(&mock->mmio_pages, index);
+ return unmapped;
+}
+
static struct mock_iommu_domain *
mock_domain_alloc_pgtable(struct device *dev,
const struct iommu_hwpt_selftest *user_cfg, u32 flags)
@@ -434,6 +484,7 @@ mock_domain_alloc_pgtable(struct device *dev,
if (!mock)
return ERR_PTR(-ENOMEM);
mock->domain.type = IOMMU_DOMAIN_UNMANAGED;
+ xa_init(&mock->mmio_pages);
mock->amdv1.iommu.nid = NUMA_NO_NODE;
@@ -454,6 +505,12 @@ mock_domain_alloc_pgtable(struct device *dev,
if (rc)
goto err_free;
+ mock->pt_ops = mock->iommu.ops;
+ mock->rec_ops = *mock->pt_ops;
+ mock->rec_ops.map_range = mock_map_range;
+ mock->rec_ops.unmap_range = mock_unmap_range;
+ mock->iommu.ops = &mock->rec_ops;
+
/*
* In huge mode userspace should only provide huge pages, we
* have to include PAGE_SIZE for the domain to be accepted by
@@ -1180,6 +1237,35 @@ static int iommufd_test_add_reserved(struct iommufd_ucmd *ucmd,
return rc;
}
+/* Check that every page in [iova, iova + length) was mapped with IOMMU_MMIO */
+static int iommufd_test_md_check_mmio(struct iommufd_ucmd *ucmd,
+ unsigned int mockpt_id,
+ unsigned long iova, size_t length)
+{
+ struct iommufd_hw_pagetable *hwpt;
+ struct mock_iommu_domain *mock;
+ unsigned long index;
+ int rc = 0;
+
+ if (!length || iova % MOCK_PAGE_SIZE || length % MOCK_PAGE_SIZE)
+ return -EINVAL;
+
+ hwpt = get_md_pagetable(ucmd, mockpt_id, &mock);
+ if (IS_ERR(hwpt))
+ return PTR_ERR(hwpt);
+
+ for (index = iova / MOCK_PAGE_SIZE;
+ index <= (iova + length - 1) / MOCK_PAGE_SIZE; index++) {
+ if (!xa_load(&mock->mmio_pages, index)) {
+ rc = -EINVAL;
+ break;
+ }
+ }
+
+ iommufd_put_object(ucmd->ictx, &hwpt->obj);
+ return rc;
+}
+
/* Check that every pfn under each iova matches the pfn under a user VA */
static int iommufd_test_md_check_pa(struct iommufd_ucmd *ucmd,
unsigned int mockpt_id, unsigned long iova,
@@ -2105,6 +2191,10 @@ int iommufd_test(struct iommufd_ucmd *ucmd)
case IOMMU_TEST_OP_MOCK_DOMAIN_REPLACE:
return iommufd_test_mock_domain_replace(
ucmd, cmd->id, cmd->mock_domain_replace.pt_id, cmd);
+ case IOMMU_TEST_OP_MD_CHECK_MMIO:
+ return iommufd_test_md_check_mmio(ucmd, cmd->id,
+ cmd->check_mmio.iova,
+ cmd->check_mmio.length);
case IOMMU_TEST_OP_MD_CHECK_MAP:
return iommufd_test_md_check_pa(
ucmd, cmd->id, cmd->check_map.iova,
diff --git a/tools/testing/selftests/iommu/iommufd.c b/tools/testing/selftests/iommu/iommufd.c
index 44193d171ba98..8f68c9aee15d5 100644
--- a/tools/testing/selftests/iommu/iommufd.c
+++ b/tools/testing/selftests/iommu/iommufd.c
@@ -1599,6 +1599,57 @@ TEST_F(iommufd_ioas, dmabuf_simple)
close(dfd);
}
+TEST_F(iommufd_ioas, dmabuf_mmio_new_domain)
+{
+ size_t buf_size = PAGE_SIZE * 4;
+ __u32 hwpt_id;
+ __u64 iova;
+ int dfd;
+
+ if (!variant->mock_domains)
+ SKIP(return, "needs a mock domain");
+
+ test_cmd_get_dmabuf(buf_size, &dfd);
+ test_ioctl_ioas_map_file(dfd, 0, buf_size, &iova);
+ test_cmd_md_check_mmio(self->hwpt_id, iova, buf_size);
+
+ /* A domain added later is filled from the already mapped area */
+ test_cmd_hwpt_alloc(self->device_id, self->ioas_id, 0, &hwpt_id);
+ test_cmd_md_check_mmio(hwpt_id, iova, buf_size);
+
+ test_ioctl_destroy(hwpt_id);
+ close(dfd);
+}
+
+TEST_F(iommufd_ioas, dmabuf_mmio_copy)
+{
+ size_t buf_size = PAGE_SIZE * 4;
+ struct iommu_ioas_copy copy_cmd = {
+ .size = sizeof(copy_cmd),
+ .flags = IOMMU_IOAS_MAP_WRITEABLE | IOMMU_IOAS_MAP_READABLE,
+ .src_ioas_id = self->ioas_id,
+ .length = buf_size,
+ };
+ __u32 stdev_id;
+ __u32 hwpt_id;
+ int dfd;
+
+ if (!variant->mock_domains)
+ SKIP(return, "needs a mock domain");
+
+ test_cmd_get_dmabuf(buf_size, &dfd);
+ test_ioctl_ioas_map_file(dfd, 0, buf_size, ©_cmd.src_iova);
+
+ /* The copy is filled from the domain of the source IOAS */
+ test_ioctl_ioas_alloc(©_cmd.dst_ioas_id);
+ test_cmd_mock_domain(copy_cmd.dst_ioas_id, &stdev_id, &hwpt_id, NULL);
+ ASSERT_EQ(0, ioctl(self->fd, IOMMU_IOAS_COPY, ©_cmd));
+ test_cmd_md_check_mmio(hwpt_id, copy_cmd.dst_iova, buf_size);
+
+ test_ioctl_destroy(stdev_id);
+ close(dfd);
+}
+
TEST_F(iommufd_ioas, dmabuf_revoke)
{
size_t buf_size = PAGE_SIZE*4;
diff --git a/tools/testing/selftests/iommu/iommufd_utils.h b/tools/testing/selftests/iommu/iommufd_utils.h
index b4928cbd4d9c8..4567a69de8df6 100644
--- a/tools/testing/selftests/iommu/iommufd_utils.h
+++ b/tools/testing/selftests/iommu/iommufd_utils.h
@@ -560,6 +560,19 @@ static int _test_cmd_destroy_access_pages(int fd, unsigned int access_id,
EXPECT_ERRNO(_errno, _test_cmd_destroy_access_pages( \
self->fd, access_id, access_pages_id))
+#define test_cmd_md_check_mmio(_hwpt_id, _iova, _length) \
+ ({ \
+ struct iommu_test_cmd check_cmd = { \
+ .size = sizeof(check_cmd), \
+ .op = IOMMU_TEST_OP_MD_CHECK_MMIO, \
+ .id = _hwpt_id, \
+ .check_mmio = { .iova = _iova, .length = _length }, \
+ }; \
+ ASSERT_EQ(0, ioctl(self->fd, \
+ _IOMMU_TEST_CMD(IOMMU_TEST_OP_MD_CHECK_MMIO), \
+ &check_cmd)); \
+ })
+
static int _test_cmd_get_dmabuf(int fd, size_t len, int *out_fd)
{
struct iommu_test_cmd cmd = {
--
2.53.0
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-10-05 14:37 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-05 14:37 [PATCH 0/2] iommufd: Keep dmabuf MMIO attributes in domains added later Andrea Parri
2026-10-05 14:37 ` [PATCH 1/2] iommufd: Keep dmabuf PFNs MMIO when filling another domain Andrea Parri
2026-10-05 14:37 ` [PATCH 2/2] iommufd/selftest: Check dmabuf MMIO mappings filled from " Andrea Parri
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®