DMA Engine development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Daniel Byrne" <daniel1.byrne@intel.com>
Cc: dmaengine@vger.kernel.org, vkoul@kernel.org, Frank.Li@kernel.org
Subject: Re: [PATCH 16/17] dmaengine: idxd: keep client callbacks on a halt flush and cancel resubmissions after a terminate
Date: Mon, 19 Oct 2026 19:36:20 +0000	[thread overview]
Message-ID: <sashiko-outbox-178145@kernel.org> (raw)
In-Reply-To: <20261019150630.4026341-17-daniel1.byrne@intel.com>

Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [Critical] dmaengine: idxd: deferred halt flush races with idxd_dmaengine_drv_remove()
- [Critical] dmaengine: idxd: halt flush fails to abort DSA_COMP_INT_HANDLE_INVAL descs
- [Critical] dmaengine: idxd: unsynchronized abort in idxd_int_handle_resubmit_work()
- [High] dmaengine: idxd: deferred halt flush runs callbacks after terminate_all()
- [High] dmaengine: idxd: idxd_dma_terminate_all() bumps terminate_gen before kill
--

commit 6d8942e7925503eef278b95468cdbb6ea9bd20c4
Author: Daniel Byrne <daniel1.byrne@intel.com>

dmaengine: idxd: keep client callbacks on a halt flush and cancel resubmissions after a terminate

This commit updates idxd_flush_pending_descs() and llist_abort_desc() to
preserve client callbacks during device halt flushes and internal
interrupt-handle resubmissions while stripping them on teardown and
client-initiated terminations. It also introduces a per-queue
terminate_gen counter to cancel pending interrupt-handle resubmissions
across dmaengine_terminate_all().

