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 2/3] dmaengine: idxd: generation-number transaction status
Date: Mon, 19 Oct 2026 16:33:32 +0000	[thread overview]
Message-ID: <sashiko-outbox-177912@kernel.org> (raw)
In-Reply-To: <20261019150632.4026424-3-daniel1.byrne@intel.com>

Thank you for your contribution! Sashiko AI review found 7 potential issue(s) to consider:
- [Critical] dmaengine: idxd: pre-halt epoch in idxd_dma_tx_submit() frees active desc
- [Critical] dmaengine: idxd: cookie 0 in idxd_dma_tx_status() double-frees wq->descs[0]
- [High] dmaengine: idxd: unordered state and reset_epoch in idxd_dma_tx_status()
- [High] dmaengine: idxd: untracked polled descriptors leak on terminate or abandon
- [High] dmaengine: idxd: unretired desc on disabled state in idxd_dma_tx_status()
- [High] dmaengine: idxd: stale txd state on submit failure in idxd_dma_tx_submit()
- [Medium] dmaengine: idxd: late epoch read in idxd_dma_synchronize() wedges workqueue
--

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

dmaengine: idxd: generation-number transaction status

This commit implements per-descriptor transaction status polling in
idxd_dma_tx_status() by encoding the descriptor slot ID, a 14-bit generation
counter, and an interrupt flag in the DMA cookie, and uses a device
reset_epoch counter to retire polled descriptors caught by a device halt.

> diff --git a/drivers/dma/idxd/dma.c b/drivers/dma/idxd/dma.c
> index 0a7517cb51f29..dd5a9d9274ce6 100644
> --- a/drivers/dma/idxd/dma.c
> +++ b/drivers/dma/idxd/dma.c

[ ... ]

