Thread (15 messages) flat view 15 messages, 4 authors, 11h ago

Re: [PATCH v1 2/5] dmaengine: arm-dma350: Add slave transfer support

From: sashiko-bot@kernel.org
Date: 2026-09-07 03:49:18
Also in: linux-devicetree

Thank you for your contribution! Sashiko AI review found 9 potential issue(s) to consider:

New issues:
- [High] DMA descriptors in memory are written using native endianness, causing corruption on big-endian CPUs.
- [High] Cyclic DMA residue calculation relies on fragile software tracking, lacks sub-period precision, and permanently desynchronizes if interrupts coalesce.
- [High] d350_of_xlate() incorrectly fails to allocate a channel if the first free channel lacks trigger support.
- [High] Valid DMA address of 0 is erroneously treated as a sentinel, disabling descriptor linking and breaking cyclic/SG transfers.
- [High] Calling dma_free_coherent() from the vchan_complete tasklet causes sleeping in atomic context.
- [High] Race condition between reading CH_LINKADDR and CH_XSIZE leads to invalid, non-monotonic DMA residue calculations.
- [Medium] Trigger block size ignores configured maxburst for DEV_TO_MEM transfers, degrading performance.
- [Medium] Torn read of the 64-bit hardware CH_LINKADDR register due to missing retry loop.

Pre-existing issues:
- [High] Spurious DMA_ERROR returned for descriptors that have been submitted but not yet issued.
--

commit e35af7caf5e3e88f06c1135c2c306f3cb90773b3
Author: Jelly Jia [off-list ref]

dmaengine: arm-dma350: Add slave transfer support

Add DMA slave support to the Arm DMA-350 driver, which so far only
supports memory-to-memory transfers.
quoted hunk ↗ jump to hunk
diff --git a/drivers/dma/arm-dma350.c b/drivers/dma/arm-dma350.c
index 4e17130de6c87..85e8c5e428044 100644
--- a/drivers/dma/arm-dma350.c
+++ b/drivers/dma/arm-dma350.c
[ ... ]
+static void d350_free_cmds(struct device *dev, struct d350_desc *desc)
+{
+	if (desc->cmds)
+		dma_free_coherent(dev, desc->cmds_size, desc->cmds,
+				  desc->cmds_dma);
+	kfree(desc->cmd_len);
+}
+
 static void d350_desc_free(struct virt_dma_desc *vd)
 {
-	kfree(to_d350_desc(vd));
+	struct d350_desc *desc = to_d350_desc(vd);
+
+	d350_free_cmds(vd->tx.chan->device->dev, desc);
+	kfree(desc);
+}
[Severity: High]
Can this sleep in atomic context?

d350_desc_free() is registered as the virt-dma desc_free callback, which
executes in a tasklet (softirq) context. However, dma_free_coherent()
might sleep (e.g., when flushing IOMMU page tables), which could trigger
scheduling-while-atomic BUGs.

