Skip to contentSkip to conversation

iommu: Fix attach handle publication ordering

PATCH Superseded Nicolin Chen 5 participants 23 messages Sep 29 – Nov 2 IOMMU

v1 of 3 Superseded Latest is v3 →

lore
Share & tools
lore
#0 lore raw

cover iommu: Fix attach handle publication ordering

Jason pointed out an issue in the attach handle replacing path:
https://lore.kernel.org/linux-iommu/[email protected]/

When an attachment is detached or replaced, the IOMMU core can still leave the outgoing attach handle published while calling into the driver. A fault report looks up the handle without the group mutex, so it can find the old handle after the driver's fault flush and just before the caller frees it.

These four patches unpublish the outgoing handle before a driver callback for both group and PASID detach and replace operations. Both replace paths store XA_ZERO_ENTRY to reserve the slot and restore the old entry on error.

This blocks new lookups, but does not retire a fault that already obtained the old handle. The driver must still synchronize those readers before the handle is freed. A same-domain replacement skips the driver callback, so it still needs a separate lifetime mechanism or fence.

This is on GitHub:
https://github.com/nicolinc/iommufd/commits/fix_iommu_attach_handle-v1

Nicolin Chen (4):
iommu: Unpublish the attach handle before the group detach callback
iommu: Unpublish the attach handle before the PASID detach callback
iommu: Unpublish the old attach handle before the group replace
callback
iommu: Unpublish the old attach handle before the PASID replace
callback

 drivers/iommu/iommu.c | 54 ++++++++++++++++++++++++++++---------------
 1 file changed, 35 insertions(+), 19 deletions(-)

base-commit: cee9395acd8043be0644b25c34bfa86623f2b935

Signature · 1 line
2.43.0
#1 lore raw

PATCH 1/4 iommu: Unpublish the attach handle before the group detach callback

The group->pasid_array entry is erased only after the detach callback has returned, so the outgoing attach handle stays published for the whole of that call. iommu_attach_handle_get() reads it under the xa_lock alone, not under the group mutex, so a fault raised in the meantime still finds that handle and the domain being detached, both of which the caller frees the moment that detach returns.

Commit 5e9f822c9c68 ("iommu: Swap the order of setting group->pasid_array and calling attach op of iommu drivers") has fixed the attach path, while the detach path never got the symmetric change.

Erase the entry ahead of the callback instead. A missing entry reads back as NULL from xa_load(), so that iommu_attach_handle_get() returns -ENOENT and the fault gets rejected rather than delivered against a domain that is on its way out.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle support")
Assisted-by LLM
---
 drivers/iommu/iommu.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
drivers/iommu/iommu.c +5 -1
index cd1bca7ede9af..a062a84686c29 100644
@@ -3951,8 +3951,12 @@ void iommu_detach_group_handle(struct iommu_domain *domain,
 			       struct iommu_group *group)
 {
 	mutex_lock(&group->mutex);
-	__iommu_group_set_core_domain(group);
+	/*
+	 * Unpublish the handle first, so it would not resolve to the detaching
+	 * domain once the driver is detaching it.
+	 */
 	xa_erase(&group->pasid_array, IOMMU_NO_PASID);
+	__iommu_group_set_core_domain(group);
 	mutex_unlock(&group->mutex);
 }
 EXPORT_SYMBOL_NS_GPL(iommu_detach_group_handle, "IOMMUFD_INTERNAL");
Signature · 1 line
2.43.0
Reviewed-by on v1 1/4
#2 lore raw

RE: [PATCH v1 1/4] iommu: Unpublish the attach handle before the group detach callback

Quoted 19 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

The group->pasid_array entry is erased only after the detach callback has returned, so the outgoing attach handle stays published for the whole of that call. iommu_attach_handle_get() reads it under the xa_lock alone, not under the group mutex, so a fault raised in the meantime still finds that handle and the domain being detached, both of which the caller frees the moment that detach returns.

Commit 5e9f822c9c68 ("iommu: Swap the order of setting group->pasid_array and calling attach op of iommu drivers") has fixed the attach path, while the detach path never got the symmetric change.

Erase the entry ahead of the callback instead. A missing entry reads back as NULL from xa_load(), so that iommu_attach_handle_get() returns -ENOENT and the fault gets rejected rather than delivered against a domain that is on its way out.