> @@ -154,11 +168,128 @@ static void idxd_dma_free_chan_resources(struct dma_chan *chan)
>  		idxd_wq_refcount(wq));
>  }
>  
> +/*
> + * A cookie has exactly one polling owner, the client that submitted the
> + * transaction. Observing a terminal completion record here retires and
> + * recycles the descriptor, since a polled descriptor has no interrupt
> + * and nobody else will. Two contexts polling the same live cookie
> + * would race that recycle, which every in-tree dmaengine polling
> + * client already avoids by construction.
> + */
>  static enum dma_status idxd_dma_tx_status(struct dma_chan *dma_chan,
>  					  dma_cookie_t cookie,
>  					  struct dma_tx_state *txstate)
>  {
> -	return DMA_OUT_OF_ORDER;
> +	struct idxd_wq *wq;
> +	struct idxd_desc *desc;
> +	u32 idx;
> +	u8 status;
> +
> +	if (txstate)
> +		memset(txstate, 0, sizeof(*txstate));
> +
> +	if (dma_submit_error(cookie))
> +		return DMA_ERROR;
> +
> +	wq = to_idxd_wq(dma_chan);
> +
> +	idx = cookie & DESC_ID_MASK;
> +	if (idx >= wq->num_descs)
> +		return DMA_ERROR;
> +
> +	desc = wq->descs[idx];
> +
> +	/*
> +	 * Descriptors with a completion interrupt armed (RCI) are
> +	 * completed and recycled by the IRQ handler. Reading their
> +	 * completion record here would race with the handler, and so
> +	 * would reading the recycled descriptor's flags, which is why
> +	 * the property travels in the cookie the caller owns. The handler
> +	 * invalidates the cookie on completion, so a matching cookie means
> +	 * the command is still in progress.
> +	 */
> +	if (cookie & IDXD_COOKIE_RCI)
> +		return READ_ONCE(desc->txd.cookie) == cookie ?
> +			DMA_IN_PROGRESS : DMA_COMPLETE;
> +
> +	if (desc->txd.cookie != cookie) {
> +		/*
> +		 * The cookie belongs to an old transaction that has
> +		 * already completed and been recycled.
> +		 */
> +		return DMA_COMPLETE;
> +	}

[Severity: Critical]
What happens here when a caller passes cookie == 0 (or any cookie whose
generation field is 0) to idxd_dma_tx_status()?

For example, ntb_transport_free_queue() in drivers/ntb/ntb_transport.c calls
dma_sync_wait(chan, qp->last_cookie) when tearing down a queue, where
qp->last_cookie is 0 if no transfer was submitted.

Because dma_submit_error(0) returns 0, cookie == 0 selects wq->descs[0].
When wq->descs[0] is free (either newly allocated or previously completed via
idxd_dma_complete_txd() -> dma_cookie_complete()), desc->txd.cookie is 0 and
desc->cpu is -1.

As a result, desc->txd.cookie != cookie evaluates 0 != 0 (false). If
wq->descs[0] completed a previous transaction, desc->completion->status is
still non-zero (or if a halt occurred, desc->epoch != reset_epoch), so
idxd_dma_tx_status() calls idxd_dma_complete_txd() -> idxd_free_desc() on
the already-free wq->descs[0]:

drivers/dma/idxd/submit.c:idxd_free_desc() {
	int cpu = desc->cpu;

	desc->cpu = -1;
	sbitmap_queue_clear(&wq->sbq, desc->id, cpu);
}

Passing cpu = -1 into sbitmap_queue_clear() -> sbitmap_update_cpu_hint() on a
non-round-robin sbitmap_queue executes *per_cpu_ptr(sb->alloc_hint, -1) = 0,
causing an out-of-bounds per-CPU write, or double-freeing slot 0 if another
thread just allocated it in idxd_alloc_desc() before reaching
idxd_dma_tx_submit().

Should idxd_dma_tx_status() reject cookies with cookie < DMA_MIN_COOKIE or a
zero generation field before indexing wq->descs[]?

> +
> +	status = desc->completion->status & DSA_COMP_STATUS_MASK;
> +
> +	if (status) {
> +		/* Capture completion record fields before desc is freed below. */
> +		u8 fault_info = desc->completion->fault_info;
> +		u64 fault_addr = desc->completion->fault_addr;
> +
> +		/*
> +		 * Check against the original status because ABORT is
> +		 * software defined as 0xff, which DSA_COMP_STATUS_MASK
> +		 * can mask out.
> +		 */
> +		if (unlikely(desc->completion->status == IDXD_COMP_DESC_ABORT))
> +			idxd_dma_complete_txd(desc, IDXD_COMPLETE_ABORT, true, NULL, NULL);
> +		else
> +			idxd_dma_complete_txd(desc, IDXD_COMPLETE_NORMAL, true, NULL, NULL);
> +
> +		dev_dbg(&wq->idxd->pdev->dev,
> +			"dsa completion: status=0x%02x fault_info=0x%02x fault_addr=0x%llx\n",
> +			status, fault_info, fault_addr);
> +
> +		if (status == DSA_COMP_SUCCESS)
> +			return DMA_COMPLETE;
> +
> +		return DMA_ERROR;
> +	}
> +
> +	/*
> +	 * The descriptor is genuinely outstanding. A polled descriptor
> +	 * carries no interrupt, so nothing but this function will ever
> +	 * retire it, and a work queue or device that is no longer enabled
> +	 * will not complete it either. Report the failure instead of
> +	 * leaving the caller polling forever.

[Severity: High]
Since a polled descriptor is not submitted with IDXD_OP_FLAG_RCI,
__idxd_submit_desc() does not add it to wq->ie.pending_llist:

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);
	}
}

If a client calls dmaengine_terminate_all() / dmaengine_terminate_sync() (for
example, dmatest_func() in drivers/dma/dmatest.c or nvmet_pci_epf_dma_transfer()
after a timeout) or abandons a cookie without polling it to a terminal result,
idxd_dma_terminate_all() -> __idxd_wq_flush_descs() only flushes
ie->pending_llist and ie->work_list.

Does this leak the descriptor slot in wq->sbq and its desc->txd.unmap buffer
while also bypassing the quarantine mechanism in idxd_dma_synchronize() for
in-flight polled descriptors?

> +	 *
> +	 * A descriptor submitted before a device halt is dead for good:
> +	 * the halt ended the engines and the reset that follows starts
> +	 * them empty, so its record will never be written even once the
> +	 * device is enabled again. Only in that case can the descriptor
> +	 * be retired here safely; otherwise the pool would shrink by one
> +	 * for every descriptor caught by a halt.
> +	 */
> +	if (desc->epoch != READ_ONCE(wq->idxd->reset_epoch)) {
> +		idxd_dma_complete_txd(desc, IDXD_COMPLETE_ABORT, true, NULL, NULL);
> +		return DMA_ERROR;
> +	}
> +	/*
> +	 * A device that is no longer enabled runs nothing it holds, so an
> +	 * outstanding descriptor on it has failed. A halt stores its state
> +	 * before it bumps the epoch, and this poll may have read the epoch
> +	 * before the halt and the state after it, so the epoch is read
> +	 * again: only the retire frees the slot of a descriptor the halt
> +	 * caught. A halted device whose epoch still matches is between the
> +	 * two stores, and the next poll retires the descriptor. The submit
> +	 * reads the state after the stamp, so it refuses a descriptor
> +	 * stamped with the bumped epoch.
> +	 */
> +	if (wq->idxd->state != IDXD_DEV_ENABLED) {
> +		if (desc->epoch != READ_ONCE(wq->idxd->reset_epoch)) {
> +			idxd_dma_complete_txd(desc, IDXD_COMPLETE_ABORT, true,
> +					      NULL, NULL);
> +			return DMA_ERROR;
> +		}
> +		if (wq->idxd->state == IDXD_DEV_HALTED)
> +			return DMA_IN_PROGRESS;
> +		return DMA_ERROR;
> +	}
> +	if (wq->state != IDXD_WQ_ENABLED)
> +		return DMA_ERROR;
> +
> +	return DMA_IN_PROGRESS;
>  }

[Severity: High]
There appear to be two issues in the non-enabled state handling at the end of
idxd_dma_tx_status():

First, wq->idxd->state is read twice without READ_ONCE() and without memory
barriers ordering it against READ_ONCE(wq->idxd->reset_epoch), while
idxd_halt() in drivers/dma/idxd/irq.c also writes idxd->state = IDXD_DEV_HALTED
without WRITE_ONCE() or a write barrier before bumping idxd->reset_epoch:

drivers/dma/idxd/irq.c:idxd_halt() {
	if (gensts.state == IDXD_DEVICE_STATE_HALT) {
		idxd->state = IDXD_DEV_HALTED;
		WRITE_ONCE(idxd->reset_epoch, idxd->reset_epoch + 1);
}

If a poller observes wq->idxd->state != IDXD_DEV_ENABLED (because it is
IDXD_DEV_HALTED) and desc->epoch == READ_ONCE(wq->idxd->reset_epoch) before
idxd_halt() increments reset_epoch, and idxd_halt() then bumps reset_epoch and
transitions idxd->state to IDXD_DEV_DISABLED or IDXD_DEV_ENABLED before the
second check:

	if (wq->idxd->state == IDXD_DEV_HALTED)
		return DMA_IN_PROGRESS;
	return DMA_ERROR;

won't the second read of wq->idxd->state see a state other than
IDXD_DEV_HALTED and return terminal DMA_ERROR without retiring desc?
Similarly, without barriers between idxd->state and idxd->reset_epoch in
idxd_halt() and the submit path, can a submitter observe the bumped
reset_epoch before observing idxd->state == IDXD_DEV_HALTED, stamping a dead
descriptor with the post-halt epoch so polling returns DMA_IN_PROGRESS
forever after software reset?

Second, when idxd_dma_tx_status() returns terminal DMA_ERROR because
wq->idxd->state == IDXD_DEV_DISABLED (for example after idxd_device_disable(),
which drains/disables the device without changing reset_epoch) or because
wq->state != IDXD_WQ_ENABLED (after idxd_wq_disable() or idxd_wq_reset()),
it does not call idxd_dma_complete_txd().

Since idxd_wq_disable(), idxd_wq_reset(), and idxd_device_disable() only set
the disabled state after hardware has stopped, doesn't returning DMA_ERROR
here without calling idxd_dma_complete_txd(desc, IDXD_COMPLETE_ABORT, true,
NULL, NULL) leak desc->txd.unmap and the descriptor slot in wq->sbq, while
leaving desc->txd.cookie intact?

> @@ -177,7 +308,17 @@ static dma_cookie_t idxd_dma_tx_submit(struct dma_async_tx_descriptor *tx)
>  	int rc;
>  	struct idxd_desc *desc = container_of(tx, struct idxd_desc, txd);
>  
> -	cookie = dma_cookie_assign(tx);
> +	cookie = (desc->gen << DESC_ID_BITS) | (desc->id & DESC_ID_MASK);
> +	if (desc->hw->flags & IDXD_OP_FLAG_RCI)
> +		cookie |= IDXD_COOKIE_RCI;
> +
> +	if (desc->gen == DESC_GEN_MAX)
> +		desc->gen = 1;
> +	else
> +		desc->gen++;
> +
> +	tx->cookie = cookie;
> +	desc->epoch = READ_ONCE(wq->idxd->reset_epoch);
>  
>  	rc = idxd_submit_desc(wq, desc);
>  	if (rc < 0) {

[Severity: Critical]
Can stamping desc->epoch in idxd_dma_tx_submit() before calling
idxd_submit_desc() cause an active post-reset descriptor to be prematurely
unmapped and freed by idxd_dma_tx_status()?

In idxd_dma_tx_submit(), desc->epoch is read before idxd_submit_desc() ->
__idxd_submit_desc() acquires wq->wq_active:

drivers/dma/idxd/submit.c:__idxd_submit_desc() {
	if (!percpu_ref_tryget_live(&wq->wq_active)) {
		wait_for_completion(&wq->wq_resurrect);
		if (!percpu_ref_tryget_live(&wq->wq_active))
			return -ENXIO;
	}

	smp_rmb();
	if (idxd->state != IDXD_DEV_ENABLED) {
		percpu_ref_put(&wq->wq_active);
		return -EIO;
	}
}

If a thread reads desc->epoch in idxd_dma_tx_submit() (say epoch E), and then
a software-reset halt occurs in idxd_halt() (bumping idxd->reset_epoch to
E + 1, killing and reviving wq->wq_active in idxd_wqs_halt_flush(idxd, true),
and re-enabling the device in idxd_device_reinit()), the submitter can wait on
wq->wq_resurrect (or be preempted before percpu_ref_tryget_live()), wake up
after idxd_device_reinit() sets idxd->state = IDXD_DEV_ENABLED, and submit
desc to the newly enabled hardware while desc->epoch is still E.

When the client then polls idxd_dma_tx_status() while hardware is still
executing the descriptor, desc->epoch != READ_ONCE(wq->idxd->reset_epoch)
(E != E + 1) matches and calls idxd_dma_complete_txd(desc,
IDXD_COMPLETE_ABORT, true, NULL, NULL), unmapping the DMA buffers and freeing
the descriptor slot while the DMA engine is still writing to them.

Should desc->epoch instead be stamped inside __idxd_submit_desc() after
acquiring wq->wq_active and before checking idxd->state != IDXD_DEV_ENABLED
(similar to how desc->terminate_gen is stamped while holding wq->wq_active)?

[Severity: High]
Also in idxd_dma_tx_submit(), when idxd_submit_desc(wq, desc) returns an error
(rc < 0), idxd_free_desc(wq, desc) is called directly:

	tx->cookie = cookie;
	desc->epoch = READ_ONCE(wq->idxd->reset_epoch);

	rc = idxd_submit_desc(wq, desc);
	if (rc < 0) {
		idxd_free_desc(wq, desc);
		return rc;
	}

In __idxd_submit_desc(), llist_abort_desc() is only called if submission fails
after pending_llist insertion and only when ie is non-NULL (which is only true
for IDXD_OP_FLAG_RCI descriptors). If __idxd_submit_desc() fails early (e.g.
idxd->state != IDXD_DEV_ENABLED or wq_active is dead) or if idxd_enqcmds()
fails for a polled descriptor where ie is NULL, desc is freed back to wq->sbq
with tx->cookie, tx->callback, tx->callback_result, and tx->unmap still set.

When that slot is later reallocated by a polling client that does not set a
callback and is completed by idxd_dma_tx_status() -> idxd_dma_complete_txd(),
won't dmaengine_desc_get_callback_invoke() invoke the stale callback from the
earlier failed submission?

> @@ -219,6 +360,7 @@ static void idxd_dma_synchronize(struct dma_chan *c)
>  {
>  	struct idxd_wq *wq = to_idxd_wq(c);
>  	LIST_HEAD(quarantined);
> +	u32 epoch;
>  
>  	/*
>  	 * dmaengine_synchronize() promises the client that its memory may
> @@ -240,7 +382,13 @@ static void idxd_dma_synchronize(struct dma_chan *c)
>  	 * Only a device reset ends the exposure of the client's memory.
>  	 */
>  	idxd_wq_quarantine_take(wq, &quarantined);
> -	if (!idxd_wq_drain(wq)) {
> +	/*
> +	 * A halt during the drain kills every descriptor submitted before
> +	 * it, which is all of them, and a reset that followed may have
> +	 * ended the drain itself. Such a drain is as good as complete.
> +	 */
> +	epoch = READ_ONCE(wq->idxd->reset_epoch);
> +	if (!idxd_wq_drain(wq) || READ_ONCE(wq->idxd->reset_epoch) != epoch) {
>  		idxd_wq_quarantine_free(wq, &quarantined);
>  		WRITE_ONCE(wq->stuck, false);
>  		return;

[Severity: Medium]
What happens if idxd_halt() runs after idxd_wq_quarantine_take(wq,
&quarantined) but before epoch = READ_ONCE(wq->idxd->reset_epoch)?

In that window:
1. idxd_wq_quarantine_take() splices the entries out of wq->quarantine.
2. idxd_halt() increments idxd->reset_epoch to E + 1 and calls
   idxd_wqs_halt_flush() -> idxd_wq_halt_release(wq), which sees an empty
   wq->quarantine and sets wq->stuck = false.
3. idxd_dma_synchronize() reads epoch = E + 1 and calls idxd_wq_drain(wq).
4. If idxd_device_reinit() resets the device while idxd_wq_drain() is running
   so that idxd_cmd_exec() fails with IDXD_CMDSTS_HW_ERR and
   idxd_device_is_halted() is already false when idxd_cmd_drain_result() checks
   it, idxd_wq_drain() returns -ETIMEDOUT while reset_epoch is still E + 1.

Wouldn't idxd_dma_synchronize() then put the quarantined descriptors back and
set wq->stuck = true after idxd_wq_halt_release() has already finished,
wedging the newly reset workqueue? Would reading reset_epoch before calling
idxd_wq_quarantine_take() avoid this window?

> diff --git a/drivers/dma/idxd/idxd.h b/drivers/dma/idxd/idxd.h
> index b0cfd71011742..7fe5ec6c3bf36 100644
> --- a/drivers/dma/idxd/idxd.h
> +++ b/drivers/dma/idxd/idxd.h

[ ... ]

> diff --git a/drivers/dma/idxd/irq.c b/drivers/dma/idxd/irq.c
> index 8217698056bb4..5e34790359ee9 100644
> --- a/drivers/dma/idxd/irq.c
> +++ b/drivers/dma/idxd/irq.c
> @@ -508,6 +508,14 @@ static irqreturn_t idxd_halt(struct idxd_device *idxd)
>  	gensts.bits = ioread32(idxd->reg_base + IDXD_GENSTATS_OFFSET);
>  	if (gensts.state == IDXD_DEVICE_STATE_HALT) {
>  		idxd->state = IDXD_DEV_HALTED;
> +		/*
> +		 * Every descriptor submitted before this point is dead: the
> +		 * device will never write its completion record. Pollers
> +		 * compare their descriptor's epoch against this to retire
> +		 * such descriptors instead of waiting for a record that never
> +		 * comes.
> +		 */
> +		WRITE_ONCE(idxd->reset_epoch, idxd->reset_epoch + 1);
>  		if (gensts.reset_type == IDXD_DEVICE_RESET_SOFTWARE) {
>  			/* The reinit below re-enables the queues, so revive them. */
>  			idxd_wqs_halt_flush(idxd, true);

[Severity: High]
As noted above in idxd_dma_tx_status(), should idxd->state = IDXD_DEV_HALTED
use WRITE_ONCE() followed by smp_wmb() before updating idxd->reset_epoch so
that readers in idxd_dma_tx_status() and __idxd_submit_desc() cannot observe
the incremented reset_epoch before observing IDXD_DEV_HALTED?

Also, if idxd_halt() is entered again while a reset is in progress or via a
subsequent halt, could bumping idxd->reset_epoch while idxd->state is already
IDXD_DEV_HALTED race with a poller in idxd_dma_tx_status() that relies on
reset_epoch only changing after idxd->state transitions to IDXD_DEV_HALTED?

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

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

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19 15:06 [PATCH 0/3] [PATCH 0/3] dmaengine: polled completion status for out-of-order providers Daniel Byrne
2026-10-19 15:06 ` [PATCH 1/3] dmaengine: add dmaengine_async_is_tx_complete() for channel status queries Daniel Byrne
2026-10-19 18:36   ` Frank Li
2026-10-19 15:06 ` [PATCH 2/3] dmaengine: idxd: generation-number transaction status Daniel Byrne
2026-10-19 16:33   ` sashiko-bot [this message]
2026-10-19 18:53   ` Dave Jiang
2026-10-19 15:06 ` [PATCH 3/3] dmaengine: drop DMA_COMPLETION_NO_ORDER and DMA_OUT_OF_ORDER Daniel Byrne
2026-10-19 19:02   ` 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-177912@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