Linux Serial subsystem development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Samarth Narang" <samarth.colleges@gmail.com>
Cc: linux-serial@vger.kernel.org
Subject: Re: [PATCH 5/5] serial: pch_uart: fix TX kfifo underflow on flush
Date: Sat, 31 Oct 2026 12:55:24 +0000	[thread overview]
Message-ID: <sashiko-outbox-191950@kernel.org> (raw)
In-Reply-To: <20261031123911.3784690-6-samarth.colleges@gmail.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] serial: pch_uart: unlocked sg_tx_p read and TX DMA state overwrite after stop_tx
- [High] serial: pch_uart: lock inversion and self-deadlock in pch_dma_tx_complete()
- [High] serial: pch_uart: active DMA buffer overwrite and duplicate TX after flush
--

commit 41c9c81259479647a63a193b50f8884cd2a2a063
Author: Samarth Narang <samarth.colleges@gmail.com>

serial: pch_uart: fix TX kfifo underflow on flush

This commit adds a flush_buffer() callback to mark in-flight TX DMA
transfers as flushed and takes the port lock in pch_dma_tx_complete() so
completed flushed transfers do not advance the transmit kfifo.

> diff --git a/drivers/tty/serial/pch_uart.c b/drivers/tty/serial/pch_uart.c
> index 9505d255f09c5..122025c4a0a66 100644
> --- a/drivers/tty/serial/pch_uart.c
> +++ b/drivers/tty/serial/pch_uart.c

[ ... ]

> @@ -750,18 +751,23 @@ static void pch_dma_tx_complete(void *arg)
>  	struct eg20t_port *priv = arg;
>  	struct uart_port *port = &priv->port;
>  	struct scatterlist *sg = priv->sg_tx_p;
> +	unsigned long flags;
>  	int i;
>  
> -	for (i = 0; i < priv->nent; i++, sg++)
> -		uart_xmit_advance(port, sg_dma_len(sg));
> +	uart_port_lock_irqsave(port, &flags);

[Severity: High]
Can taking port->lock in pch_dma_tx_complete() cause a lock inversion with
pd_chan->lock as well as a single-CPU recursive deadlock on port->lock?

In pch_uart_interrupt(), the interrupt handler holds priv->port.lock while
calling dma_handle_tx(), which calls into drivers/dma/pch_dma.c:

  pch_uart_interrupt()
    uart_port_lock(&priv->port)
    dma_handle_tx()
      dmaengine_prep_slave_sg() -> pd_prep_slave_sg() -> pdc_desc_get()
        spin_lock(&pd_chan->lock)
      dmaengine_submit() -> pd_tx_submit()
        spin_lock(&pd_chan->lock)
      dma_async_issue_pending() -> pd_issue_pending()
        spin_lock(&pd_chan->lock)

This establishes the lock order priv->port.lock -> pd_chan->lock.

In drivers/dma/pch_dma.c, every path that invokes the descriptor callback
pch_dma_tx_complete() holds pd_chan->lock:

  pdc_tasklet() / pd_issue_pending() / pd_device_terminate_all()
    spin_lock(&pd_chan->lock)
    pdc_advance_work() / pdc_complete_all() / pdc_handle_error()
      pdc_chain_complete()
        dmaengine_desc_callback_invoke()
          pch_dma_tx_complete()
            uart_port_lock_irqsave(port, &flags)

On SMP, if one CPU enters pch_uart_interrupt() -> dma_handle_tx() while
another CPU runs pdc_tasklet() -> pch_dma_tx_complete(), can this deadlock
between priv->port.lock and pd_chan->lock?

In addition, in pd_tx_submit(), when pd_chan->active_list is empty,
pdc_dostart() starts the hardware DMA transfer immediately. Two lines later
in dma_handle_tx() (still holding priv->port.lock),
dma_async_issue_pending() calls pd_issue_pending():

  if (pdc_is_idle(pd_chan)) {
          spin_lock(&pd_chan->lock);
          pdc_advance_work(pd_chan);
          spin_unlock(&pd_chan->lock);
  }

If a short transfer into the UART TX FIFO has already finished by the time
pdc_is_idle() is checked, pdc_advance_work() will invoke
pch_dma_tx_complete() synchronously on the same CPU while
pch_uart_interrupt() already holds priv->port.lock.

> +	if (!priv->tx_dma_flushed)
> +		for (i = 0; i < priv->nent; i++, sg++)
> +			uart_xmit_advance(port, sg_dma_len(sg));
>  
>  	async_tx_ack(priv->desc_tx);
>  	dma_unmap_sg(port->dev, priv->sg_tx_p, priv->orig_nent, DMA_TO_DEVICE);
>  	priv->tx_dma_use = 0;
> +	priv->tx_dma_flushed = false;
>  	priv->nent = 0;
>  	priv->orig_nent = 0;
>  	kfree(priv->sg_tx_p);
>  	pch_uart_hal_enable_interrupt(priv, PCH_UART_HAL_TX_INT);
> +	uart_port_unlock_irqrestore(port, flags);
>  }
>  
>  static int handle_rx_to(struct eg20t_port *priv)
> @@ -947,6 +953,7 @@ static unsigned int dma_handle_tx(struct eg20t_port *priv)
>  	}
>  	priv->orig_nent = num;
>  	priv->nent = nent;
> +	priv->tx_dma_flushed = false;

[Severity: Critical]
What happens if pch_uart_stop_tx() is called while a TX DMA transfer is in
flight, followed by a flush and a subsequent TX restart before that first
transfer completes?