Quoted 5 lines
Fixes 8519e689834a ("iommu: Extend domain attach group with handle

support")

Reviewed-by on v1 1/4
#3 lore raw

Re: [PATCH v1 1/4] iommu: Unpublish the attach handle before the group detach callback

In reply to Nicolin Chen
Quoted 11 lines · Nicolin Chen

The group->pasid_array entry is erased only after the detach callback has returned, so the outgoing attach handle stays published for the whole of that call. iommu_attach_handle_get() reads it under the xa_lock alone, not under the group mutex, so a fault raised in the meantime still finds that handle and the domain being detached, both of which the caller frees the moment that detach returns.

Commit 5e9f822c9c68 ("iommu: Swap the order of setting group->pasid_array and calling attach op of iommu drivers") has fixed the attach path, while the detach path never got the symmetric change.

Quoted 8 lines

Erase the entry ahead of the callback instead. A missing entry reads back as NULL from xa_load(), so that iommu_attach_handle_get() returns -ENOENT and the fault gets rejected rather than delivered against a domain that is on its way out.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle support")
Assisted-by LLM

This tag is no longer necessary.

Quoted 13 lines
---
  drivers/iommu/iommu.c | 6 +++++-
  1 file changed, 5 insertions(+), 1 deletion(-)
drivers/iommu/iommu.c +0 -0
index cd1bca7ede9af..a062a84686c29 100644
@@ -3951,8 +3951,12 @@ void iommu_detach_group_handle(struct iommu_domain *domain,
  			       struct iommu_group *group)
  {
  	mutex_lock(&group->mutex);

drivers/iommu/iommu.c void iommu_detach_group_handle(struct iommu_domain *domain, · line 3962

Quoted 10 lines
drivers/iommu/iommu.c +5 -1
-	__iommu_group_set_core_domain(group);
+	/*
+	 * Unpublish the handle first, so it would not resolve to the detaching
+	 * domain once the driver is detaching it.
+	 */
  	xa_erase(&group->pasid_array, IOMMU_NO_PASID);
+	__iommu_group_set_core_domain(group);
  	mutex_unlock(&group->mutex);
  }
  EXPORT_SYMBOL_NS_GPL(iommu_detach_group_handle, "IOMMUFD_INTERNAL");
#4 lore raw

PATCH 2/4 iommu: Unpublish the attach handle before the PASID detach callback

In reply to Nicolin Chen

iommu_detach_device_pasid() erases the group->pasid_array entry only once __iommu_remove_group_pasid() has returned, so the detaching handle stays visible to iommu_attach_handle_get() for the whole of that callback, and the caller frees it as soon as the detach returns.

Erase the entry ahead of the callback, for the same reason the group path
does: a fault raised in the meantime must not resolve to a domain that is on its way out.

Fixes 16603704559c ("iommu: Add attach/detach_dev_pasid iommu interfaces")
Assisted-by LLM
---
 drivers/iommu/iommu.c | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
drivers/iommu/iommu.c +5 -1
index a062a84686c29..19b880d23314b 100644
@@ -3818,8 +3818,12 @@ void iommu_detach_device_pasid(struct iommu_domain *domain, struct device *dev,
 	struct iommu_group *group = dev->iommu_group;
 
 	mutex_lock(&group->mutex);
-	__iommu_remove_group_pasid(group, pasid, domain);
+	/*
+	 * Unpublish the handle first, so it would not resolve to the detaching
+	 * domain once the driver is detaching it.
+	 */
 	xa_erase(&group->pasid_array, pasid);
+	__iommu_remove_group_pasid(group, pasid, domain);
 	mutex_unlock(&group->mutex);
 }
 EXPORT_SYMBOL_GPL(iommu_detach_device_pasid);
Signature · 1 line
2.43.0
Reviewed-by on v1 2/4
#5 lore raw

RE: [PATCH v1 2/4] iommu: Unpublish the attach handle before the PASID detach callback

Quoted 12 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

iommu_detach_device_pasid() erases the group->pasid_array entry only once __iommu_remove_group_pasid() has returned, so the detaching handle stays visible to iommu_attach_handle_get() for the whole of that callback, and the caller frees it as soon as the detach returns.

Erase the entry ahead of the callback, for the same reason the group path
does: a fault raised in the meantime must not resolve to a domain that is on its way out.

Quoted 5 lines
Fixes 16603704559c ("iommu: Add attach/detach_dev_pasid iommu

interfaces")

Reviewed-by on v1 2/4
#6 lore raw

Re: [PATCH v1 2/4] iommu: Unpublish the attach handle before the PASID detach callback

In reply to Nicolin Chen
Quoted 5 lines · Nicolin Chen

iommu_detach_device_pasid() erases the group->pasid_array entry only once __iommu_remove_group_pasid() has returned, so the detaching handle stays visible to iommu_attach_handle_get() for the whole of that callback, and the caller frees it as soon as the detach returns.

Quoted 7 lines

Erase the entry ahead of the callback, for the same reason the group path
does: a fault raised in the meantime must not resolve to a domain that is on its way out.

Fixes 16603704559c ("iommu: Add attach/detach_dev_pasid iommu interfaces")
Assisted-by LLM

ditto

Quoted 13 lines
---
  drivers/iommu/iommu.c | 6 +++++-
  1 file changed, 5 insertions(+), 1 deletion(-)
drivers/iommu/iommu.c +0 -0
index a062a84686c29..19b880d23314b 100644
@@ -3818,8 +3818,12 @@ void iommu_detach_device_pasid(struct iommu_domain *domain, struct device *dev,
  	struct iommu_group *group = dev->iommu_group;
 
  	mutex_lock(&group->mutex);

drivers/iommu/iommu.c void iommu_detach_device_pasid(struct iommu_domain *domain, struct device *dev, · line 3829

Quoted 10 lines
drivers/iommu/iommu.c +5 -1
-	__iommu_remove_group_pasid(group, pasid, domain);
+	/*
+	 * Unpublish the handle first, so it would not resolve to the detaching
+	 * domain once the driver is detaching it.
+	 */
  	xa_erase(&group->pasid_array, pasid);
+	__iommu_remove_group_pasid(group, pasid, domain);
  	mutex_unlock(&group->mutex);
  }
  EXPORT_SYMBOL_GPL(iommu_detach_device_pasid);
#7 lore raw

PATCH 3/4 iommu: Unpublish the old attach handle before the group replace callback

In reply to Nicolin Chen

iommu_replace_group_handle() looks like it hides the detaching handle ahead of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace, there must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays into a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain() and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

  xa_reserve()                    ,-- old handle stays published
    no-op, the slot is occupied   |
  __iommu_group_set_domain()      |
    driver attach/detach ops      |
        iopf_queue_flush_dev()    |
                                  |   iommu_attach_handle_get()
                                  |     reads old_handle
  xa_store(new entry)             `-- window closes
  mutex_unlock()
  kfree(old_handle)  [caller]
                                  UAF: old_handle->domain->iopf_handler()

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish the old handle.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle support")
Assisted-by LLM
---
 drivers/iommu/iommu.c | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)
drivers/iommu/iommu.c +13 -10
index 19b880d23314b..52e59f38cec18 100644
@@ -3985,7 +3985,7 @@ int iommu_replace_group_handle(struct iommu_group *group,
 			       struct iommu_domain *new_domain,
 			       struct iommu_attach_handle *handle)
 {
-	void *curr, *entry;
+	void *curr, *entry, *old;
 	int ret;
 
 	if (!new_domain || !handle)
@@ -3993,22 +3993,25 @@ int iommu_replace_group_handle(struct iommu_group *group,
 
 	mutex_lock(&group->mutex);
 	entry = iommu_make_pasid_array_entry(new_domain, handle);
-	ret = xa_reserve(&group->pasid_array, IOMMU_NO_PASID, GFP_KERNEL);
-	if (ret)
+	/*
+	 * iommu_attach_handle_get() runs without the group mutex, so unpublish
+	 * the old handle before the driver callback: a fault must not resolve
+	 * to either domain while the switch is in progress. This reserves the
+	 * slot too, so the store below cannot fail.
+	 */
+	old = xa_store(&group->pasid_array, IOMMU_NO_PASID, XA_ZERO_ENTRY,
+		       GFP_KERNEL);
+	if (xa_is_err(old)) {
+		ret = xa_err(old);
 		goto err_unlock;
+	}
 
 	ret = __iommu_group_set_domain(group, new_domain);
 	if (ret)
-		goto err_release;
+		entry = old; /* Restore the old handle */
 
 	curr = xa_store(&group->pasid_array, IOMMU_NO_PASID, entry, GFP_KERNEL);
 	WARN_ON(xa_is_err(curr));
-
-	mutex_unlock(&group->mutex);
-
-	return 0;
-err_release:
-	xa_release(&group->pasid_array, IOMMU_NO_PASID);
 err_unlock:
 	mutex_unlock(&group->mutex);
 	return ret;
Signature · 1 line
2.43.0
Reviewed-by on v1 3/4
#8 lore raw

RE: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 30 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

iommu_replace_group_handle() looks like it hides the detaching handle ahead of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace, there must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays into a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain()
and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

  xa_reserve()                    ,-- old handle stays published
    no-op, the slot is occupied   |
  __iommu_group_set_domain()      |
    driver attach/detach ops      |
        iopf_queue_flush_dev()    |
                                  |   iommu_attach_handle_get()
                                  |     reads old_handle
  xa_store(new entry)             `-- window closes
  mutex_unlock()
  kfree(old_handle)  [caller]
                                  UAF: old_handle->domain->iopf_handler()
Quoted 8 lines

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish the old handle.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle

support")

btw I just realized that neither old code nor this fix can actually support replacement with lossless fault handling, but I don't recall whether it was the original goal (then destroyed by a following change) or intended to be a future enhancement...

#9 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 39 lines · Kevin
Quoted 30 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

iommu_replace_group_handle() looks like it hides the detaching handle ahead of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace, there must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays into a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain()
and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

   xa_reserve()                    ,-- old handle stays published
     no-op, the slot is occupied   |
   __iommu_group_set_domain()      |
     driver attach/detach ops      |
         iopf_queue_flush_dev()    |
                                   |   iommu_attach_handle_get()
                                   |     reads old_handle
   xa_store(new entry)             `-- window closes
   mutex_unlock()
   kfree(old_handle)  [caller]
                                   UAF: old_handle->domain->iopf_handler()
Quoted 8 lines

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish the old handle.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle

support")

Quoted 6 lines

btw I just realized that neither old code nor this fix can actually support replacement with lossless fault handling, but I don't recall whether it was the original goal (then destroyed by a following change) or intended to be a future enhancement...

There was a related discussion[1]. With this patch, a page request in the replace window gets IOMMU_PAGE_RESP_INVALID instead of being serviced - spec only says re-issuing the same PRG won't succeed unless the mapping changes, it doesn't forbid retry. So I think this is just a more conservative instance of the failure case Jason already called OK in [1], not a new hazard.

[1] https://lore.kernel.org/linux-iommu/[email protected]/

Regards,
Yi Liu

#10 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 54 lines · Yi Liu
Quoted 39 lines · Kevin
Quoted 30 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

iommu_replace_group_handle() looks like it hides the detaching handle ahead of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace, there must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays into a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain()
and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

   xa_reserve()                    ,-- old handle stays published
     no-op, the slot is occupied   |
   __iommu_group_set_domain()      |
     driver attach/detach ops      |
         iopf_queue_flush_dev()    |
                                   |   iommu_attach_handle_get()
                                   |     reads old_handle
   xa_store(new entry)             `-- window closes
   mutex_unlock()
   kfree(old_handle)  [caller]
                                   UAF: old_handle->domain->iopf_handler()
Quoted 8 lines

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish the old handle.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle

support")

Quoted 6 lines

btw I just realized that neither old code nor this fix can actually support replacement with lossless fault handling, but I don't recall whether it was the original goal (then destroyed by a following change) or intended to be a future enhancement...

There was a related discussion[1]. With this patch, a page request in the replace window gets IOMMU_PAGE_RESP_INVALID instead of being serviced - spec only says re-issuing the same PRG won't succeed unless the mapping changes, it doesn't forbid retry. So I think this is just a more conservative instance of the failure case Jason already called OK in [1], not a new hazard.

Quoted 2 lines

[1]
https://lore.kernel.org/linux-iommu/[email protected]/

I called using either the old or new translation OK, using neither seems suboptimal. So returning IOMMU_PAGE_RESP_INVALID doesn't sound ideal..

Jason

#11 lore raw

RE: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 62 lines

From: Jason Gunthorpe <[email protected]>
Sent: Friday, October 30, 2026 2:46 AM

Quoted 5 lines · Yi Liu
Quoted 4 lines · Kevin
Quoted 4 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

iommu_replace_group_handle() looks like it hides the detaching handle

ahead

Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace,

there

Quoted 2 lines
Quoted 2 lines
Quoted 2 lines

must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays

into

Quoted 22 lines
Quoted 22 lines
Quoted 22 lines

a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain()
and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

   xa_reserve()                    ,-- old handle stays published
     no-op, the slot is occupied   |
   __iommu_group_set_domain()      |
     driver attach/detach ops      |
         iopf_queue_flush_dev()    |
                                   |   iommu_attach_handle_get()
                                   |     reads old_handle
   xa_store(new entry)             `-- window closes
   mutex_unlock()
   kfree(old_handle)  [caller]
                                   UAF: old_handle->domain->iopf_handler()
Quoted 1 line
Quoted 1 line
Quoted 1 line

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish

the old

Quoted 22 lines
Quoted 8 lines
Quoted 7 lines

handle.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle

support")

Quoted 6 lines

btw I just realized that neither old code nor this fix can actually support replacement with lossless fault handling, but I don't recall whether it was the original goal (then destroyed by a following change) or intended to be a future enhancement...

There was a related discussion[1]. With this patch, a page request in the replace window gets IOMMU_PAGE_RESP_INVALID instead of being serviced - spec only says re-issuing the same PRG won't succeed unless the mapping changes, it doesn't forbid retry. So I think this is just a more conservative instance of the failure case Jason already called OK in [1], not a new hazard.

Quoted 8 lines
Quoted 2 lines

[1]
https://lore.kernel.org/linux-

iommu/[email protected]/

I called using either the old or new translation OK, using neither seems suboptimal. So returning IOMMU_PAGE_RESP_INVALID doesn't sound ideal..

This probably need a RCU so the old domain can continue to be referenced by in-fly faults in that window...

#12 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

In reply to Jason Gunthorpe
Quoted 55 lines · Jason Gunthorpe
Quoted 54 lines · Yi Liu
Quoted 39 lines · Kevin
Quoted 30 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

iommu_replace_group_handle() looks like it hides the detaching handle ahead of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace, there must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays into a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain()
and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

xa_reserve() ,-- old handle stays published
no-op, the slot is occupied |
__iommu_group_set_domain() |
driver attach/detach ops |
iopf_queue_flush_dev() |
| iommu_attach_handle_get()
| reads old_handle
xa_store(new entry) `-- window closes
mutex_unlock()
kfree(old_handle) [caller]
UAF: old_handle->domain->iopf_handler()

Quoted 8 lines

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish the old handle.

Fixes 8519e689834a ("iommu: Extend domain attach group with handle

support")

Quoted 6 lines

btw I just realized that neither old code nor this fix can actually support replacement with lossless fault handling, but I don't recall whether it was the original goal (then destroyed by a following change) or intended to be a future enhancement...

There was a related discussion[1]. With this patch, a page request in the replace window gets IOMMU_PAGE_RESP_INVALID instead of being serviced - spec only says re-issuing the same PRG won't succeed unless the mapping changes, it doesn't forbid retry. So I think this is just a more conservative instance of the failure case Jason already called OK in [1], not a new hazard.

Quoted 6 lines
Quoted 2 lines

[1]
https://lore.kernel.org/linux-iommu/[email protected]/

I called using either the old or new translation OK, using neither seems suboptimal. So returning IOMMU_PAGE_RESP_INVALID doesn't sound ideal..

I see. maybe we can have a interim state for the replace window. This state responds PageRequests with success for the device. The device shall retry DMA and re-trigger PageRequests again. Once the replacement is done, PageRequests should be forwarded to the correct domain and get resolved.

Regards,
Yi Liu

#13 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 9 lines · Yi Liu
Quoted 3 lines

I called using either the old or new translation OK, using neither seems suboptimal. So returning IOMMU_PAGE_RESP_INVALID doesn't sound ideal..

I see. maybe we can have a interim state for the replace window. This state responds PageRequests with success for the device. The device shall retry DMA and re-trigger PageRequests again. Once the replacement is done, PageRequests should be forwarded to the correct domain and get resolved.

Yeah, maybe we can drop in a dummy domain that does that somehow..

But that seems like another series after this

Jason

Reviewed-by on v1 3/4
#14 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

In reply to Nicolin Chen
Quoted 29 lines · Nicolin Chen

iommu_replace_group_handle() looks like it hides the detaching handle ahead of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace, there must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays into a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain() and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

   xa_reserve()                    ,-- old handle stays published
     no-op, the slot is occupied   |
   __iommu_group_set_domain()      |
     driver attach/detach ops      |
         iopf_queue_flush_dev()    |
                                   |   iommu_attach_handle_get()
                                   |     reads old_handle
   xa_store(new entry)             `-- window closes
   mutex_unlock()
   kfree(old_handle)  [caller]
                                   UAF: old_handle->domain->iopf_handler()

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish the old handle.

Quoted 7 lines
Fixes 8519e689834a ("iommu: Extend domain attach group with handle support")
Assisted-by LLM
---
  drivers/iommu/iommu.c | 23 +++++++++++++----------
  1 file changed, 13 insertions(+), 10 deletions(-)

This doesn't fix the same-domain replace issue, which can still result in a use-after-free bug. I agree that this needs a separate lifetime mechanism as stated in the cover letter. But is it possible to prevent the same-domain replace case from going through this interface?

For this patch alone,

#15 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 39 lines · Baolu Lu
Quoted 30 lines · Nicolin Chen

iommu_replace_group_handle() looks like it hides the detaching handle ahead of the driver callback. But in fact, xa_reserve() calls xa_cmpxchg(), which stores only when the current entry matches @old=NULL. For a replace, there must be an entry sitting there by definition (i.e. @old cannot be NULL), so the compare fails, XA_ZERO_ENTRY is never written, and the call decays into a plain read.

Therefore, the old handle stays published during __iommu_group_set_domain() and only gets replaced after the call. This creates a window, during which an asynchronous fault path might hit UAF:

   core, holding group->mutex          fault path, no group->mutex ==========================          ===========================

   xa_reserve()                    ,-- old handle stays published no-op, the slot is occupied   | __iommu_group_set_domain()      |
     driver attach/detach ops      | iopf_queue_flush_dev()    |
                                   |   iommu_attach_handle_get() |     reads old_handle xa_store(new entry)             `-- window closes mutex_unlock()
   kfree(old_handle)  [caller]
                                   UAF: old_handle->domain-

Quoted 1 line

iopf_handler()

Replace the xa_reserve() with xa_store(XA_ZERO_ENTRY) to unpublish the old handle.

Quoted 7 lines
Fixes 8519e689834a ("iommu: Extend domain attach group with handle support")
Assisted-by LLM

---
  drivers/iommu/iommu.c | 23 +++++++++++++----------

  1 file changed, 13 insertions(+), 10 deletions(-)
Quoted 4 lines

This doesn't fix the same-domain replace issue, which can still result in a use-after-free bug. I agree that this needs a separate lifetime mechanism as stated in the cover letter. But is it possible to prevent the same-domain replace case from going through this interface?

fwiw, iommufd already guards this at the device-replace layer
(iommufd_device_do_replace() short-circuits when hwpt == old_hwpt) [1],
so IOMMUFD callers can't currently trigger this. But that's a caller-side convention, not something iommu_replace_group_handle()/iommu_replace_device_pasid() enforce themselves - so it's still reachable if a future/other caller doesn't do the same check. Worth
deciding whether to push the guard down into the core API, or document
it as a caller contract.

[1] https://github.com/torvalds/linux/blob/6628e7d4c6b25c8d18f1345e3a20d21829aae7e4/drivers/iommu/iommufd/device.c#L888

Regards,
Yi Liu

#16 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 6 lines · Yi Liu
Quoted 4 lines · Baolu Lu

This doesn't fix the same-domain replace issue, which can still result in a use-after-free bug. I agree that this needs a separate lifetime mechanism as stated in the cover letter. But is it possible to prevent the same-domain replace case from going through this interface?

Quoted 9 lines

fwiw, iommufd already guards this at the device-replace layer (iommufd_device_do_replace() short-circuits when hwpt == old_hwpt) [1], so IOMMUFD callers can't currently trigger this. But that's a caller-side convention, not something iommu_replace_group_handle()/iommu_replace_device_pasid() enforce themselves
- so it's still
reachable if a future/other caller doesn't do the same check. Worth deciding whether to push the guard down into the core API, or document it as a caller contract.

I can add a fifth patch. Please see if it looks good to you.

Subject: [PATCH] iommu: Reject a same-domain replace in the attach handle APIs

Both iommu_replace_group_handle() and iommu_replace_device_pasid() skip the driver callback when the new domain is the one currently attached. In that case, nothing flushes the pending faults that may still be holding the old handle. Then, the caller frees the old handle once the replace returns, so such a fault would hit a use-after-free.

No caller does this today, since iommufd_device_do_replace() returns early when the new hwpt is the old one. However, it is only a caller convention, which neither API enforces.

Reject a same-domain replace in both APIs with a WARN_ON, and document it in their kernel-docs.

---
 drivers/iommu/iommu.c | 34 +++++++++++++++++++++++++++-------
 1 file changed, 27 insertions(+), 7 deletions(-)
drivers/iommu/iommu.c +27 -7
index 3fea48d3538ad..a9bc4ca5dbb4e 100644
@@ -3716,6 +3716,10 @@ EXPORT_SYMBOL_GPL(iommu_attach_device_pasid);
  * Caller should always provide a new handle to avoid race with the paths
  * that have lockless reference to handle if it intends to pass a valid handle.
  *
+ * The @domain must not be the domain currently attached to the @pasid. Such a
+ * same-domain replace would skip the driver callback flushing pending faults.
+ * Then, those faults could use the old handle after the caller frees it.
+ *
  * Return 0 on success, or an error.
  */
 int iommu_replace_device_pasid(struct iommu_domain *domain,
@@ -3785,15 +3789,17 @@ int iommu_replace_device_pasid(struct iommu_domain *domain,
 	}
 
 	curr_domain = pasid_array_entry_to_domain(curr);
-	ret = 0;
-
-	if (curr_domain != domain) {
-		ret = __iommu_set_group_pasid(domain, group,
-					      pasid, curr_domain);
-		if (ret)
-			entry = curr; /* restore the old handle */
+	/* Same as iommu_replace_group_handle(), reject a same-domain replace */
+	if (WARN_ON(curr_domain == domain)) {
+		entry = curr; /* restore the old handle */
+		ret = -EINVAL;
+		goto out_store;
 	}
 
+	ret = __iommu_set_group_pasid(domain, group, pasid, curr_domain);
+	if (ret)
+		entry = curr; /* restore the old handle */
+
 out_store:
 	/*
 	 * The xa_store() above reserved the memory, and the group->mutex
@@ -3985,6 +3991,10 @@ EXPORT_SYMBOL_NS_GPL(iommu_detach_group_handle, "IOMMUFD_INTERNAL");
  *
  * Caller should always provide a new handle to avoid race with the paths
  * that have lockless reference to handle.
+ *
+ * The @new_domain must not be the domain currently attached to the group. Such
+ * a same-domain replace would skip the driver callback flushing pending faults.
+ * Then, those faults could use the old handle after the caller frees it.
  */
 int iommu_replace_group_handle(struct iommu_group *group,
 			       struct iommu_domain *new_domain,
@@ -3997,6 +4007,16 @@ int iommu_replace_group_handle(struct iommu_group *group,
 		return -EINVAL;
 
 	mutex_lock(&group->mutex);
+	/*
+	 * A same-domain replace would skip the driver callback flushing pending
+	 * faults. Then, those faults could use the old handle after the caller
+	 * frees it.
+	 */
+	if (WARN_ON(group->domain == new_domain)) {
+		ret = -EINVAL;
+		goto err_unlock;
+	}
+
 	entry = iommu_make_pasid_array_entry(new_domain, handle);
 	/*
 	 * iommu_attach_handle_get() runs without the group mutex, so unpublish
Signature · 2 lines
Thanks
Nicolin
#17 lore raw

Re: [PATCH v1 3/4] iommu: Unpublish the old attach handle before the group replace callback

Quoted 96 lines · Nicolin Chen
Quoted 5 lines · Yi Liu
Quoted 4 lines · Baolu Lu

This doesn't fix the same-domain replace issue, which can still result in a use-after-free bug. I agree that this needs a separate lifetime mechanism as stated in the cover letter. But is it possible to prevent the same-domain replace case from going through this interface?

Quoted 9 lines

fwiw, iommufd already guards this at the device-replace layer (iommufd_device_do_replace() short-circuits when hwpt == old_hwpt) [1], so IOMMUFD callers can't currently trigger this. But that's a caller-side convention, not something iommu_replace_group_handle()/iommu_replace_device_pasid() enforce themselves
- so it's still
reachable if a future/other caller doesn't do the same check. Worth deciding whether to push the guard down into the core API, or document it as a caller contract.

I can add a fifth patch. Please see if it looks good to you.

Subject: [PATCH] iommu: Reject a same-domain replace in the attach handle APIs

Both iommu_replace_group_handle() and iommu_replace_device_pasid() skip the driver callback when the new domain is the one currently attached. In that case, nothing flushes the pending faults that may still be holding the old handle. Then, the caller frees the old handle once the replace returns, so such a fault would hit a use-after-free.

No caller does this today, since iommufd_device_do_replace() returns early when the new hwpt is the old one. However, it is only a caller convention, which neither API enforces.

Reject a same-domain replace in both APIs with a WARN_ON, and document it in their kernel-docs.

---
  drivers/iommu/iommu.c | 34 +++++++++++++++++++++++++++-------
  1 file changed, 27 insertions(+), 7 deletions(-)
drivers/iommu/iommu.c +20 -7
index 3fea48d3538ad..a9bc4ca5dbb4e 100644
@@ -3716,6 +3716,10 @@ EXPORT_SYMBOL_GPL(iommu_attach_device_pasid);
   * Caller should always provide a new handle to avoid race with the paths
   * that have lockless reference to handle if it intends to pass a valid handle.
   *
+ * The @domain must not be the domain currently attached to the @pasid. Such a
+ * same-domain replace would skip the driver callback flushing pending faults.
+ * Then, those faults could use the old handle after the caller frees it.
+ *
   * Return 0 on success, or an error.
   */
  int iommu_replace_device_pasid(struct iommu_domain *domain,
@@ -3785,15 +3789,17 @@ int iommu_replace_device_pasid(struct iommu_domain *domain,
  	}
 
  	curr_domain = pasid_array_entry_to_domain(curr);
-	ret = 0;
-
-	if (curr_domain != domain) {
-		ret = __iommu_set_group_pasid(domain, group,
-					      pasid, curr_domain);
-		if (ret)
-			entry = curr; /* restore the old handle */
+	/* Same as iommu_replace_group_handle(), reject a same-domain replace */
+	if (WARN_ON(curr_domain == domain)) {
+		entry = curr; /* restore the old handle */
+		ret = -EINVAL;
+		goto out_store;
  	}
 
+	ret = __iommu_set_group_pasid(domain, group, pasid, curr_domain);
+	if (ret)
+		entry = curr; /* restore the old handle */
+
  out_store:
  	/*
  	 * The xa_store() above reserved the memory, and the group->mutex
@@ -3985,6 +3991,10 @@ EXPORT_SYMBOL_NS_GPL(iommu_detach_group_handle, "IOMMUFD_INTERNAL");
   *
   * Caller should always provide a new handle to avoid race with the paths
   * that have lockless reference to handle.
+ *
+ * The @new_domain must not be the domain currently attached to the group. Such
+ * a same-domain replace would skip the driver callback flushing pending faults.
+ * Then, those faults could use the old handle after the caller frees it.
   */
  int iommu_replace_group_handle(struct iommu_group *group,
  			       struct iommu_domain *new_domain,
@@ -3997,6 +4007,16 @@ int iommu_replace_group_handle(struct iommu_group *group,
  		return -EINVAL;
 
  	mutex_lock(&group->mutex);
+	/*
+	 * A same-domain replace would skip the driver callback flushing pending
+	 * faults. Then, those faults could use the old handle after the caller

drivers/iommu/iommu.c int iommu_replace_group_handle(struct iommu_group *group, · line 4022

Quoted 10 lines
drivers/iommu/iommu.c +7 -0
+	 * frees it.
+	 */
+	if (WARN_ON(group->domain == new_domain)) {
+		ret = -EINVAL;
+		goto err_unlock;
+	}
+
  	entry = iommu_make_pasid_array_entry(new_domain, handle);
  	/*
  	 * iommu_attach_handle_get() runs without the group mutex, so unpublish

I am fine with this change.

Thanks,
baolu

#18 lore raw

PATCH 4/4 iommu: Unpublish the old attach handle before the PASID replace callback

In reply to Nicolin Chen

Similar to iommu_replace_group_handle(), iommu_replace_device_pasid() has the same inert reserve as the group path and the UAF window:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

  xa_cmpxchg()                    ,-- old handle stays published
    no-op, the slot is occupied   |
  __iommu_set_group_pasid()       |
    driver attach/detach ops      |
        iopf_queue_flush_dev()    |
                                  |   iommu_attach_handle_get()
                                  |     reads old_handle
  xa_store(new entry)             `-- window closes
  mutex_unlock()
  kfree(old_handle)  [caller]
                                  UAF: old_handle->domain->iopf_handler()

Store XA_ZERO_ENTRY outright, just as the group path now does.

Fixes 8a9e1e773f60 ("iommu: Introduce a replace API for device pasid")
Assisted-by LLM
---
 drivers/iommu/iommu.c | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)
drivers/iommu/iommu.c +12 -7
index 52e59f38cec18..2ff81e94a7b7b 100644
@@ -3751,8 +3751,13 @@ int iommu_replace_device_pasid(struct iommu_domain *domain,
 	}
 
 	entry = iommu_make_pasid_array_entry(domain, handle);
-	curr = xa_cmpxchg(&group->pasid_array, pasid, NULL,
-			  XA_ZERO_ENTRY, GFP_KERNEL);
+	/*
+	 * iommu_attach_handle_get() runs without the group mutex, so unpublish
+	 * the old handle before the driver callback: a fault must not resolve
+	 * to either domain while the switch is in progress. This reserves the
+	 * slot too, so the store below cannot fail.
+	 */
+	curr = xa_store(&group->pasid_array, pasid, XA_ZERO_ENTRY, GFP_KERNEL);
 	if (xa_is_err(curr)) {
 		ret = xa_err(curr);
 		goto out_unlock;
@@ -3776,7 +3781,7 @@ int iommu_replace_device_pasid(struct iommu_domain *domain,
 	if (curr == entry) {
 		WARN_ON(1);
 		ret = -EINVAL;
-		goto out_unlock;
+		goto out_store;
 	}
 
 	curr_domain = pasid_array_entry_to_domain(curr);
@@ -3786,16 +3791,16 @@ int iommu_replace_device_pasid(struct iommu_domain *domain,
 		ret = __iommu_set_group_pasid(domain, group,
 					      pasid, curr_domain);
 		if (ret)
-			goto out_unlock;
+			entry = curr; /* restore the old handle */
 	}
 
+out_store:
 	/*
-	 * The above xa_cmpxchg() reserved the memory, and the
-	 * group->mutex is held, this cannot fail.
+	 * The xa_store() above reserved the memory, and the group->mutex
+	 * is held, this cannot fail.
 	 */
 	WARN_ON(xa_is_err(xa_store(&group->pasid_array,
 				   pasid, entry, GFP_KERNEL)));
-
 out_unlock:
 	mutex_unlock(&group->mutex);
 	return ret;
Signature · 1 line
2.43.0
Reviewed-by on v1 4/4
#19 lore raw

RE: [PATCH v1 4/4] iommu: Unpublish the old attach handle before the PASID replace callback

Quoted 21 lines

From: Nicolin Chen <[email protected]>
Sent: Wednesday, September 30, 2026 5:52 AM

Similar to iommu_replace_group_handle(), iommu_replace_device_pasid() has the same inert reserve as the group path and the UAF window:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

  xa_cmpxchg()                    ,-- old handle stays published
    no-op, the slot is occupied   |
  __iommu_set_group_pasid()       |
    driver attach/detach ops      |
        iopf_queue_flush_dev()    |
                                  |   iommu_attach_handle_get()
                                  |     reads old_handle
  xa_store(new entry)             `-- window closes
  mutex_unlock()
  kfree(old_handle)  [caller]
                                  UAF: old_handle->domain->iopf_handler()
Quoted 6 lines

Store XA_ZERO_ENTRY outright, just as the group path now does.

Fixes 8a9e1e773f60 ("iommu: Introduce a replace API for device pasid")
Assisted-by LLM
Reviewed-by on v1 4/4
#20 lore raw

Re: [PATCH v1 4/4] iommu: Unpublish the old attach handle before the PASID replace callback

In reply to Nicolin Chen
Quoted 20 lines · Nicolin Chen

Similar to iommu_replace_group_handle(), iommu_replace_device_pasid() has the same inert reserve as the group path and the UAF window:

core, holding group->mutex fault path, no group->mutex
========================== ===========================

   xa_cmpxchg()                    ,-- old handle stays published
     no-op, the slot is occupied   |
   __iommu_set_group_pasid()       |
     driver attach/detach ops      |
         iopf_queue_flush_dev()    |
                                   |   iommu_attach_handle_get()
                                   |     reads old_handle
   xa_store(new entry)             `-- window closes
   mutex_unlock()
   kfree(old_handle)  [caller]
                                   UAF: old_handle->domain->iopf_handler()

Store XA_ZERO_ENTRY outright, just as the group path now does.

Quoted 7 lines
Fixes 8a9e1e773f60 ("iommu: Introduce a replace API for device pasid")
Assisted-by LLM
---
  drivers/iommu/iommu.c | 19 ++++++++++++-------
  1 file changed, 12 insertions(+), 7 deletions(-)
#21 lore raw
In reply to Nicolin Chen
Quoted 20 lines · Nicolin Chen

Jason pointed out an issue in the attach handle replacing path:
https://lore.kernel.org/linux-iommu/[email protected]/

When an attachment is detached or replaced, the IOMMU core can still leave the outgoing attach handle published while calling into the driver. A fault report looks up the handle without the group mutex, so it can find the old handle after the driver's fault flush and just before the caller frees it.

These four patches unpublish the outgoing handle before a driver callback for both group and PASID detach and replace operations. Both replace paths store XA_ZERO_ENTRY to reserve the slot and restore the old entry on error.

This blocks new lookups, but does not retire a fault that already obtained the old handle. The driver must still synchronize those readers before the handle is freed. A same-domain replacement skips the driver callback, so it still needs a separate lifetime mechanism or fence.

This is on GitHub:
https://github.com/nicolinc/iommufd/commits/fix_iommu_attach_handle-v1

Quoted 7 lines

Nicolin Chen (4):
iommu: Unpublish the attach handle before the group detach callback
iommu: Unpublish the attach handle before the PASID detach callback
iommu: Unpublish the old attach handle before the group replace
callback
iommu: Unpublish the old attach handle before the PASID replace
callback

Baolu/Kevin/Yi,

Would it be possible for you to take a look at these fixes?

Thanks
Nicolin

Reviewed-by on the v1 series
#22 lore raw
In reply to Nicolin Chen
Quoted 12 lines · Nicolin Chen

Jason pointed out an issue in the attach handle replacing path:
https://lore.kernel.org/linux-iommu/[email protected]/

When an attachment is detached or replaced, the IOMMU core can still leave the outgoing attach handle published while calling into the driver. A fault report looks up the handle without the group mutex, so it can find the old handle after the driver's fault flush and just before the caller frees it.

These four patches unpublish the outgoing handle before a driver callback for both group and PASID detach and replace operations. Both replace paths store XA_ZERO_ENTRY to reserve the slot and restore the old entry on error.

Quoted 7 lines

This blocks new lookups, but does not retire a fault that already obtained the old handle. The driver must still synchronize those readers before the handle is freed. A same-domain replacement skips the driver callback, so it still needs a separate lifetime mechanism or fence.

This is on GitHub:
https://github.com/nicolinc/iommufd/commits/fix_iommu_attach_handle-v1

this series LGTM.

Quoted 13 lines

Nicolin Chen (4):
iommu: Unpublish the attach handle before the group detach callback
iommu: Unpublish the attach handle before the PASID detach callback
iommu: Unpublish the old attach handle before the group replace
callback
iommu: Unpublish the old attach handle before the PASID replace
callback

  drivers/iommu/iommu.c | 54 ++++++++++++++++++++++++++++---------------
  1 file changed, 35 insertions(+), 19 deletions(-)

base-commit: cee9395acd8043be0644b25c34bfa86623f2b935