From 1d56f4d6db3d2b2c93017a80c4f9c4cfffa9d193 Mon Sep 17 00:00:00 2001 From: Qinyun Tan Date: Wed, 2 Sep 2026 16:54:14 +0800 Subject: [PATCH 01/15] iommufd/selftest: Return dmabuf fd from IOMMU_TEST_OP_DMABUF_GET again The selftest helper reads the new dmabuf fd from the ioctl return value: *out_fd = ioctl(fd, IOMMU_TEST_CMD, &cmd); Commit dba4254e216d ("iommufd/selftest: Fix dmabuf leak in iommufd_test_dmabuf_get()") fixed the dmabuf leak on dma_buf_fd() failure, but the applied version also changed the success path to return 0, so userspace no longer receives the fd. Note the patch as posted on the list returned rc here; the change to return 0 appeared when it was applied: https://lore.kernel.org/all/20260707030635.221577-1-seven.yi.lee@gmail.com/ Every test using test_cmd_get_dmabuf() then operates on fd 0 instead of the dmabuf, and the dmabuf_simple and dmabuf_revoke selftests fail across all fixtures: # iommufd.c:1595:dmabuf_simple:Expected -1 (-1) == _test_ioctl_ioas_map_file(...) (0) Keep the dma_buf_put() on failure but return the fd on success. Fixes: dba4254e216d ("iommufd/selftest: Fix dmabuf leak in iommufd_test_dmabuf_get()") Signed-off-by: Qinyun Tan Link: https://patch.msgid.link/20260902085414.549830-1-qinyuntan@linux.alibaba.com Tested-by: Yi Lai Signed-off-by: Jason Gunthorpe --- drivers/iommu/iommufd/selftest.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/drivers/iommu/iommufd/selftest.c b/drivers/iommu/iommufd/selftest.c index ee706f18f7e9..f6da927461b5 100644 --- a/drivers/iommu/iommufd/selftest.c +++ b/drivers/iommu/iommufd/selftest.c @@ -2056,11 +2056,9 @@ static int iommufd_test_dmabuf_get(struct iommufd_ucmd *ucmd, } rc = dma_buf_fd(dmabuf, open_flags); - if (rc < 0) { + if (rc < 0) dma_buf_put(dmabuf); - return rc; - } - return 0; + return rc; err_free: kfree(priv->memory); From b1cf7f04c28ff3389559965dc95addbbd02f9138 Mon Sep 17 00:00:00 2001 From: Eva Kurchatova Date: Wed, 9 Sep 2026 00:48:12 +0300 Subject: [PATCH 02/15] iommufd/selftest: Skip when the mock device is not there Both iommufd tests need the mock device the iommufd module registers under CONFIG_IOMMUFD_TEST, and they open /dev/iommu in every fixture without checking, so on a kernel that does not provide it every case fails on the open. Checking the open is not enough on its own. /dev/iommu appears as soon as the iommufd module is loaded, which says nothing about the mock device: the release config here is CONFIG_IOMMUFD=m with CONFIG_IOMMUFD_TEST unset, so the open succeeds and the tests fail later in the ioctls that drive the mock device instead. Only the debug config sets CONFIG_IOMMUFD_TEST. /sys/bus/iommufd_mock covers both, it exists only once the module is loaded and the kernel was built with CONFIG_IOMMUFD_TEST. Check for it once before handing over to the harness, rather than in each of the fixtures, and report a skip. test_harness_run() is what TEST_HARNESS_MAIN calls anyway, so the tests still run exactly as they did. Signed-off-by: Eva Kurchatova Link: https://patch.msgid.link/20260908214825.2682078-2-eva.kurchatova@virtuozzo.com Signed-off-by: Jason Gunthorpe --- tools/testing/selftests/iommu/iommufd.c | 14 +++++++++++++- tools/testing/selftests/iommu/iommufd_fail_nth.c | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/tools/testing/selftests/iommu/iommufd.c b/tools/testing/selftests/iommu/iommufd.c index d44b34b05757..44193d171ba9 100644 --- a/tools/testing/selftests/iommu/iommufd.c +++ b/tools/testing/selftests/iommu/iommufd.c @@ -3613,4 +3613,16 @@ TEST_F(iommufd_device_pasid, pasid_attach) test_cmd_mock_domain_replace(self->stdev_id, self->ioas_id); } -TEST_HARNESS_MAIN +static bool iommufd_mock_available(void) +{ + return access("/sys/bus/iommufd_mock", F_OK) == 0; +} + +int main(int argc, char **argv) +{ + if (!iommufd_mock_available()) + ksft_exit_skip( + "no iommufd mock device, the tests need CONFIG_IOMMUFD_TEST=y and the iommufd module loaded\n"); + + return test_harness_run(argc, argv); +} diff --git a/tools/testing/selftests/iommu/iommufd_fail_nth.c b/tools/testing/selftests/iommu/iommufd_fail_nth.c index 25495d8dceb3..1a31fff3f56c 100644 --- a/tools/testing/selftests/iommu/iommufd_fail_nth.c +++ b/tools/testing/selftests/iommu/iommufd_fail_nth.c @@ -745,4 +745,16 @@ TEST_FAIL_NTH(basic_fail_nth, device) return 0; } -TEST_HARNESS_MAIN +static bool iommufd_mock_available(void) +{ + return access("/sys/bus/iommufd_mock", F_OK) == 0; +} + +int main(int argc, char **argv) +{ + if (!iommufd_mock_available()) + ksft_exit_skip( + "no iommufd mock device, the tests need CONFIG_IOMMUFD_TEST=y and the iommufd module loaded\n"); + + return test_harness_run(argc, argv); +} From 26564d25cd814dfd72a21a2c52b81e678cf02bae Mon Sep 17 00:00:00 2001 From: Nicolin Chen Date: Sun, 30 Aug 2026 15:26:38 -0700 Subject: [PATCH 03/15] iommu/arm-smmu-v3-iommufd: Reject unsupported bits in invalidation commands The arm_vsmmu_cache_invalidate() op hands a guest's invalidation commands to the trusted main command queue after enforcing only the VMID or the SID, and passes the rest of the command through to the queue unchanged. That lets a guest set bits the host never meant to forward: a reserved or undefined bit makes a command malformed; per the Arm SMMUv3 specification, in its section 4.1.3 "Command errors", a CERROR_ILL is raised, among other cases, when: A valid command opcode is used and a Reserved or undefined field is optionally detected as non-zero, which results in the command being treated as malformed. Restrict each opcode to the fields that the driver supports and reject the command with -EIO if it sets any other bit, before the command reaches the queue. This stops the host from forwarding any bit whose meaning it does not control. Document this contract in the uAPI header, so user space must take the responsibility to forward valid commands only. Some fields and whole opcodes are legal only on an SMMU that implements the matching feature, so accept them conditionally: - NUM, SCALE, TG and TTL need FEAT_RANGE_INV. - SCALE bit 25, for values above 31, and TTL == 0b01 with a 16KB TG need SMMU_IDR5.DS, gated on ARM_SMMU_FEAT_DS so that a VMM exposing DS from the reported IDR5 register keeps working. - ASID is limited to asid_bits, since its upper 8 bits are RES0 on an SMMU that only supports 8-bit ASIDs. - ATC_INV needs FEAT_ATS. Per the specification's section 4.5 "ATS and PRI", CMD_ATC_INV is ILLEGAL when: SMMU_IDR0.ATS == 0 and this command is issued on a Non-secure or Secure Command queue. - SSV, SSID and Global need a non-zero ssid_bits. Without it, setting them is not illegal but CONSTRAINED UNPREDICTABLE, which a guest should not be able to provoke. Global also takes effect only when SSV == 1, broadening the invalidation from the one SSID to all the PASIDs of the single device that the SID field addresses. Some values inside the accepted fields are Reserved too: - NUM == 0, SCALE == 0 and TTL == 0 together are a Reserved combination and cause a CERROR_ILL. - NUM, SCALE and TTL turn RES0 when TG == 0. - An ATC_INV Size above 52, the invalidate-all span, is permitted to raise a CERROR_ILL. Reject these Reserved values the same way. In contrast, an out-of-range address or ID value is defined as CONSTRAINED UNPREDICTABLE that would be scoped to the guest itself, so it does not deserve a check. Fixes: d68beb276ba2 ("iommu/arm-smmu-v3: Support IOMMU_HWPT_INVALIDATE using a VIOMMU object") Cc: stable@vger.kernel.org Reviewed-by: Jason Gunthorpe Reviewed-by: Pranjal Shrivastava Assisted-by: Claude:claude-fable-5 Signed-off-by: Nicolin Chen Link: https://patch.msgid.link/40f4aabf98e434d6ac9f42a8d278b92200f202ca.1788127877.git.nicolinc@nvidia.com Signed-off-by: Jason Gunthorpe --- .../arm/arm-smmu-v3/arm-smmu-v3-iommufd.c | 131 ++++++++++++++++-- include/uapi/linux/iommufd.h | 4 +- 2 files changed, 122 insertions(+), 13 deletions(-) diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c index 25982bdbcbd9..747b9d47ff7e 100644 --- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c +++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c @@ -322,6 +322,106 @@ struct arm_vsmmu_invalidation_cmd { }; }; +/* Reject the range field values that the spec defines as Reserved */ +static int arm_vsmmu_validate_range(struct arm_smmu_device *smmu, u64 data[2]) +{ + bool range = !!(data[0] & (CMDQ_TLBI_0_NUM | CMDQ_TLBI_0_SCALE)); + u8 ttl = FIELD_GET(CMDQ_TLBI_1_TTL, data[1]); + u8 tg = FIELD_GET(CMDQ_TLBI_1_TG, data[1]); + + /* NUM, SCALE and TTL are RES0 when TG == 0 */ + if (!tg) + return (range || ttl) ? -EIO : 0; + /* TTL == 0b01 with a 16KB TG requires SMMU_IDR5.DS */ + if (tg == 2 && ttl == 1 && !(smmu->features & ARM_SMMU_FEAT_DS)) + return -EIO; + /* NUM == 0, SCALE == 0 with TTL == 0 is a reserved combination */ + if (!range && !ttl) + return -EIO; + return 0; +} + +static int arm_vsmmu_validate_user_cmd(struct arm_vsmmu *vsmmu, u64 data[2]) +{ + struct arm_smmu_device *smmu = vsmmu->smmu; + u64 allowed[2] = { CMDQ_0_OP }; + + /* Collect the fields userspace is allowed to set for each opcode */ + switch (data[0] & CMDQ_0_OP) { + case CMDQ_OP_TLBI_NSNH_ALL: + break; + case CMDQ_OP_TLBI_NH_VA: + /* An SMMU with 8-bit ASIDs treats the upper 8 bits as RES0 */ + allowed[0] |= FIELD_PREP(CMDQ_TLBI_0_ASID, + GENMASK(smmu->asid_bits - 1, 0)); + fallthrough; + case CMDQ_OP_TLBI_NH_VAA: + /* NUM/SCALE/TG/TTL are range fields gated on FEAT_RANGE_INV */ + if (smmu->features & ARM_SMMU_FEAT_RANGE_INV) { + if (arm_vsmmu_validate_range(smmu, data)) + return -EIO; + allowed[0] |= CMDQ_TLBI_0_NUM | CMDQ_TLBI_0_SCALE; + allowed[1] |= CMDQ_TLBI_1_TG | CMDQ_TLBI_1_TTL; + /* SCALE bit 25 (values above 31) is RES0 without DS */ + if (!(smmu->features & ARM_SMMU_FEAT_DS)) + allowed[0] &= ~FIELD_PREP(CMDQ_TLBI_0_SCALE, + BIT(5)); + } + allowed[0] |= CMDQ_TLBI_0_VMID; + allowed[1] |= CMDQ_TLBI_1_LEAF | CMDQ_TLBI_1_VA_MASK; + break; + case CMDQ_OP_TLBI_NH_ASID: + /* An SMMU with 8-bit ASIDs treats the upper 8 bits as RES0 */ + allowed[0] |= FIELD_PREP(CMDQ_TLBI_0_ASID, + GENMASK(smmu->asid_bits - 1, 0)); + fallthrough; + case CMDQ_OP_TLBI_NH_ALL: + allowed[0] |= CMDQ_TLBI_0_VMID; + break; + case CMDQ_OP_ATC_INV: + /* ATC_INV is illegal unless the SMMU implements ATS */ + if (!(smmu->features & ARM_SMMU_FEAT_ATS)) + return -EIO; + /* A Size above 52 (invalidate-all) may raise a CERROR_ILL */ + if (FIELD_GET(CMDQ_ATC_1_SIZE, data[1]) > ATC_INV_SIZE_ALL) + return -EIO; + /* + * SSV/SSID/Global need substream support. SSID and Global are + * IGNORED (not RES0) when SSV == 0, so they need no SSV check. + */ + if (smmu->ssid_bits) + allowed[0] |= CMDQ_0_SSV | CMDQ_ATC_0_SSID | + CMDQ_ATC_0_GLOBAL; + allowed[0] |= CMDQ_ATC_0_SID; + allowed[1] |= CMDQ_ATC_1_SIZE | CMDQ_ATC_1_ADDR_MASK; + break; + case CMDQ_OP_CFGI_CD: + /* No SSV for CFGI_CD; SSID requires substream support */ + if (smmu->ssid_bits) + allowed[0] |= CMDQ_CFGI_0_SSID; + allowed[1] |= CMDQ_CFGI_1_LEAF; + fallthrough; + case CMDQ_OP_CFGI_CD_ALL: + allowed[0] |= CMDQ_CFGI_0_SID; + break; + default: + return -EIO; + } + + /* + * Reject any other bit, e.g. a RES0 bit or a Secure bit, before the + * command reaches the trusted main cmdq, so a guest cannot wedge the + * shared queue for every device with a CERROR_ILL. + * + * By contrast, an out-of-range address or ID value does not need a + * check: the spec defines it as CONSTRAINED UNPREDICTABLE, which is + * scoped to the guest itself and does not raise a CERROR_ILL. + */ + if ((data[0] & ~allowed[0]) || (data[1] & ~allowed[1])) + return -EIO; + return 0; +} + /* * Convert, in place, the raw invalidation command into an internal format that * can be passed to arm_smmu_cmdq_issue_cmdlist(). Internally commands are @@ -332,33 +432,40 @@ struct arm_vsmmu_invalidation_cmd { static int arm_vsmmu_convert_user_cmd(struct arm_vsmmu *vsmmu, struct arm_vsmmu_invalidation_cmd *cmd) { - /* Commands are le64 stored in u64 */ - cmd->cmd.data[0] = le64_to_cpu(cmd->ucmd.cmd[0]); - cmd->cmd.data[1] = le64_to_cpu(cmd->ucmd.cmd[1]); + u64 *data = cmd->cmd.data; + int ret; - switch (cmd->cmd.data[0] & CMDQ_0_OP) { + /* Commands are le64 stored in u64 */ + data[0] = le64_to_cpu(cmd->ucmd.cmd[0]); + data[1] = le64_to_cpu(cmd->ucmd.cmd[1]); + + ret = arm_vsmmu_validate_user_cmd(vsmmu, data); + if (ret) + return ret; + + switch (data[0] & CMDQ_0_OP) { case CMDQ_OP_TLBI_NSNH_ALL: /* Convert to NH_ALL */ - cmd->cmd.data[0] = CMDQ_OP_TLBI_NH_ALL | - FIELD_PREP(CMDQ_TLBI_0_VMID, vsmmu->vmid); - cmd->cmd.data[1] = 0; + data[0] = CMDQ_OP_TLBI_NH_ALL | + FIELD_PREP(CMDQ_TLBI_0_VMID, vsmmu->vmid); + data[1] = 0; break; case CMDQ_OP_TLBI_NH_VA: case CMDQ_OP_TLBI_NH_VAA: case CMDQ_OP_TLBI_NH_ALL: case CMDQ_OP_TLBI_NH_ASID: - cmd->cmd.data[0] &= ~CMDQ_TLBI_0_VMID; - cmd->cmd.data[0] |= FIELD_PREP(CMDQ_TLBI_0_VMID, vsmmu->vmid); + data[0] &= ~CMDQ_TLBI_0_VMID; + data[0] |= FIELD_PREP(CMDQ_TLBI_0_VMID, vsmmu->vmid); break; case CMDQ_OP_ATC_INV: case CMDQ_OP_CFGI_CD: case CMDQ_OP_CFGI_CD_ALL: { - u32 sid, vsid = FIELD_GET(CMDQ_CFGI_0_SID, cmd->cmd.data[0]); + u32 sid, vsid = FIELD_GET(CMDQ_CFGI_0_SID, data[0]); if (arm_vsmmu_vsid_to_sid(vsmmu, vsid, &sid)) return -EIO; - cmd->cmd.data[0] &= ~CMDQ_CFGI_0_SID; - cmd->cmd.data[0] |= FIELD_PREP(CMDQ_CFGI_0_SID, sid); + data[0] &= ~CMDQ_CFGI_0_SID; + data[0] |= FIELD_PREP(CMDQ_CFGI_0_SID, sid); break; } default: diff --git a/include/uapi/linux/iommufd.h b/include/uapi/linux/iommufd.h index 206fa667c782..5d47d1421c1f 100644 --- a/include/uapi/linux/iommufd.h +++ b/include/uapi/linux/iommufd.h @@ -948,7 +948,9 @@ struct iommu_hwpt_vtd_s1_invalidate { * CMDQ_OP_CFGI_CD * CMDQ_OP_CFGI_CD_ALL * - * -EIO will be returned if the command is not supported. + * User space must forward only valid commands: the kernel rejects, with + * -EIO, any command carrying an unsupported opcode, an unsupported field, + * or a field value that the underlying SMMU hardware does not implement. */ struct iommu_viommu_arm_smmuv3_invalidate { __aligned_le64 cmd[2]; From 61e2f25a49bc4d274e71856b2bcac0753f38bafb Mon Sep 17 00:00:00 2001 From: Nicolin Chen Date: Sun, 30 Aug 2026 15:26:39 -0700 Subject: [PATCH 04/15] iommufd: Iterate the cache invalidation array in the core The cache invalidation ops, cache_invalidate_user() for a nested HWPT and the cache_invalidate() for a vIOMMU, are each handed the full user request array and report how many of the array entries they handled by setting the array->entry_num. Every driver therefore implements its own loop over the array, and a driver wanting to process that array in fixed-size chunks (e.g. to issue commands out of a fixed-size on-stack buffer) has to carry the loop and its sub-array bookkeeping all on its own. Move the iteration into the iommufd core instead. Invoke the op with a sub-array that starts at the first not-yet-handled entry, let it handle a prefix of that sub-array and report the count via array->entry_num, then advance the base pointer and re-invoke the op until the entire array has been consumed or until the op returns an error along the way. A driver that handles the entire window in one single call, as all of the current drivers happen to do, finishes the loop in just one pass, so this does not change any of the existing behavior. It instead lets each of the drivers convert to bounded chunk processing on its own, done by each of the subsequent changes. Suggested-by: Jason Gunthorpe Reviewed-by: Kevin Tian Reviewed-by: Lu Baolu Reviewed-by: Pranjal Shrivastava Reviewed-by: Jason Gunthorpe Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Nicolin Chen Link: https://patch.msgid.link/e1896d79382392f8a0bf0ae736aa295f11c45ef0.1788127877.git.nicolinc@nvidia.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/iommufd/hw_pagetable.c | 25 +++++++++++++++---------- include/linux/iommu.h | 6 ++++-- include/linux/iommufd.h | 2 ++ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/drivers/iommu/iommufd/hw_pagetable.c b/drivers/iommu/iommufd/hw_pagetable.c index ef6e119c2a75..ae62db50885c 100644 --- a/drivers/iommu/iommufd/hw_pagetable.c +++ b/drivers/iommu/iommufd/hw_pagetable.c @@ -514,6 +514,8 @@ int iommufd_hwpt_invalidate(struct iommufd_ucmd *ucmd) .entry_len = cmd->entry_len, .entry_num = cmd->entry_num, }; + struct iommufd_hw_pagetable *hwpt = NULL; + struct iommufd_viommu *viommu = NULL; struct iommufd_object *pt_obj; u32 done_num = 0; int rc; @@ -540,31 +542,34 @@ int iommufd_hwpt_invalidate(struct iommufd_ucmd *ucmd) goto out; } if (pt_obj->type == IOMMUFD_OBJ_HWPT_NESTED) { - struct iommufd_hw_pagetable *hwpt = - container_of(pt_obj, struct iommufd_hw_pagetable, obj); - + hwpt = container_of(pt_obj, struct iommufd_hw_pagetable, obj); if (!hwpt->domain->ops || !hwpt->domain->ops->cache_invalidate_user) { rc = -EOPNOTSUPP; goto out_put_pt; } - rc = hwpt->domain->ops->cache_invalidate_user(hwpt->domain, - &data_array); } else if (pt_obj->type == IOMMUFD_OBJ_VIOMMU) { - struct iommufd_viommu *viommu = - container_of(pt_obj, struct iommufd_viommu, obj); - + viommu = container_of(pt_obj, struct iommufd_viommu, obj); if (!viommu->ops || !viommu->ops->cache_invalidate) { rc = -EOPNOTSUPP; goto out_put_pt; } - rc = viommu->ops->cache_invalidate(viommu, &data_array); } else { rc = -EINVAL; goto out_put_pt; } - done_num = data_array.entry_num; + do { + if (viommu) + rc = viommu->ops->cache_invalidate(viommu, &data_array); + else + rc = hwpt->domain->ops->cache_invalidate_user( + hwpt->domain, &data_array); + + done_num += data_array.entry_num; + data_array.uptr += data_array.entry_num * data_array.entry_len; + data_array.entry_num = cmd->entry_num - done_num; + } while (!rc && done_num != cmd->entry_num); out_put_pt: iommufd_put_object(ucmd->ictx, pt_obj); diff --git a/include/linux/iommu.h b/include/linux/iommu.h index ac43b8b93f14..05f7cee1375e 100644 --- a/include/linux/iommu.h +++ b/include/linux/iommu.h @@ -773,8 +773,10 @@ struct iommu_ops { * passes in the cache invalidation requests, in form * of a driver data structure. The driver must update * array->entry_num to report the number of handled - * invalidation requests. The driver data structure - * must be defined in include/uapi/linux/iommufd.h + * invalidation requests. A driver may handle fewer than + * the requested, in which case the core re-invokes the + * op for the remainder. The driver data structure must + * be defined in include/uapi/linux/iommufd.h * @iova_to_phys: translate iova to physical address * @enforce_cache_coherency: Prevent any kind of DMA from bypassing IOMMU_CACHE, * including no-snoop TLPs on PCIe or other platform diff --git a/include/linux/iommufd.h b/include/linux/iommufd.h index 6e7efe83bc5d..3087f5b2def8 100644 --- a/include/linux/iommufd.h +++ b/include/linux/iommufd.h @@ -154,6 +154,8 @@ struct iommufd_hw_queue { * The @array passes in the cache invalidation requests, in * form of a driver data structure. A driver must update the * array->entry_num to report the number of handled requests. + * A driver may handle fewer than the requested entry_num, in + * which case the core re-invokes the op for the remainder. * The data structure of the array entry must be defined in * include/uapi/linux/iommufd.h * @vdevice_size: Size of the driver-defined vDEVICE structure per this vIOMMU From 5519695903a4b443903dd6d9207ad340bc3c984b Mon Sep 17 00:00:00 2001 From: Nicolin Chen Date: Sun, 30 Aug 2026 15:26:40 -0700 Subject: [PATCH 05/15] iommufd/selftest: Convert cache invalidation mocks to the core array loop The vIOMMU and the nested-domain selftest invalidation mocks each used to walk the whole request array on their own, with the vIOMMU mock even allocating a buffer sized to the entire array in order to do so first. The iommufd core now iterates the request array itself and re-invokes the op with the not-yet-handled sub-array, so handle just a single request per call out of the front of that sub-array and report one handled entry via the array->entry_num. Drop both of the loops and the kzalloc_objs() in the viommu callback function, and keep returning a success for an empty array as a probe of the selftest data type. Reviewed-by: Kevin Tian Reviewed-by: Pranjal Shrivastava Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Nicolin Chen Link: https://patch.msgid.link/535f3b97e16c437f1df0c988a96229f623142ee9.1788127877.git.nicolinc@nvidia.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/iommufd/selftest.c | 157 +++++++++++++++---------------- 1 file changed, 77 insertions(+), 80 deletions(-) diff --git a/drivers/iommu/iommufd/selftest.c b/drivers/iommu/iommufd/selftest.c index f6da927461b5..25387969ab75 100644 --- a/drivers/iommu/iommufd/selftest.c +++ b/drivers/iommu/iommufd/selftest.c @@ -633,70 +633,63 @@ mock_viommu_alloc_domain_nested(struct iommufd_viommu *viommu, u32 flags, static int mock_viommu_cache_invalidate(struct iommufd_viommu *viommu, struct iommu_user_data_array *array) { - struct iommu_viommu_invalidate_selftest *cmds; - struct iommu_viommu_invalidate_selftest *cur; - struct iommu_viommu_invalidate_selftest *end; - int rc; + struct iommu_viommu_invalidate_selftest cmd; + struct mock_dev *mdev; + struct device *dev; + u32 processed = 0; + int rc = 0; + int i; - /* A zero-length array is allowed to validate the array type */ - if (array->entry_num == 0 && - array->type == IOMMU_VIOMMU_INVALIDATE_DATA_SELFTEST) { - array->entry_num = 0; - return 0; + if (array->type != IOMMU_VIOMMU_INVALIDATE_DATA_SELFTEST) { + rc = -EINVAL; + goto out; } - cmds = kzalloc_objs(*cmds, array->entry_num); - if (!cmds) - return -ENOMEM; - cur = cmds; - end = cmds + array->entry_num; + /* + * The core re-invokes this op for the remaining requests, so handle one + * request per call. A zero-length array only probes the type, validated + * above. + */ + if (!array->entry_num) + return 0; - static_assert(sizeof(*cmds) == 3 * sizeof(u32)); - rc = iommu_copy_struct_from_full_user_array( - cmds, sizeof(*cmds), array, - IOMMU_VIOMMU_INVALIDATE_DATA_SELFTEST); + rc = iommu_copy_struct_from_user_array( + &cmd, array, IOMMU_VIOMMU_INVALIDATE_DATA_SELFTEST, 0, + cache_id); if (rc) goto out; - while (cur != end) { - struct mock_dev *mdev; - struct device *dev; - int i; - - if (cur->flags & ~IOMMU_TEST_INVALIDATE_FLAG_ALL) { - rc = -EOPNOTSUPP; - goto out; - } - - if (cur->cache_id > MOCK_DEV_CACHE_ID_MAX) { - rc = -EINVAL; - goto out; - } - - xa_lock(&viommu->vdevs); - dev = iommufd_viommu_find_dev(viommu, - (unsigned long)cur->vdev_id); - if (!dev) { - xa_unlock(&viommu->vdevs); - rc = -EINVAL; - goto out; - } - mdev = container_of(dev, struct mock_dev, dev); - - if (cur->flags & IOMMU_TEST_INVALIDATE_FLAG_ALL) { - /* Invalidate all cache entries and ignore cache_id */ - for (i = 0; i < MOCK_DEV_CACHE_NUM; i++) - mdev->cache[i] = 0; - } else { - mdev->cache[cur->cache_id] = 0; - } - xa_unlock(&viommu->vdevs); - - cur++; + if (cmd.flags & ~IOMMU_TEST_INVALIDATE_FLAG_ALL) { + rc = -EOPNOTSUPP; + goto out; } + + if (cmd.cache_id > MOCK_DEV_CACHE_ID_MAX) { + rc = -EINVAL; + goto out; + } + + xa_lock(&viommu->vdevs); + dev = iommufd_viommu_find_dev(viommu, (unsigned long)cmd.vdev_id); + if (!dev) { + xa_unlock(&viommu->vdevs); + rc = -EINVAL; + goto out; + } + mdev = container_of(dev, struct mock_dev, dev); + + if (cmd.flags & IOMMU_TEST_INVALIDATE_FLAG_ALL) { + /* Invalidate all cache entries and ignore cache_id */ + for (i = 0; i < MOCK_DEV_CACHE_NUM; i++) + mdev->cache[i] = 0; + } else { + mdev->cache[cmd.cache_id] = 0; + } + xa_unlock(&viommu->vdevs); + + processed = 1; out: - array->entry_num = cur - cmds; - kfree(cmds); + array->entry_num = processed; return rc; } @@ -877,42 +870,46 @@ mock_domain_cache_invalidate_user(struct iommu_domain *domain, struct mock_iommu_domain_nested *mock_nested = to_mock_nested(domain); struct iommu_hwpt_invalidate_selftest inv; u32 processed = 0; - int i = 0, j; int rc = 0; + int i; if (array->type != IOMMU_HWPT_INVALIDATE_DATA_SELFTEST) { rc = -EINVAL; goto out; } - for ( ; i < array->entry_num; i++) { - rc = iommu_copy_struct_from_user_array(&inv, array, - IOMMU_HWPT_INVALIDATE_DATA_SELFTEST, - i, iotlb_id); - if (rc) - break; + /* + * The core re-invokes this op for the remaining requests, so handle one + * request per call. A zero-length array only probes the type, validated + * above. + */ + if (!array->entry_num) + return 0; - if (inv.flags & ~IOMMU_TEST_INVALIDATE_FLAG_ALL) { - rc = -EOPNOTSUPP; - break; - } + rc = iommu_copy_struct_from_user_array( + &inv, array, IOMMU_HWPT_INVALIDATE_DATA_SELFTEST, 0, iotlb_id); + if (rc) + goto out; - if (inv.iotlb_id > MOCK_NESTED_DOMAIN_IOTLB_ID_MAX) { - rc = -EINVAL; - break; - } - - if (inv.flags & IOMMU_TEST_INVALIDATE_FLAG_ALL) { - /* Invalidate all mock iotlb entries and ignore iotlb_id */ - for (j = 0; j < MOCK_NESTED_DOMAIN_IOTLB_NUM; j++) - mock_nested->iotlb[j] = 0; - } else { - mock_nested->iotlb[inv.iotlb_id] = 0; - } - - processed++; + if (inv.flags & ~IOMMU_TEST_INVALIDATE_FLAG_ALL) { + rc = -EOPNOTSUPP; + goto out; } + if (inv.iotlb_id > MOCK_NESTED_DOMAIN_IOTLB_ID_MAX) { + rc = -EINVAL; + goto out; + } + + if (inv.flags & IOMMU_TEST_INVALIDATE_FLAG_ALL) { + /* Invalidate all mock iotlb entries and ignore iotlb_id */ + for (i = 0; i < MOCK_NESTED_DOMAIN_IOTLB_NUM; i++) + mock_nested->iotlb[i] = 0; + } else { + mock_nested->iotlb[inv.iotlb_id] = 0; + } + + processed = 1; out: array->entry_num = processed; return rc; From 2d4f03f6610d569070a86d8d2c583ddfcd390f75 Mon Sep 17 00:00:00 2001 From: Nicolin Chen Date: Sun, 30 Aug 2026 15:26:41 -0700 Subject: [PATCH 06/15] iommu/arm-smmu-v3-iommufd: Convert cache invalidation to the core array loop arm_vsmmu_cache_invalidate() allocated a buffer for the entire user request array, walked the array converting each of the commands, and issued those converted commands to the cmdq in CMDQ_BATCH_ENTRIES sized chunks, carrying the sub-array bookkeeping all on its own. The iommufd core now iterates the invalidation array and re-invokes the op with the not-yet-handled sub-array, so the driver only has to proceed with a single chunk per call. Instead of a per-array allocation, use a fixed on-stack batch to copy from the userspace array. If the copy fails due to nonzero padding (VMM violates the ABI), fail the entire batch. Convert the whole batch before issuing any of it: a malformed command is a userspace bug, so the first illegal command fails the batch as a unit, issuing nothing and leaving array->entry_num at zero, the same way the copy above bails on nonzero padding. A batch that converts cleanly is issued in full, so the op returns either a handled count with no error or zero with an error. A zero-length array now returns success once the data type gets validated, matching the documented probe behavior, rather than the -EINVAL that the full-array copy helper would previously return. This also fixes two long-standing bugs: 1) On a conversion failure the old code reported commands that it had converted but not yet issued, so user space advanced its consumer index past invalidations that never reached the cmdq. 2) A zero-length array was rejected with -EINVAL, although the uAPI documents it as a valid request that only probes the data type. Reviewed-by: Kevin Tian Reviewed-by: Pranjal Shrivastava Reviewed-by: Jason Gunthorpe Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Nicolin Chen Link: https://patch.msgid.link/5223275dbc8ef00af233f3fee01efa09e45f26b5.1788127877.git.nicolinc@nvidia.com Signed-off-by: Jason Gunthorpe --- .../arm/arm-smmu-v3/arm-smmu-v3-iommufd.c | 68 +++++++++++-------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c index 747b9d47ff7e..ab1078a97d80 100644 --- a/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c +++ b/drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3-iommufd.c @@ -478,49 +478,57 @@ int arm_vsmmu_cache_invalidate(struct iommufd_viommu *viommu, struct iommu_user_data_array *array) { struct arm_vsmmu *vsmmu = container_of(viommu, struct arm_vsmmu, core); + struct arm_vsmmu_invalidation_cmd cmds[CMDQ_BATCH_ENTRIES - 1]; struct arm_smmu_device *smmu = vsmmu->smmu; - struct arm_vsmmu_invalidation_cmd *last; - struct arm_vsmmu_invalidation_cmd *cmds; - struct arm_vsmmu_invalidation_cmd *cur; - struct arm_vsmmu_invalidation_cmd *end; + struct iommu_user_data_array batch = { + .type = array->type, + .uptr = array->uptr, + .entry_len = array->entry_len, + }; + u32 processed = 0; int ret; - - cmds = kzalloc_objs(*cmds, array->entry_num); - if (!cmds) - return -ENOMEM; - cur = cmds; - end = cmds + array->entry_num; + u32 i; static_assert(sizeof(*cmds) == 2 * sizeof(u64)); + + if (array->type != IOMMU_VIOMMU_INVALIDATE_DATA_ARM_SMMUV3) { + ret = -EINVAL; + goto out; + } + + /* A zero-length array only probes the type, validated above */ + if (!array->entry_num) + return 0; + + /* + * The core re-invokes this op for the remaining requests, so copy one + * cmdq batch worth of commands into a fixed on-stack buffer rather than + * allocating for the whole array. + */ + batch.entry_num = min_t(u32, array->entry_num, ARRAY_SIZE(cmds)); ret = iommu_copy_struct_from_full_user_array( - cmds, sizeof(*cmds), array, + cmds, sizeof(*cmds), &batch, IOMMU_VIOMMU_INVALIDATE_DATA_ARM_SMMUV3); if (ret) goto out; - last = cmds; - while (cur != end) { - ret = arm_vsmmu_convert_user_cmd(vsmmu, cur); + /* + * Convert the whole batch. Sending an illegal command is a VMM bug, so + * a single one fails the entire batch, issuing nothing. + */ + for (i = 0; i < batch.entry_num; i++) { + ret = arm_vsmmu_convert_user_cmd(vsmmu, &cmds[i]); if (ret) goto out; - - /* FIXME work in blocks of CMDQ_BATCH_ENTRIES and copy each block? */ - cur++; - if (cur != end && (cur - last) != CMDQ_BATCH_ENTRIES - 1) - continue; - - /* FIXME always uses the main cmdq rather than trying to group by type */ - ret = __arm_smmu_cmdq_issue_cmdlist(smmu, &smmu->cmdq, &last->cmd, - cur - last, true); - if (ret) { - cur--; - goto out; - } - last = cur; } + + /* FIXME always uses the main cmdq rather than trying to group by type */ + ret = __arm_smmu_cmdq_issue_cmdlist(smmu, &smmu->cmdq, &cmds->cmd, + batch.entry_num, true); + if (!ret) + processed = batch.entry_num; out: - array->entry_num = cur - cmds; - kfree(cmds); + array->entry_num = processed; return ret; } From fe85c40e992be9578b8cdfa992d9e23005e25e77 Mon Sep 17 00:00:00 2001 From: Nicolin Chen Date: Sun, 30 Aug 2026 15:26:42 -0700 Subject: [PATCH 07/15] iommu/vt-d: Convert nested cache invalidation to the core array loop intel_nested_cache_invalidate_user() used to walk the whole request array on its own, copying and then flushing one single entry at a time. The iommufd core now iterates the request array itself and re-invokes the op with the not-yet-handled sub-array, so handle just a single request per call out of the front of that sub-array and report one handled entry via the array->entry_num. An empty array keeps returning a success, used as a probe of IOMMU_HWPT_INVALIDATE_DATA_VTD_S1 support. Reviewed-by: Kevin Tian Reviewed-by: Lu Baolu Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Nicolin Chen Link: https://patch.msgid.link/1a668edccc94a2469df993d42609b5909f6f1dee.1788127877.git.nicolinc@nvidia.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/intel/nested.c | 50 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/drivers/iommu/intel/nested.c b/drivers/iommu/intel/nested.c index 2b979bec56ce..36e0e8e85c06 100644 --- a/drivers/iommu/intel/nested.c +++ b/drivers/iommu/intel/nested.c @@ -93,7 +93,7 @@ static int intel_nested_cache_invalidate_user(struct iommu_domain *domain, { struct dmar_domain *dmar_domain = to_dmar_domain(domain); struct iommu_hwpt_vtd_s1_invalidate inv_entry; - u32 index, processed = 0; + u32 processed = 0; int ret = 0; if (array->type != IOMMU_HWPT_INVALIDATE_DATA_VTD_S1) { @@ -101,31 +101,37 @@ static int intel_nested_cache_invalidate_user(struct iommu_domain *domain, goto out; } - for (index = 0; index < array->entry_num; index++) { - ret = iommu_copy_struct_from_user_array(&inv_entry, array, - IOMMU_HWPT_INVALIDATE_DATA_VTD_S1, - index, __reserved); - if (ret) - break; + /* + * The core re-invokes this op for the remaining requests, so handle one + * request per call. A zero-length array only probes the type, validated + * above. + */ + if (!array->entry_num) + return 0; - if ((inv_entry.flags & ~IOMMU_VTD_INV_FLAGS_LEAF) || - inv_entry.__reserved) { - ret = -EOPNOTSUPP; - break; - } + ret = iommu_copy_struct_from_user_array( + &inv_entry, array, IOMMU_HWPT_INVALIDATE_DATA_VTD_S1, 0, + __reserved); + if (ret) + goto out; - if (!IS_ALIGNED(inv_entry.addr, VTD_PAGE_SIZE) || - ((inv_entry.npages == U64_MAX) && inv_entry.addr)) { - ret = -EINVAL; - break; - } - - cache_tag_flush_range(dmar_domain, inv_entry.addr, - inv_entry.addr + nrpages_to_size(inv_entry.npages) - 1, - inv_entry.flags & IOMMU_VTD_INV_FLAGS_LEAF); - processed++; + if ((inv_entry.flags & ~IOMMU_VTD_INV_FLAGS_LEAF) || + inv_entry.__reserved) { + ret = -EOPNOTSUPP; + goto out; } + if (!IS_ALIGNED(inv_entry.addr, VTD_PAGE_SIZE) || + (inv_entry.npages == U64_MAX && inv_entry.addr)) { + ret = -EINVAL; + goto out; + } + + cache_tag_flush_range(dmar_domain, inv_entry.addr, + inv_entry.addr + + nrpages_to_size(inv_entry.npages) - 1, + inv_entry.flags & IOMMU_VTD_INV_FLAGS_LEAF); + processed = 1; out: array->entry_num = processed; return ret; From 54dadb030c7e2350957855d3995de05ae02c2e66 Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Sat, 12 Sep 2026 01:18:50 +0200 Subject: [PATCH 08/15] iommufd: Fix iommufd_hw_capabilities reference in kernel-doc The kernel-doc comment of struct iommu_hw_info refers to iommu_hw_capabilities, which does not exist. The name is iommufd_hw_capabilities. Say iommufd_hw_capabilities. Fixes: 7623683857e5 ("iommufd: Add capabilities to IOMMU_GET_HW_INFO") Assisted-by: LLM Signed-off-by: Karl Mehltretter Link: https://patch.msgid.link/20260911231850.20737-1-kmehltretter@gmail.com Reviewed-by: Kevin Tian Signed-off-by: Jason Gunthorpe --- include/uapi/linux/iommufd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/uapi/linux/iommufd.h b/include/uapi/linux/iommufd.h index 5d47d1421c1f..b4c7194cb08d 100644 --- a/include/uapi/linux/iommufd.h +++ b/include/uapi/linux/iommufd.h @@ -776,7 +776,7 @@ enum iommufd_hw_info_flags { * @out_data_type: Output the iommu hardware info type as defined in the enum * iommu_hw_info_type. * @out_capabilities: Output the generic iommu capability info type as defined - * in the enum iommu_hw_capabilities. + * in the enum iommufd_hw_capabilities. * @out_max_pasid_log2: Output the width of PASIDs. 0 means no PASID support. * PCI devices turn to out_capabilities to check if the * specific capabilities is supported or not. From 1e67cca28490f897788c128c650a6f2cbb565403 Mon Sep 17 00:00:00 2001 From: Donggeun Yoo Date: Tue, 29 Sep 2026 12:14:02 +0900 Subject: [PATCH 09/15] iommufd: Keep reserved IOVA ranges when a replace within one IOAS fails iommufd_device_do_replace() installs the group's reserved IOVA ranges in the new hwpt's IOAS only when it differs from the old hwpt's IOAS. If the replace fails in iommufd_device_do_replace() or iommufd_group_do_replace_reserved_iova(), the error paths remove the ranges from the new hwpt's IOAS whether or not the IOAS is shared between the old and new hwpt, which removes the ranges that the old hwpt still needs. For example, on x86 with intel-iommu, after a replace within a shared IOAS fails with -ENODEV, IOMMU_IOAS_IOVA_RANGES goes from 0x0-0xfedfffff 0xfef00000-0xffffffffffff to 0x0-0xffffffffffff and IOMMU_IOAS_MAP at 0xfee00000 succeeds. Remove the ranges on failure only when the replace installed them, and look up the old hwpt_paging before the replace so that the error path can compare the IOAS. Fixes: e88d4ec154a8 ("iommufd: Add iommufd_device_replace()") Assisted-by: LLM Signed-off-by: Donggeun Yoo Link: https://patch.msgid.link/20260929031402.1998253-1-donggeunyoo.kernel@gmail.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/iommufd/device.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/drivers/iommu/iommufd/device.c b/drivers/iommu/iommufd/device.c index 5c4b06eda546..67c2b5df17c6 100644 --- a/drivers/iommu/iommufd/device.c +++ b/drivers/iommu/iommufd/device.c @@ -849,7 +849,8 @@ iommufd_group_do_replace_reserved_iova(struct iommufd_group *igroup, return 0; err_unresv: - iommufd_group_remove_reserved_iova(igroup, hwpt_paging); + if (!old_hwpt_paging || hwpt_paging->ioas != old_hwpt_paging->ioas) + iommufd_group_remove_reserved_iova(igroup, hwpt_paging); return rc; } @@ -889,6 +890,7 @@ iommufd_device_do_replace(struct iommufd_device *idev, ioasid_t pasid, return NULL; } + old_hwpt_paging = find_hwpt_paging(old_hwpt); if (attach_resv) { rc = iommufd_group_do_replace_reserved_iova(igroup, hwpt_paging); if (rc) @@ -899,7 +901,6 @@ iommufd_device_do_replace(struct iommufd_device *idev, ioasid_t pasid, if (rc) goto err_unresv; - old_hwpt_paging = find_hwpt_paging(old_hwpt); if (old_hwpt_paging && pasid == IOMMU_NO_PASID && (!hwpt_paging || hwpt_paging->ioas != old_hwpt_paging->ioas)) iommufd_group_remove_reserved_iova(igroup, old_hwpt_paging); @@ -920,7 +921,8 @@ iommufd_device_do_replace(struct iommufd_device *idev, ioasid_t pasid, /* Caller must destroy old_hwpt */ return old_hwpt; err_unresv: - if (attach_resv) + if (attach_resv && + (!old_hwpt_paging || hwpt_paging->ioas != old_hwpt_paging->ioas)) iommufd_group_remove_reserved_iova(igroup, hwpt_paging); err_unlock: mutex_unlock(&igroup->lock); From 769f2cd9226c96619d77fa16af84de74cae41b6b Mon Sep 17 00:00:00 2001 From: Andrea Parri Date: Thu, 8 Oct 2026 14:52:03 +0200 Subject: [PATCH 10/15] iommufd: Keep dmabuf PFNs MMIO when filling another domain 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 Link: https://lore.kernel.org/all/20260716154123.32DC01F000E9@smtp.kernel.org/ Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Andrea Parri Link: https://patch.msgid.link/20261008125206.11990-2-parri.andrea@gmail.com Signed-off-by: Jason Gunthorpe --- 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 404f31d8f729..2f6153108add 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, From 440ca8a4b69f40088065b9e05ef11373292a254e Mon Sep 17 00:00:00 2001 From: Nirmoy Das Date: Wed, 14 Oct 2026 06:36:32 -0700 Subject: [PATCH 11/15] iommufd: Fix event queue release use-after-free iommufd_eventq_fops_release() drops the file's users reference before reading eventq->ictx. Once that reference is dropped, IOMMU_DESTROY in another thread can remove and free the event queue, so the later load accesses freed memory. Read ictx while the users reference still pins the event queue, then drop the reference and put the saved context. The context reference taken by iommufd_eventq_init() keeps ictx valid until the put. Fault and vEVENTQ file descriptors share this release path. Fixes: 07838f7fd529 ("iommufd: Add iommufd fault object") Cc: stable@vger.kernel.org Assisted-by: LLM, Specula Signed-off-by: Nirmoy Das Link: https://patch.msgid.link/20261014133632.3440297-1-nirmoyd@nvidia.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/iommufd/eventq.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/iommu/iommufd/eventq.c b/drivers/iommu/iommufd/eventq.c index 747dd5155121..b8a54454dd17 100644 --- a/drivers/iommu/iommufd/eventq.c +++ b/drivers/iommu/iommufd/eventq.c @@ -396,9 +396,11 @@ static __poll_t iommufd_eventq_fops_poll(struct file *filep, static int iommufd_eventq_fops_release(struct inode *inode, struct file *filep) { struct iommufd_eventq *eventq = filep->private_data; + /* eventq may be freed once the users reference is dropped. */ + struct iommufd_ctx *ictx = eventq->ictx; refcount_dec(&eventq->obj.users); - iommufd_ctx_put(eventq->ictx); + iommufd_ctx_put(ictx); return 0; } From a6ec38d74c08ddbe1b7c3936d6d25f346d12c7be Mon Sep 17 00:00:00 2001 From: Samarth Narang Date: Tue, 13 Oct 2026 10:10:45 -0700 Subject: [PATCH 12/15] iommufd/iova_bitmap: Fail GET_DIRTY_BITMAP when a bitmap page cannot be pinned The iova_bitmap_set() function reports dirty IOVAs into a user-provided bitmap, it does so by pinning the bitmap pages lazily one chunk at a time as the reported IOVA advances. When advancing to the next chunk fails, the function simply returns without recording the dirty bits. if (iova_bitmap_advance_to(bitmap, iova)) return; So on an IOMMU_HWPT_GET_DIRTY_BITMAP ioctl, if any bitmap chunk past the first cannot be pinned, the dirty pages in that chunk are cleared in the page table, not set in the bitmap, and the ioctl will still return success. This means that the destination VM will observe stale memory. This patch records the first such error in the iova_bitmap and propagates it so the ioctl fails instead of silently succeeding. Fixes: 4a00f9434891 ("iommupt: Add read_and_clear_dirty op") Signed-off-by: Samarth Narang Link: https://patch.msgid.link/20261013171045.3104619-1-samarth.colleges@gmail.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/iommufd/io_pagetable.c | 2 +- drivers/iommu/iommufd/iova_bitmap.c | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/drivers/iommu/iommufd/io_pagetable.c b/drivers/iommu/iommufd/io_pagetable.c index 4e447ce74cf6..e6540b0a2117 100644 --- a/drivers/iommu/iommufd/io_pagetable.c +++ b/drivers/iommu/iommufd/io_pagetable.c @@ -589,7 +589,7 @@ iommu_read_and_clear_dirty(struct iommu_domain *domain, arg.iopt = iopt; arg.domain = domain; arg.dirty = &dirty; - iova_bitmap_for_each(iter, &arg, __iommu_read_and_clear_dirty); + ret = iova_bitmap_for_each(iter, &arg, __iommu_read_and_clear_dirty); if (!(flags & IOMMU_DIRTY_NO_CLEAR)) iommu_iotlb_sync(domain, &gather); diff --git a/drivers/iommu/iommufd/iova_bitmap.c b/drivers/iommu/iommufd/iova_bitmap.c index dac3e657d498..483f0cc84646 100644 --- a/drivers/iommu/iommufd/iova_bitmap.c +++ b/drivers/iommu/iommufd/iova_bitmap.c @@ -116,6 +116,9 @@ struct iova_bitmap { /* length of the IOVA range for the whole bitmap */ size_t length; + + /* first error from pinning a bitmap chunk; -EFAULT etc. */ + int err; }; /* @@ -384,7 +387,9 @@ static int iova_bitmap_advance_to(struct iova_bitmap *bitmap, int iova_bitmap_for_each(struct iova_bitmap *bitmap, void *opaque, iova_bitmap_fn_t fn) { - return fn(bitmap, bitmap->iova, bitmap->length, opaque); + int ret = fn(bitmap, bitmap->iova, bitmap->length, opaque); + + return ret ? ret : bitmap->err; } EXPORT_SYMBOL_NS_GPL(iova_bitmap_for_each, "IOMMUFD"); @@ -411,8 +416,13 @@ void iova_bitmap_set(struct iova_bitmap *bitmap, * may fail if it's out of bounds, or pinning the pages * returns an error. */ - if (iova_bitmap_advance_to(bitmap, iova)) + int ret = iova_bitmap_advance_to(bitmap, iova); + + if (ret) { + if (!bitmap->err) + bitmap->err = ret; return; + } } last_page_idx = mapped->npages - 1; From f6b3fd8f573e801066c21b2aec3d2e949e61e48e Mon Sep 17 00:00:00 2001 From: Andrea Parri Date: Fri, 25 Sep 2026 18:11:39 +0200 Subject: [PATCH 13/15] iommufd: Reject a zero-length dirty bitmap request 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 Link: https://patch.msgid.link/20260925161142.189813-4-parri.andrea@gmail.com Signed-off-by: Jason Gunthorpe --- 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 e6540b0a2117..5ac736cf5bb5 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; From e21952d72b4740f08a272d694cca223b6b0099ca Mon Sep 17 00:00:00 2001 From: Andrea Parri Date: Fri, 25 Sep 2026 18:11:37 +0200 Subject: [PATCH 14/15] iommufd/iova_bitmap: Ensure that iova_bitmap_set() handles the full IOVA Generally userspace is going to step the dirty bitmap through the IOVA space. However if the bitmap ends in the middle of a large PTE this is problematic. The first step will clear the dirty bit and report dirty to the end of its bitmap while the second step will see the clear'd dirty and report clean for the start of the bitmap. If it happens this ends up losing dirties. Further this function is missing bounds checking so if this does happen ther is a good chance it will write memory beyond what the userspace was expecting. Return an error code if this illegal condition happens. Fixes: 58ccf0190d19 ("vfio: Add an IOVA bitmap support") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Andrea Parri Link: https://patch.msgid.link/20260925161142.189813-2-parri.andrea@gmail.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/iommufd/iova_bitmap.c | 12 ++++++++++++ include/uapi/linux/iommufd.h | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/drivers/iommu/iommufd/iova_bitmap.c b/drivers/iommu/iommufd/iova_bitmap.c index 483f0cc84646..953f6a57d0ca 100644 --- a/drivers/iommu/iommufd/iova_bitmap.c +++ b/drivers/iommu/iommufd/iova_bitmap.c @@ -409,6 +409,18 @@ 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 the range cannot be recorded then the system call returns failure. + * This is to prevent loosing dirties if userspace is expecting to step + * the bitmap through the IOVA space. + */ + if (!length || length > bitmap->length || iova < bitmap->iova || + iova - bitmap->iova > bitmap->length - length) { + if (!bitmap->err) + bitmap->err = -EPROTO; + return; + } + update_indexes: if (unlikely(!iova_bitmap_mapped_range(mapped, iova, length))) { /* diff --git a/include/uapi/linux/iommufd.h b/include/uapi/linux/iommufd.h index b4c7194cb08d..010f47f7d3d2 100644 --- a/include/uapi/linux/iommufd.h +++ b/include/uapi/linux/iommufd.h @@ -871,6 +871,12 @@ enum iommufd_hwpt_get_dirty_bitmap_flags { * Walk the IOMMU pagetables for a given IOVA range to return a bitmap * with the dirty IOVAs. In doing so it will also by default clear any * dirty bit metadata set in the IOPTE. + * + * The IOVA range given must fully enclose any IOPTE. If the range starts or + * ends inside a dirty IOPTE then the ioctl will fail. This avoids partially + * reporting a dirty range. + * + * On failure the dirty state becomes unknown and the userspace must restart. */ struct iommu_hwpt_get_dirty_bitmap { __u32 size; From 04bcf84613346b973a8cc0c59238b6b5bc902688 Mon Sep 17 00:00:00 2001 From: Jason Gunthorpe Date: Wed, 14 Oct 2026 21:38:40 -0300 Subject: [PATCH 15/15] iommupt: Do not allow PTEs to be split when dirty logging If a single PTE cannot be stored in the current bitmap that should be a failure and reported back to userspace. Allowing a PTE to be split creates a possibility that dirty memory will be reported as clean. The gather must also cover the entire PTE that is being cleaned, not just the part that fits within the bitmap. Fix both issues by computing the full IOVA range of every PTE and using that to update the bitmap and the gather. The bitmap code now rejects ranges that don't fit in it, and the gather always combined ranges. The selftest quietly relied on a bug where the CONT was split, that is why the half page size for the mock domain saw alternating dirties. Increase the dirty test page size to PAGE_SIZE so it still works as intended. Cc: stable@vger.kernel.org Fixes: 4a00f9434891 ("iommupt: Add read_and_clear_dirty op") Reported-by: Samarth Narang Closes: https://lore.kernel.org/all/20261013165814.309255 Link: https://patch.msgid.link/r/0-v1-5bb926ff73ec+576-iommufd_dirty_jgg@nvidia.com Signed-off-by: Jason Gunthorpe --- drivers/iommu/generic_pt/iommu_pt.h | 30 +++++++------------------ tools/testing/selftests/iommu/iommufd.c | 10 ++++----- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/drivers/iommu/generic_pt/iommu_pt.h b/drivers/iommu/generic_pt/iommu_pt.h index 07ec2b3ab986..837d835e4fad 100644 --- a/drivers/iommu/generic_pt/iommu_pt.h +++ b/drivers/iommu/generic_pt/iommu_pt.h @@ -240,27 +240,14 @@ struct pt_iommu_dirty_args { }; static void record_dirty(struct pt_state *pts, - struct pt_iommu_dirty_args *dirty, - unsigned int num_contig_lg2) + struct pt_iommu_dirty_args *dirty) { - pt_vaddr_t dirty_len; - - if (num_contig_lg2 != ilog2(1)) { - unsigned int index = pts->index; - unsigned int end_index = log2_set_mod_max_t( - unsigned int, pts->index, num_contig_lg2); - - /* Adjust for being contained inside a contiguous page */ - end_index = min(end_index, pts->end_index); - dirty_len = (end_index - index) * - log2_to_int(pt_table_item_lg2sz(pts)); - } else { - dirty_len = log2_to_int(pt_table_item_lg2sz(pts)); - } + unsigned int lg2sz = pt_entry_oa_lg2sz(pts); + pt_vaddr_t start_va = log2_set_mod(pts->range->va, 0, lg2sz); + pt_vaddr_t dirty_len = log2_to_int(lg2sz); if (dirty->dirty->bitmap) - iova_bitmap_set(dirty->dirty->bitmap, pts->range->va, - dirty_len); + iova_bitmap_set(dirty->dirty->bitmap, start_va, dirty_len); if (!(dirty->flags & IOMMU_DIRTY_NO_CLEAR)) { /* @@ -268,8 +255,8 @@ static void record_dirty(struct pt_state *pts, * dirty tracking bits can't work together */ pt_entry_make_write_clean(pts); - iommu_iotlb_gather_add_range(dirty->dirty->gather, - pts->range->va, dirty_len); + iommu_iotlb_gather_add_range(dirty->dirty->gather, start_va, + dirty_len); } } @@ -289,8 +276,7 @@ static inline int __read_and_clear_dirty(struct pt_range *range, void *arg, continue; } if (pts.type == PT_ENTRY_OA && pt_entry_is_write_dirty(&pts)) - record_dirty(&pts, dirty, - pt_entry_num_contig_lg2(&pts)); + record_dirty(&pts, dirty); } return 0; } diff --git a/tools/testing/selftests/iommu/iommufd.c b/tools/testing/selftests/iommu/iommufd.c index 44193d171ba9..ca2b35e9d443 100644 --- a/tools/testing/selftests/iommu/iommufd.c +++ b/tools/testing/selftests/iommu/iommufd.c @@ -2175,9 +2175,9 @@ FIXTURE_SETUP(iommufd_dirty_tracking) test_ioctl_ioas_alloc(&self->ioas_id); /* - * For dirty testing it is important that the page size fed into - * the iommu page tables matches the size the dirty logic - * expects, or set_dirty can touch too much stuff. + * Disabling huge pages maps in PAGE_SIZE units, which can span + * multiple contiguous MOCK_PAGE_SIZE entries. The dirty tests must + * expect the entire mapping to be reported dirty. */ cmd.object_id = self->ioas_id; if (!variant->hugepages) @@ -2335,7 +2335,7 @@ TEST_F(iommufd_dirty_tracking, device_dirty_capability) TEST_F(iommufd_dirty_tracking, get_dirty_bitmap) { - uint32_t page_size = MOCK_PAGE_SIZE; + uint32_t page_size = PAGE_SIZE; uint32_t ioas_id = self->ioas_id; uint32_t hwpt_id; @@ -2377,7 +2377,7 @@ TEST_F(iommufd_dirty_tracking, get_dirty_bitmap) TEST_F(iommufd_dirty_tracking, get_dirty_bitmap_no_clear) { - uint32_t page_size = MOCK_PAGE_SIZE; + uint32_t page_size = PAGE_SIZE; uint32_t ioas_id = self->ioas_id; uint32_t hwpt_id;