> diff --git a/drivers/dma/idxd/device.c b/drivers/dma/idxd/device.c
> index 158c141650e3d..5ccdc5ff8d53d 100644
> --- a/drivers/dma/idxd/device.c
> +++ b/drivers/dma/idxd/device.c
> @@ -1527,7 +1527,7 @@ int idxd_device_load_config(struct idxd_device *idxd)
>  }
>  
>  static void idxd_flush_pending_descs(struct idxd_irq_entry *ie,
> -				     bool quarantine)
> +				     bool keep_callbacks, bool quarantine)
>  {
>  	struct idxd_wq *wq = ie_to_wq(ie);
>  	struct idxd_desc *desc, *itr;
> @@ -1543,6 +1543,28 @@ static void idxd_flush_pending_descs(struct idxd_irq_entry *ie,
>  			list_add_tail(&desc->list, &ie->work_list);
>  	}
>  
> +	/*
> +	 * A halt flush that delivers the callbacks of a dmaengine queue
> +	 * leaves its descriptors to the interrupt thread, which runs them
> +	 * without wq_lock, as it runs every completion. A callback may
> +	 * terminate the channel, which takes that lock, and the halt flush
> +	 * holds it. The halted device writes no record any more, so one it
> +	 * never finished is marked aborted for the thread to complete.
> +	 */
> +	if (keep_callbacks && idxd_wq_has_txd(wq)) {
> +		bool wake = false;
> +
> +		list_for_each_entry(desc, &ie->work_list, list) {
> +			if (!desc->completion->status)
> +				desc->completion->status = IDXD_COMP_DESC_ABORT;
> +			wake = true;
> +		}
> +		spin_unlock(&ie->list_lock);
> +		if (wake)
> +			irq_wake_thread(ie->vector, ie);
> +		return;
> +	}

[Severity: High]
Can this deferred completion path still invoke client callbacks after
dmaengine_terminate_all() or dmaengine_terminate_sync() has returned?

When idxd_wq_thread() runs irq_process_work_list(), it moves every
descriptor with a non-zero status from ie->work_list onto its stack-local
flist before dropping ie->list_lock:

drivers/dma/idxd/irq.c:irq_process_work_list() {
	list_for_each_entry_safe(desc, n, &irq_entry->work_list, list) {
		if (desc->completion->status) {
			list_move_tail(&desc->list, &flist);
		}
	}

	spin_unlock(&irq_entry->list_lock);

	list_for_each_entry_safe(desc, n, &flist, list) {
		list_del(&desc->list);

		if (unlikely(desc->completion->status == IDXD_COMP_DESC_ABORT)) {
			idxd_desc_complete(desc, IDXD_COMPLETE_ABORT, true);
			continue;
		}

		idxd_desc_complete(desc, IDXD_COMPLETE_NORMAL, true);
	}
}

If the first descriptor's callback calls dmaengine_terminate_async() ->
idxd_dma_terminate_all() (or another CPU calls dmaengine_terminate_all()
concurrently), idxd_flush_pending_descs() sees ie->pending_llist and
ie->work_list already empty because the remaining descriptors are on
irq_process_work_list()'s local flist. Since neither irq_process_work_list()
nor idxd_dma_complete_txd() checks wq->terminate_gen, irq_process_work_list()
continues iterating flist and invokes the remaining callbacks after
dmaengine_terminate_all() has returned.

In addition, on a system-reset halt or during idxd_device_reinit() ->
idxd_device_reset(), idxd_device_clear_state() ->
idxd_device_wqs_clear_state() -> idxd_wq_disable_cleanup() sets
wq->state = IDXD_WQ_DISABLED right after idxd_wqs_halt_flush() returns. If
the client then calls dmaengine_terminate_all(), __idxd_wq_flush_descs()
returns early without flushing ie->work_list or stripping callbacks:

drivers/dma/idxd/device.c:__idxd_wq_flush_descs() {
	if (wq->state != IDXD_WQ_ENABLED || wq->type != IDXD_WQT_KERNEL)
		return;
}

And idxd_dma_synchronize() does not synchronize with ie->vector (and
idxd_wq_drain() also returns immediately when wq->state != IDXD_WQ_ENABLED),
allowing dmaengine_terminate_sync() to return while idxd_wq_thread() is
still running callbacks.

[Severity: Critical]
Also in this loop, what happens if a descriptor on ie->work_list (or moved
from ie->pending_llist) finished just before the halt with
desc->completion->status == DSA_COMP_INT_HANDLE_INVAL?

Because desc->completion->status is already non-zero, the loop leaves it as
DSA_COMP_INT_HANDLE_INVAL. When idxd_wq_thread() -> irq_process_work_list()
processes the descriptor, it sees status != IDXD_COMP_DESC_ABORT and calls
idxd_desc_complete(desc, IDXD_COMPLETE_NORMAL, true) ->
idxd_dma_complete_txd(), which queues idxd_int_handle_resubmit_work() on
idxd->wq instead of aborting the descriptor:

drivers/dma/idxd/dma.c:idxd_dma_complete_txd() {
	} else if (desc->completion->status) {
		if (idxd->request_int_handles && comp_type != IDXD_COMPLETE_ABORT &&
		    desc->completion->status == DSA_COMP_INT_HANDLE_INVAL &&
		    idxd_queue_int_handle_resubmit(desc))
			return;
}

On an FLR halt, idxd_device_flr runs on idxd->wq, unbinds the queue driver
via idxd_dmaengine_drv_remove(), and frees wq->descs, wq->compls,
wq->idxd_chan, and wq->sbq while idxd_int_handle_resubmit_work() is still
queued. Should a descriptor with status == DSA_COMP_INT_HANDLE_INVAL also
be overwritten with IDXD_COMP_DESC_ABORT here, matching the non-callback
path below?

[ ... ]

> diff --git a/drivers/dma/idxd/dma.c b/drivers/dma/idxd/dma.c
> index 1d08856f5c4ec..0a7517cb51f29 100644
> --- a/drivers/dma/idxd/dma.c
> +++ b/drivers/dma/idxd/dma.c
> @@ -199,8 +199,18 @@ static int idxd_dma_terminate_all(struct dma_chan *c)
>  {
>  	struct idxd_wq *wq = to_idxd_wq(c);
>  
> +	guard(mutex)(&wq->wq_lock);
> +	/*
> +	 * A descriptor that completed with a revoked interrupt handle is
> +	 * resubmitted by a work item. One submitted before this terminate
> +	 * may reach that work after the flush, from the interrupt thread or
> +	 * from a work item queued earlier, and is cancelled there rather
> +	 * than run with its callback once the client may have freed its
> +	 * memory.
> +	 */
> +	WRITE_ONCE(wq->terminate_gen, wq->terminate_gen + 1);
>  	/* The device keeps running what the lists held. See synchronize. */
> -	idxd_wq_flush_descs(wq, true);
> +	__idxd_wq_flush_descs(wq, false, true);
>  
>  	return 0;
>  }

[Severity: High]
Since WRITE_ONCE(wq->terminate_gen, wq->terminate_gen + 1) runs before
__idxd_wq_flush_descs() calls idxd_wq_kill(wq), can an in-flight
idxd_submit_desc() stamp the new post-terminate generation onto a
pre-terminate descriptor?

If a concurrent client submission in __idxd_submit_desc() has already
acquired wq->wq_active via percpu_ref_tryget_live(&wq->wq_active) before
idxd_wq_kill(wq) calls percpu_ref_kill(), it can reach:

drivers/dma/idxd/submit.c:__idxd_submit_desc() {
	if (desc_flags & IDXD_OP_FLAG_RCI) {
		ie = &wq->ie;
		desc->hw->int_handle = ie->int_handle;
		if (submitter_cleans)
			desc->terminate_gen = READ_ONCE(wq->terminate_gen);
		llist_add(&desc->llnode, &ie->pending_llist);
	}
}

while idxd_wq_kill(wq) is waiting in wait_for_completion(&wq->wq_dead).
That pre-terminate submission then stamps desc->terminate_gen with the
already-incremented wq->terminate_gen. If the descriptor completes with
DSA_COMP_INT_HANDLE_INVAL and idxd_wq_thread() pops it from
ie->pending_llist and queues idxd_int_handle_resubmit_work() before
idxd_flush_pending_descs() takes ie->list_lock, the resubmission will see
desc->terminate_gen == READ_ONCE(wq->terminate_gen) and will not be
cancelled.

[ ... ]

> diff --git a/drivers/dma/idxd/irq.c b/drivers/dma/idxd/irq.c
> index 99b2df7fc2de0..4517a09fcf1b9 100644
> --- a/drivers/dma/idxd/irq.c
> +++ b/drivers/dma/idxd/irq.c
> @@ -452,7 +452,7 @@ static void idxd_wqs_halt_flush(struct idxd_device *idxd, bool resurrect)
>  			continue;
>  		}
>  		killed = idxd_wq_kill(wq);
> -		__idxd_wq_flush_descs(wq, false);
> +		__idxd_wq_flush_descs(wq, idxd_wq_refcount(wq) > 0, false);
>  		idxd_wq_halt_release(wq);
>  
>  		if (resurrect) {

[Severity: Critical]
When resurrect is false (on an FLR halt), does leaving descriptors on
ie->work_list for idxd_wq_thread() race with idxd_dmaengine_drv_remove()?

After idxd_wqs_halt_flush(idxd, false) wakes idxd_wq_thread() and releases
wq->wq_lock, idxd_halt() queues idxd_device_flr, which calls
pci_reset_function() -> idxd_reset_done() -> idxd_unbind() ->
idxd_device_drv_remove() -> idxd_dmaengine_drv_remove() (this path can also
run if the driver is unbound via sysfs after a halt):

drivers/dma/idxd/dma.c:idxd_dmaengine_drv_remove() {
	mutex_lock(&wq->wq_lock);
	__idxd_wq_quiesce(wq);
	idxd_unregister_dma_channel(wq);
	idxd_drv_disable_wq(wq);
	mutex_unlock(&wq->wq_lock);
}

idxd_unregister_dma_channel(wq) kfrees wq->idxd_chan before
idxd_drv_disable_wq(wq) -> idxd_wq_free_irq(wq) calls free_irq(ie->vector,
ie) and idxd_flush_pending_descs(ie, false, false).

Can this cause two regressions during teardown:

1. When idxd_wq_thread() or idxd_wq_free_irq() -> idxd_flush_pending_descs()
completes those descriptors via idxd_dma_complete_txd(),
dma_cookie_complete(&desc->txd) writes tx->chan->completed_cookie into the
already-freed wq->idxd_chan?

2. While idxd_dmaengine_drv_remove() holds wq->wq_lock and blocks in
idxd_wq_free_irq() -> free_irq(ie->vector, ie) waiting for idxd_wq_thread()
to finish, if a callback running in idxd_wq_thread() calls
dmaengine_terminate_all() -> idxd_dma_terminate_all(), will it deadlock
trying to acquire wq->wq_lock?

> @@ -659,22 +659,27 @@ static void idxd_int_handle_resubmit_work(struct work_struct *work)
>  	int rc;
>  
>  	desc->completion->status = 0;
> -	rc = idxd_submit_desc(wq, desc);
> +	rc = idxd_resubmit_desc(wq, desc);
>  	if (rc < 0) {
>  		dev_dbg(&wq->idxd->pdev->dev, "Failed to resubmit desc %d to wq %d.\n",
>  			desc->id, wq->id);
>  		/*
> -		 * If the error is not -EAGAIN, it means the submission failed due to wq
> -		 * has been killed instead of ENQCMDS failure. Here the driver needs to
> -		 * notify the submitter of the failure by reporting abort status.
> -		 *
> -		 * -EAGAIN comes from ENQCMDS failure. idxd_submit_desc() will handle the
> -		 * abort.
> +		 * The submission failed because the device is not enabled,
> +		 * because the wq has been killed or because ENQCMDS failed,
> +		 * and in each case the descriptor is on no list. Here the
> +		 * driver needs to notify the submitter of the failure by
> +		 * reporting abort status. The submit has dropped its
> +		 * wq_active reference by now, so a callback that submits or
> +		 * terminates does not wait for it. A descriptor submitted
> +		 * before a terminate_all, which cancels its resubmission,
> +		 * gets no callback, since its client terminated it.
>  		 */
> -		if (rc != -EAGAIN) {
> -			desc->completion->status = IDXD_COMP_DESC_ABORT;
> -			idxd_desc_complete(desc, IDXD_COMPLETE_ABORT, false);
> +		if (desc->terminate_gen != READ_ONCE(wq->terminate_gen)) {
> +			desc->txd.callback = NULL;
> +			desc->txd.callback_result = NULL;
>  		}
> +		desc->completion->status = IDXD_COMP_DESC_ABORT;
> +		idxd_desc_complete(desc, IDXD_COMPLETE_ABORT, false);
>  		idxd_free_desc(wq, desc);
>  	}
>  	kfree(irw);

[Severity: Critical]
Because idxd_int_handle_resubmit_work() runs on idxd->wq without holding
wq->wq_active or wq->wq_lock once idxd_resubmit_desc() returns rc < 0, and
neither idxd_dma_synchronize() nor idxd_dmaengine_drv_remove() ->
idxd_drv_disable_wq() flushes idxd->wq, can this failure path race with
channel termination and workqueue teardown?

1. If idxd_resubmit_desc() fails with -EAGAIN, -EIO, or -ENXIO just before
the client calls dmaengine_terminate_sync(), idxd_int_handle_resubmit_work()
can read wq->terminate_gen before idxd_dma_terminate_all() increments it,
and then invoke desc->txd.callback in idxd_desc_complete() after
dmaengine_terminate_sync() has already returned to the client.

2. Even when desc->terminate_gen != READ_ONCE(wq->terminate_gen) is true,
idxd_desc_complete(desc, IDXD_COMPLETE_ABORT, false) ->
idxd_dma_complete_txd() runs asynchronously after dmaengine_terminate_sync()
has returned:

drivers/dma/idxd/dma.c:idxd_dma_complete_txd() {
	tx = &desc->txd;
	if (complete && tx->cookie) {
		dma_cookie_complete(tx);
		dma_descriptor_unmap(tx);
		dmaengine_desc_get_callback_invoke(tx, &res);
		tx->callback = NULL;
		tx->callback_result = NULL;
	}
}

Doesn't this call dma_descriptor_unmap(tx) after dmaengine_synchronize() has
already returned, and write tx->chan->completed_cookie = tx->cookie in
dma_cookie_complete(tx) where it can overwrite chan->completed_cookie
backwards if the client has already submitted and completed newer
descriptors after terminate_sync()?

3. If the workqueue is unbound or disabled via idxd_dmaengine_drv_remove()
-> idxd_drv_disable_wq() while idxd_int_handle_resubmit_work() is still
pending on idxd->wq, idxd_unregister_dma_channel() frees wq->idxd_chan and
idxd_wq_free_resources() frees wq->descs, wq->compls, and wq->sbq before
idxd_int_handle_resubmit_work() accesses desc and calls idxd_free_desc(wq,
desc).

[ ... ]

> diff --git a/drivers/dma/idxd/submit.c b/drivers/dma/idxd/submit.c
> index cf8536e94c15e..469a0cadb984d 100644
> --- a/drivers/dma/idxd/submit.c
> +++ b/drivers/dma/idxd/submit.c

[ ... ]

> @@ -250,6 +274,12 @@ int idxd_submit_desc(struct idxd_wq *wq, struct idxd_desc *desc)
>  	if (desc_flags & IDXD_OP_FLAG_RCI) {
>  		ie = &wq->ie;
>  		desc->hw->int_handle = ie->int_handle;
> +		/*
> +		 * A resubmission keeps the stamp of the client's submit, so
> +		 * the resubmit work sees every terminate since then.
> +		 */
> +		if (submitter_cleans)
> +			desc->terminate_gen = READ_ONCE(wq->terminate_gen);
>  		llist_add(&desc->llnode, &ie->pending_llist);
>  	}

[Severity: High]
As noted above in idxd_dma_terminate_all(), because this read of
wq->terminate_gen happens under wq->wq_active while idxd_dma_terminate_all()
updates wq->terminate_gen before calling idxd_wq_kill(wq), can a submission
that entered __idxd_submit_desc() prior to dmaengine_terminate_all() read
the post-terminate generation here and escape cancellation if it later hits
an invalid interrupt handle?

>  
> @@ -275,7 +305,7 @@ int idxd_submit_desc(struct idxd_wq *wq, struct idxd_desc *desc)

[ ... ]

>  			if (ie)
> -				llist_abort_desc(wq, ie, desc);
> +				llist_abort_desc(wq, ie, desc, submitter_cleans);
>  			percpu_ref_put(&wq->wq_active);
>  			return rc;
>  		}
> @@ -284,4 +314,15 @@ int idxd_submit_desc(struct idxd_wq *wq, struct idxd_desc *desc)
>  	percpu_ref_put(&wq->wq_active);
>  	return 0;
>  }
> +/* A client submission: on failure the return value is the notification. */
> +int idxd_submit_desc(struct idxd_wq *wq, struct idxd_desc *desc)
> +{
> +	return __idxd_submit_desc(wq, desc, true);
> +}
>  EXPORT_SYMBOL_NS_GPL(idxd_submit_desc, "IDXD");
> +
> +/* The driver's own resubmission: a failure must still call the client back. */
> +int idxd_resubmit_desc(struct idxd_wq *wq, struct idxd_desc *desc)
> +{
> +	return __idxd_submit_desc(wq, desc, false);
> +}