In pch_dma_tx_complete(), sg is initialized from priv->sg_tx_p before
uart_port_lock_irqsave() is called:

  struct scatterlist *sg = priv->sg_tx_p;
  ...
  uart_port_lock_irqsave(port, &flags);
  if (!priv->tx_dma_flushed)
          for (i = 0; i < priv->nent; i++, sg++)
                  uart_xmit_advance(port, sg_dma_len(sg));

Meanwhile, pch_uart_stop_tx() clears priv->tx_dma_use = 0 without stopping
the in-flight DMA transfer. If TX is restarted via pch_uart_start_tx(),
dma_handle_tx() sees priv->tx_dma_use == 0 and starts a second TX DMA
transfer while the first transfer is still running:

  priv->sg_tx_p = kmalloc_objs(struct scatterlist, num, GFP_ATOMIC);
  ...
  priv->orig_nent = num;
  priv->nent = nent;
  priv->tx_dma_flushed = false;

When the first transfer's pch_dma_tx_complete() runs:

1. Because dma_handle_tx() reset priv->tx_dma_flushed to false, the flushed
first transfer still calls uart_xmit_advance() and underflows the kfifo.

2. If pch_dma_tx_complete() read sg = priv->sg_tx_p before dma_handle_tx()
replaced priv->sg_tx_p under port->lock, and the second transfer has a
larger priv->nent than the first, the loop over priv->nent reads past the
end of the first scatterlist allocation.

3. dma_unmap_sg() and kfree(priv->sg_tx_p) in the first completion will
unmap and free the second transfer's active scatterlist while the first
scatterlist is leaked, and the second completion will then unmap and kfree()
priv->sg_tx_p a second time.

>  
>  	for (i = 0; i < nent; i++, sg++) {
>  		sg->offset = tail + fifo_size * i;
> @@ -1150,6 +1157,21 @@ static void pch_uart_stop_tx(struct uart_port *port)
>  	priv->tx_dma_use = 0;
>  }
>  
> +static void pch_uart_flush_buffer(struct uart_port *port)
> +{
> +	struct eg20t_port *priv;
> +
> +	priv = container_of(port, struct eg20t_port, port);
> +
> +	/*
> +	 * The kfifo has been reset. Let the running transfer finish, so that
> +	 * its buffer is unmapped only once it has stopped, but do not advance
> +	 * the kfifo for it.
> +	 */
> +	if (priv->nent)
> +		priv->tx_dma_flushed = true;
> +}

[Severity: High]
Could leaving the TX DMA transfer running after uart_flush_buffer() resets
the kfifo cause post-flush writes to overwrite the active DMA buffer and be
transmitted twice?

When uart_flush_buffer() runs during an active TX DMA transfer:

  uart_flush_buffer()
    uart_port_lock_irqsave()
    kfifo_reset(&state->port.xmit_fifo)
    pch_uart_flush_buffer()
      priv->tx_dma_flushed = true
    uart_port_unlock_irqrestore()
    tty_port_tty_wakeup(&state->port)

Because kfifo_reset() resets xmit_fifo.in and xmit_fifo.out to offset 0 of
state->port.xmit_buf while the in-flight DMA transfer still has
state->port.xmit_buf mapped DMA_TO_DEVICE via priv->sg_tx_p, any new data
written through uart_write() or uart_put_char() before the DMA transfer
completes will write into state->port.xmit_buf starting at offset 0 while
DMA is actively reading from that buffer.

Then, if the in-flight DMA transfer reads those newly written bytes from
offset 0 and finishes, pch_dma_tx_complete() skips uart_xmit_advance()
because priv->tx_dma_flushed is set and enables PCH_UART_HAL_TX_INT, causing
dma_handle_tx() to transmit the same post-flush bytes from xmit_fifo a
second time.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261031123911.3784690-1-samarth.colleges@gmail.com?part=5

  reply	other threads:[~2026-10-31 12:55 UTC|newest]

Thread overview: 17+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-31  0:47 [BUG] serial: TX kfifo underflow when TCOFLUSH races with TX DMA (samsung, sprd, msm, pch_uart, tegra) Samarth Narang
2026-10-31  8:54 ` Greg Kroah-Hartman
2026-10-31 15:26   ` [PATCH 0/1] tty: serial: samsung: fix TX fifo underflow on flush race tianci xie
2026-10-31 15:26     ` [PATCH] " tianci xie
2026-10-31 15:48       ` sashiko-bot
2026-10-31 12:39 ` [PATCH 0/5] serial: fix TX kfifo underflow on flush in five drivers Samarth Narang
2026-10-31 12:39   ` [PATCH 1/5] tty: serial: samsung: fix TX kfifo underflow on flush Samarth Narang
2026-10-31 13:02     ` sashiko-bot
2026-10-31 12:39   ` [PATCH 2/5] serial: tegra: don't advance the TX kfifo for a flushed transfer Samarth Narang
2026-10-31 12:55     ` sashiko-bot
2026-10-31 12:39   ` [PATCH 3/5] serial: sprd: fix TX kfifo underflow on flush Samarth Narang
2026-10-31 12:58     ` sashiko-bot
2026-10-31 12:39   ` [PATCH 4/5] tty: serial: msm: " Samarth Narang
2026-10-31 12:55     ` sashiko-bot
2026-10-31 12:39   ` [PATCH 5/5] serial: pch_uart: " Samarth Narang
2026-10-31 12:55     ` sashiko-bot [this message]
2026-10-31 12:59   ` [PATCH 0/5] serial: fix TX kfifo underflow on flush in five drivers Greg Kroah-Hartman

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-191950@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=linux-serial@vger.kernel.org \
    --cc=samarth.colleges@gmail.com \
    --cc=sashiko-reviews@lists.linux.dev \
    /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