[ ... ]
+static struct dma_chan *d350_of_xlate(struct of_phandle_args *dma_spec,
+				      struct of_dma *ofdma)
+{
+	struct d350 *dmac = ofdma->of_dma_data;
+	struct dma_chan *chan;
+	struct d350_chan *dch;
+	u32 req;
[ ... ]
+	chan = dma_get_any_slave_channel(&dmac->dma);
+	if (!chan) {
+		dev_err(dmac->dma.dev, "can't get a dma channel\n");
+		return NULL;
+	}
+
+	dch = to_d350_chan(chan);
+	if (!dch->has_trig) {
+		dev_err(dmac->dma.dev, "channel %d has no trigger support\n",
+			chan->chan_id);
+		dma_release_channel(chan);
+		return NULL;
+	}
[Severity: High]
Does this incorrectly reject valid requests on heterogeneous DMA configurations?

If dma_get_any_slave_channel() happens to return a channel lacking trigger
support, the request is dropped entirely instead of continuing to search
the remaining free channels for a capable one.

[ ... ]
+static void d350_fill_slave_cmd(struct d350_chan *dch, struct d350_desc *desc,
+				u32 *cmd, dma_addr_t mem, dma_addr_t dev_dma_addr,
+				size_t len, dma_addr_t link_addr,
+				enum dma_transfer_direction direction,
+				enum dma_slave_buswidth width, u32 maxburst,
+				enum ch_ctrl_donetype donetype)
+{
+	bool mem_to_dev = direction == DMA_MEM_TO_DEV;
+	u16 xsize, xsizehi;
+	u32 devcfg;
+	u32 memcfg;
+	u32 trigcfg;
+
+	desc->tsz = __ffs(width);
+	xsize = lower_16_bits(len >> desc->tsz);
+	xsizehi = upper_16_bits(len >> desc->tsz);
+	devcfg = d350_device_transcfg(maxburst);
+	memcfg = dch->coherent ? TRANSCFG_WB : TRANSCFG_NC;
+
+	trigcfg = FIELD_PREP(CH_TRIGINCFG_BLKSIZE,
+			     mem_to_dev ? maxburst - 1 : 0) |
[Severity: Medium]
Does this degrade performance for device-to-memory transfers?

The trigger block size ignores the configured maxburst and is hardcoded to 0
for DEV_TO_MEM transfers. This means only a single item is processed per
hardware trigger, which might cause peripheral FIFOs to overflow if they
expect burst servicing.
+		  FIELD_PREP(CH_TRIGINCFG_MODE, CH_TRIGINCFG_MODE_PERIPH_FC) |
+		  FIELD_PREP(CH_TRIGINCFG_TYPE, CH_TRIGINCFG_TYPE_HW) |
+		  FIELD_PREP(CH_TRIGINCFG_SEL, dch->req);
+
+	cmd[0] = LINK_CTRL | LINK_SRCADDR | LINK_SRCADDRHI | LINK_DESADDR |
+		 LINK_DESADDRHI | LINK_XSIZE | LINK_XSIZEHI | LINK_SRCTRANSCFG |
+		 LINK_DESTRANSCFG | LINK_XADDRINC | LINK_LINKADDR |
+		 LINK_LINKADDRHI |
+		 (mem_to_dev ? LINK_DESTRIGINCFG : LINK_SRCTRIGINCFG);
[Severity: High]
Will this cause corruption on big-endian CPUs?

Native CPU-endian words are being written directly to the DMA command list
buffer (desc->cmds), which will be fetched by the little-endian hardware.
Should this use cpu_to_le32 wrappers to avoid swapped addresses and sizes?

[ ... ]
+	cmd[11] = trigcfg;
+	cmd[12] = lower_32_bits(link_addr) |
+		  (link_addr ? CH_LINKADDR_EN : 0);
[Severity: High]
Is it safe to use link_addr as a boolean condition here?

dma_alloc_coherent() can validly return 0 as a bus address. If it does,
CH_LINKADDR_EN will not be set, which would prematurely terminate and break
cyclic or scatter-gather transfers.
+	cmd[13] = upper_32_bits(link_addr);
+}
[ ... ]
+static u32 d350_get_sg_residue(struct d350_chan *dch)
+{
+	struct d350_desc *desc = dch->desc;
+	size_t cmd_size = D350_SLAVE_CMD_WORDS * sizeof(u32);
+	size_t cmd = 0, i;
+	u32 residue;
+	u64 next_cmd;
+
+	if (!desc->cmd_len)
+		return d350_get_residue(dch);
+
+	/*
+	 * CH_LINKADDR points at the next command. Match it against the command
+	 * array to find the command currently executing, then add every later
+	 * command which has not started yet.
+	 */
+	next_cmd = readl_relaxed(dch->base + CH_LINKADDR) & ~CH_LINKADDR_EN;
+	next_cmd |= (u64)readl_relaxed(dch->base + CH_LINKADDRHI) << 32;
[Severity: Medium]
Could this result in a torn read of the 64-bit hardware CH_LINKADDR register?

The two 32-bit reads are executed sequentially without protection against
concurrent hardware updates, unlike the safe retry loop you used for CH_XSIZE.
If the hardware advances the command chain between the reads, the CPU constructs
a torn address.
+
+	if (!next_cmd) {
+		cmd = desc->ncmds - 1;
+	} else {
+		for (i = 1; i < desc->ncmds; i++) {
+			if (next_cmd == desc->cmds_bus + i * cmd_size) {
+				cmd = i - 1;
+				break;
+			}
+		}
+		if (i == desc->ncmds)
+			return dch->residue;
+	}
+
+	residue = d350_get_residue(dch);
[Severity: High]
Is there a race condition between reading CH_LINKADDR and CH_XSIZE?

d350_get_residue() reads CH_XSIZE. If the hardware completes command N and
starts N+1 between the earlier read of CH_LINKADDR and the read of CH_XSIZE,
the driver calculates residue using command N's position in the loop but
command N+1's remaining bytes. This would double-count the size of N+1 and
cause the reported residue to jump upwards, breaking monotonicity.
+	for (i = cmd + 1; i < desc->ncmds; i++)
+		residue += desc->cmd_len[i];
+
+	return residue;
+}
[ ... ]
+static u32 d350_get_cyclic_residue(struct d350_desc *desc)
+{
+	return desc->bytes - desc->period * desc->period_len;
+}
[Severity: High]
Does this provide sufficient precision for cyclic consumers like ALSA?

This calculates an inaccurate residue that ignores intra-period hardware
progress, returning only period boundaries. ALSA PCM pointers typically
require high-resolution, accurate sub-period updates.

[ ... ]
 static enum dma_status d350_tx_status(struct dma_chan *chan, dma_cookie_t cookie,
 				      struct dma_tx_state *state)
 {
 	struct d350_chan *dch = to_d350_chan(chan);
 	struct virt_dma_desc *vd;
 	enum dma_status status;
 	unsigned long flags;
 	u32 residue = 0;
 
 	status = dma_cookie_status(chan, cookie, state);
 
 	spin_lock_irqsave(&dch->vc.lock, flags);
 	if (cookie == dch->cookie) {
 		status = dch->status;
 		if (status == DMA_IN_PROGRESS || status == DMA_PAUSED)
-			dch->residue = d350_get_residue(dch);
+			dch->residue = d350_get_active_residue(dch);
 		residue = dch->residue;
 	} else if ((vd = vchan_find_desc(&dch->vc, cookie))) {
 		residue = d350_desc_bytes(to_d350_desc(vd));
 	} else if (status == DMA_IN_PROGRESS) {
 		/* Somebody else terminated it? */
 		status = DMA_ERROR;
 	}
[Severity: High]
This is a pre-existing issue, but does this incorrectly return DMA_ERROR for
submitted but unissued descriptors?

vchan_find_desc() only searches the desc_issued list. If a client queries
status after tx_submit but before issue_pending, this explicit overwrite
converts a valid DMA_IN_PROGRESS status into DMA_ERROR.

[ ... ]
 static irqreturn_t d350_irq(int irq, void *data)
 {
[ ... ]
+	if (irq_status & CH_STAT_INTR_ERR) {
[ ... ]
+	} else {
+		if (desc->cyclic) {
+			desc->period = (desc->period + 1) % desc->periods;
+			dch->residue = d350_get_cyclic_residue(desc);
+			vchan_cyclic_callback(vd);
[Severity: High]
Can this permanently desynchronize the cyclic stream?

Unconditionally advancing the period by 1 software counter will lag behind the
actual hardware position if interrupt coalescing occurs or interrupts are
delayed, causing multiple hardware periods to complete between IRQ handling.
+		} else {
+			dch->status = DMA_COMPLETE;
+			dch->residue = 0;
+			dch->desc = NULL;
+			vchan_cookie_complete(vd);
+			d350_start_next(dch);
+		}
 	}
 	spin_unlock(&dch->vc.lock);
 
 	return IRQ_HANDLED;
 }
-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260907033404.596839-1-Jelly.Jia@cixtech.com?part=2
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help