[ ... ]

>

endasi

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261019150630.4026341-1-daniel1.byrne@intel.com?part=16

  reply	other threads:[~2026-10-19 19:36 UTC|newest]

Thread overview: 29+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19 15:06 [PATCH 00/17] [PATCH 00/17] dmaengine: idxd: fixes found by an in-kernel polling client Daniel Byrne
2026-10-19 15:06 ` [PATCH 01/17] dmaengine: idxd: don't queue EVL fault work for kernel WQs Daniel Byrne
2026-10-19 15:06 ` [PATCH 02/17] dmaengine: idxd: detach the user fault workqueue under the event log lock Daniel Byrne
2026-10-19 15:06 ` [PATCH 03/17] dmaengine: idxd: refuse a memcpy longer than the WQ max transfer size Daniel Byrne
2026-10-19 21:19   ` Frank Li
2026-10-19 15:06 ` [PATCH 04/17] dmaengine: idxd: list a new channel only once it is complete Daniel Byrne
2026-10-19 15:06 ` [PATCH 05/17] dmaengine: idxd: unbind the device driver before its work queues go away Daniel Byrne
2026-10-19 15:06 ` [PATCH 06/17] dmaengine: idxd: keep the interrupt handle across terminate_all Daniel Byrne
2026-10-19 15:06 ` [PATCH 07/17] dmaengine: idxd: bound device command waits and fail fast after a timeout Daniel Byrne
2026-10-19 17:05   ` sashiko-bot
2026-10-19 15:06 ` [PATCH 08/17] dmaengine: idxd: compare the device disable status code whole Daniel Byrne
2026-10-19 15:06 ` [PATCH 09/17] dmaengine: idxd: quarantine terminate_all's descriptor slots until the device has drained Daniel Byrne
2026-10-19 17:26   ` sashiko-bot
2026-10-19 15:06 ` [PATCH 10/17] dmaengine: idxd: return -ETIMEDOUT from drains and resets and leak what the device may still write Daniel Byrne
2026-10-19 17:50   ` sashiko-bot
2026-10-19 15:06 ` [PATCH 11/17] dmaengine: idxd: disable the device before its work queues are unregistered Daniel Byrne
2026-10-19 18:09   ` sashiko-bot
2026-10-19 15:06 ` [PATCH 12/17] dmaengine: idxd: hold device commands off while the reset re-creates its interrupts Daniel Byrne
2026-10-19 18:19   ` sashiko-bot
2026-10-19 15:06 ` [PATCH 13/17] dmaengine: idxd: fix the double free of a descriptor whose submission failed Daniel Byrne
2026-10-19 15:06 ` [PATCH 14/17] dmaengine: idxd: complete an iaa_crypto descriptor through its own driver on abort and flush Daniel Byrne
2026-10-19 18:56   ` sashiko-bot
2026-10-19 15:06 ` [PATCH 15/17] dmaengine: idxd: flush the work queues on every halt type Daniel Byrne
2026-10-19 19:17   ` sashiko-bot
2026-10-19 15:06 ` [PATCH 16/17] dmaengine: idxd: keep client callbacks on a halt flush and cancel resubmissions after a terminate Daniel Byrne
2026-10-19 19:36   ` sashiko-bot [this message]
2026-10-19 15:06 ` [PATCH 17/17] dmaengine: idxd: run the interrupt-handle revoke under the queue lock Daniel Byrne
2026-10-19 19:53   ` sashiko-bot
2026-10-19 19:09 ` [PATCH 00/17] [PATCH 00/17] dmaengine: idxd: fixes found by an in-kernel polling client Frank Li

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-178145@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=Frank.Li@kernel.org \
    --cc=daniel1.byrne@intel.com \
    --cc=dmaengine@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=vkoul@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox