diff mbox series

serial: 8250: Fix fifo underflow on flush

Message ID 20250206155555.85093-1-jkeeping@inmusicbrands.com
State Superseded
Headers show
Series serial: 8250: Fix fifo underflow on flush | expand

Commit Message

John Keeping Feb. 6, 2025, 3:55 p.m. UTC
When flushing the serial port's buffer, uart_flush_buffer() calls
kfifo_reset() but if there is an outstanding DMA transfer then the
completion function will consume data from the kfifo via
uart_xmit_advance(), underflowing and leading to ongoing DMA as the
driver tries to transmit another 2^32 bytes.

This is readily reproduced with serial-generic and amidi sending even
short messages as closing the device on exit will wait for the fifo to
drain and in the underflow case amidi hangs for 30 seconds on exit in
tty_wait_until_sent().  A trace of that gives:

     kworker/1:1-84    [001]    51.769423: bprint:               serial8250_tx_dma: tx_size=3 fifo_len=3
           amidi-763   [001]    51.769460: bprint:               uart_flush_buffer: resetting fifo
 irq/21-fe530000-76    [000]    51.769474: bprint:               __dma_tx_complete: tx_size=3
 irq/21-fe530000-76    [000]    51.769479: bprint:               serial8250_tx_dma: tx_size=4096 fifo_len=4294967293
 irq/21-fe530000-76    [000]    51.781295: bprint:               __dma_tx_complete: tx_size=4096
 irq/21-fe530000-76    [000]    51.781301: bprint:               serial8250_tx_dma: tx_size=4096 fifo_len=4294963197
 irq/21-fe530000-76    [000]    51.793131: bprint:               __dma_tx_complete: tx_size=4096
 irq/21-fe530000-76    [000]    51.793135: bprint:               serial8250_tx_dma: tx_size=4096 fifo_len=4294959101
 irq/21-fe530000-76    [000]    51.804949: bprint:               __dma_tx_complete: tx_size=4096

Since the port lock is held in when the kfifo is reset in
uart_flush_buffer() and in __dma_tx_complete(), adding a flush_buffer
hook to adjust the outstanding DMA byte count is sufficient to avoid the
kfifo underflow.

Signed-off-by: John Keeping <jkeeping@inmusicbrands.com>
---
 drivers/tty/serial/8250/8250.h      |  1 +
 drivers/tty/serial/8250/8250_dma.c  | 15 +++++++++++++++
 drivers/tty/serial/8250/8250_port.c |  9 +++++++++
 3 files changed, 25 insertions(+)

Comments

John Keeping Feb. 7, 2025, 11:24 a.m. UTC | #1
On Thu, Feb 06, 2025 at 08:55:09PM +0200, Andy Shevchenko wrote:
> On Thu, Feb 06, 2025 at 03:55:51PM +0000, John Keeping wrote:
> > When flushing the serial port's buffer, uart_flush_buffer() calls
> > kfifo_reset() but if there is an outstanding DMA transfer then the
> > completion function will consume data from the kfifo via
> > uart_xmit_advance(), underflowing and leading to ongoing DMA as the
> > driver tries to transmit another 2^32 bytes.
> > 
> > This is readily reproduced with serial-generic and amidi sending even
> > short messages as closing the device on exit will wait for the fifo to
> > drain and in the underflow case amidi hangs for 30 seconds on exit in
> > tty_wait_until_sent().
> 
> Sounds not good user experience...
> 
> > A trace of that gives:
> > 
> >      kworker/1:1-84    [001]    51.769423: bprint:               serial8250_tx_dma: tx_size=3 fifo_len=3
> >            amidi-763   [001]    51.769460: bprint:               uart_flush_buffer: resetting fifo
> >  irq/21-fe530000-76    [000]    51.769474: bprint:               __dma_tx_complete: tx_size=3
> >  irq/21-fe530000-76    [000]    51.769479: bprint:               serial8250_tx_dma: tx_size=4096 fifo_len=4294967293
> >  irq/21-fe530000-76    [000]    51.781295: bprint:               __dma_tx_complete: tx_size=4096
> >  irq/21-fe530000-76    [000]    51.781301: bprint:               serial8250_tx_dma: tx_size=4096 fifo_len=4294963197
> >  irq/21-fe530000-76    [000]    51.793131: bprint:               __dma_tx_complete: tx_size=4096
> >  irq/21-fe530000-76    [000]    51.793135: bprint:               serial8250_tx_dma: tx_size=4096 fifo_len=4294959101
> >  irq/21-fe530000-76    [000]    51.804949: bprint:               __dma_tx_complete: tx_size=4096
> > 
> > Since the port lock is held in when the kfifo is reset in
> > uart_flush_buffer() and in __dma_tx_complete(), adding a flush_buffer
> > hook to adjust the outstanding DMA byte count is sufficient to avoid the
> > kfifo underflow.
> 
> Shouldn't this have a Fixes tag?

I'll add one in v2.

> > +void serial8250_tx_dma_flush(struct uart_8250_port *p)
> > +{
> > +	struct uart_8250_dma *dma = p->dma;
> 
> > +	if (dma->tx_running) {
> 
> 	if (!dma->tx_running)
> 		return;

Will change in v2.

> > +		/*
> > +		 * kfifo_reset() has been called by the serial core, avoid
> > +		 * advancing and underflowing in __dma_tx_complete().
> > +		 */
> > +		dma->tx_size = 0;
> > +
> > +		dmaengine_terminate_async(dma->rxchan);
> > +	}
> > +}

Thanks for the review!


Regards,
John
diff mbox series

Patch

diff --git a/drivers/tty/serial/8250/8250.h b/drivers/tty/serial/8250/8250.h
index 11e05aa014e54..8ef45622e4363 100644
--- a/drivers/tty/serial/8250/8250.h
+++ b/drivers/tty/serial/8250/8250.h
@@ -374,6 +374,7 @@  static inline int is_omap1510_8250(struct uart_8250_port *pt)
 
 #ifdef CONFIG_SERIAL_8250_DMA
 extern int serial8250_tx_dma(struct uart_8250_port *);
+extern void serial8250_tx_dma_flush(struct uart_8250_port *);
 extern int serial8250_rx_dma(struct uart_8250_port *);
 extern void serial8250_rx_dma_flush(struct uart_8250_port *);
 extern int serial8250_request_dma(struct uart_8250_port *);
diff --git a/drivers/tty/serial/8250/8250_dma.c b/drivers/tty/serial/8250/8250_dma.c
index d215c494ee24c..272b52cd2a18f 100644
--- a/drivers/tty/serial/8250/8250_dma.c
+++ b/drivers/tty/serial/8250/8250_dma.c
@@ -149,6 +149,21 @@  int serial8250_tx_dma(struct uart_8250_port *p)
 	return ret;
 }
 
+void serial8250_tx_dma_flush(struct uart_8250_port *p)
+{
+	struct uart_8250_dma *dma = p->dma;
+
+	if (dma->tx_running) {
+		/*
+		 * kfifo_reset() has been called by the serial core, avoid
+		 * advancing and underflowing in __dma_tx_complete().
+		 */
+		dma->tx_size = 0;
+
+		dmaengine_terminate_async(dma->rxchan);
+	}
+}
+
 int serial8250_rx_dma(struct uart_8250_port *p)
 {
 	struct uart_8250_dma		*dma = p->dma;
diff --git a/drivers/tty/serial/8250/8250_port.c b/drivers/tty/serial/8250/8250_port.c
index d7976a21cca9c..442967a6cd52d 100644
--- a/drivers/tty/serial/8250/8250_port.c
+++ b/drivers/tty/serial/8250/8250_port.c
@@ -2555,6 +2555,14 @@  static void serial8250_shutdown(struct uart_port *port)
 		serial8250_do_shutdown(port);
 }
 
+static void serial8250_flush_buffer(struct uart_port *port)
+{
+	struct uart_8250_port *up = up_to_u8250p(port);
+
+	if (up->dma)
+		serial8250_tx_dma_flush(up);
+}
+
 static unsigned int serial8250_do_get_divisor(struct uart_port *port,
 					      unsigned int baud,
 					      unsigned int *frac)
@@ -3244,6 +3252,7 @@  static const struct uart_ops serial8250_pops = {
 	.break_ctl	= serial8250_break_ctl,
 	.startup	= serial8250_startup,
 	.shutdown	= serial8250_shutdown,
+	.flush_buffer	= serial8250_flush_buffer,
 	.set_termios	= serial8250_set_termios,
 	.set_ldisc	= serial8250_set_ldisc,
 	.pm		= serial8250_pm,