linux-serial.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH 0/2] tty/serial: Fix tcsetattr() TCSADRAIN/FLUSH and write() races
@ 2023-03-16 13:24 Ilpo Järvinen
  2023-03-16 13:24 ` [PATCH 1/2] tty: Prevent writing chars during tcsetattr TCSADRAIN/FLUSH Ilpo Järvinen
  2023-03-16 13:24 ` [PATCH 2/2] serial: 8250: Fix serial8250_tx_empty() race with DMA Tx Ilpo Järvinen
  0 siblings, 2 replies; 5+ messages in thread
From: Ilpo Järvinen @ 2023-03-16 13:24 UTC (permalink / raw)
  To: linux-serial, Greg Kroah-Hartman, Jiri Slaby
  Cc: linux-kernel, Ilpo Järvinen

Fix two races related TCSADRAIN/FLUSH. One in tty core and the other
that can occur when 8250 UART is using DMA for Tx.

These might be slightly controversial as it requires userspace to race
write() with tcsetattr() to trigger these races. But since the races
still seem fixable on kernel side, I made these patches.

Ilpo Järvinen (2):
  tty: Prevent writing chars during tcsetattr TCSADRAIN/FLUSH
  serial: 8250: Fix serial8250_tx_empty() race with DMA Tx

 drivers/tty/serial/8250/8250.h      | 12 ++++++++
 drivers/tty/serial/8250/8250_port.c |  7 ++++-
 drivers/tty/tty.h                   |  2 ++
 drivers/tty/tty_io.c                |  4 +--
 drivers/tty/tty_ioctl.c             | 45 +++++++++++++++++++++--------
 5 files changed, 55 insertions(+), 15 deletions(-)

-- 
2.30.2


^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH 1/2] tty: Prevent writing chars during tcsetattr TCSADRAIN/FLUSH
  2023-03-16 13:24 [PATCH 0/2] tty/serial: Fix tcsetattr() TCSADRAIN/FLUSH and write() races Ilpo Järvinen
@ 2023-03-16 13:24 ` Ilpo Järvinen
  2023-03-16 13:24 ` [PATCH 2/2] serial: 8250: Fix serial8250_tx_empty() race with DMA Tx Ilpo Järvinen
  1 sibling, 0 replies; 5+ messages in thread
From: Ilpo Järvinen @ 2023-03-16 13:24 UTC (permalink / raw)
  To: linux-serial, Greg Kroah-Hartman, Jiri Slaby, linux-kernel
  Cc: Ilpo Järvinen, stable

If userspace races tcsetattr() with a write, the drained condition
might not be guaranteed by the kernel. There is a race window after
checking Tx is empty before tty_set_termios() takes termios_rwsem for
write. During that race window, more characters can be queued by a
racing writer.

Any ongoing transmission might produce garbage during HW's
->set_termios() call. The intent of TCSADRAIN/FLUSH seems to be
preventing such a character corruption. If those flags are set, take
tty's write lock to stop any writer before performing the lower layer
Tx empty check and wait for the pending characters to be sent (if any).

The initial wait for all-writers-done must be placed outside of tty's
write lock to avoid deadlock which makes it impossible to use
tty_wait_until_sent(). The write lock is retried if a racing write is
detected.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
---
 drivers/tty/tty.h       |  2 ++
 drivers/tty/tty_io.c    |  4 ++--
 drivers/tty/tty_ioctl.c | 45 ++++++++++++++++++++++++++++++-----------
 3 files changed, 37 insertions(+), 14 deletions(-)

diff --git a/drivers/tty/tty.h b/drivers/tty/tty.h
index f45cd683c02e..1e0d80e98d26 100644
--- a/drivers/tty/tty.h
+++ b/drivers/tty/tty.h
@@ -62,6 +62,8 @@ int __tty_check_change(struct tty_struct *tty, int sig);
 int tty_check_change(struct tty_struct *tty);
 void __stop_tty(struct tty_struct *tty);
 void __start_tty(struct tty_struct *tty);
+void tty_write_unlock(struct tty_struct *tty);
+int tty_write_lock(struct tty_struct *tty, int ndelay);
 void tty_vhangup_session(struct tty_struct *tty);
 void tty_open_proc_set_tty(struct file *filp, struct tty_struct *tty);
 int tty_signal_session_leader(struct tty_struct *tty, int exit_session);
diff --git a/drivers/tty/tty_io.c b/drivers/tty/tty_io.c
index 766750e355ac..cfb3da0dee47 100644
--- a/drivers/tty/tty_io.c
+++ b/drivers/tty/tty_io.c
@@ -933,13 +933,13 @@ static ssize_t tty_read(struct kiocb *iocb, struct iov_iter *to)
 	return i;
 }
 
-static void tty_write_unlock(struct tty_struct *tty)
+void tty_write_unlock(struct tty_struct *tty)
 {
 	mutex_unlock(&tty->atomic_write_lock);
 	wake_up_interruptible_poll(&tty->write_wait, EPOLLOUT);
 }
 
-static int tty_write_lock(struct tty_struct *tty, int ndelay)
+int tty_write_lock(struct tty_struct *tty, int ndelay)
 {
 	if (!mutex_trylock(&tty->atomic_write_lock)) {
 		if (ndelay)
diff --git a/drivers/tty/tty_ioctl.c b/drivers/tty/tty_ioctl.c
index 12983ce4e43e..a13e3797c477 100644
--- a/drivers/tty/tty_ioctl.c
+++ b/drivers/tty/tty_ioctl.c
@@ -500,21 +500,42 @@ static int set_termios(struct tty_struct *tty, void __user *arg, int opt)
 	tmp_termios.c_ispeed = tty_termios_input_baud_rate(&tmp_termios);
 	tmp_termios.c_ospeed = tty_termios_baud_rate(&tmp_termios);
 
-	ld = tty_ldisc_ref(tty);
+	if (opt & (TERMIOS_FLUSH|TERMIOS_WAIT)) {
+retry_write_wait:
+		retval = wait_event_interruptible(tty->write_wait, !tty_chars_in_buffer(tty));
+		if (retval < 0)
+			return retval;
 
-	if (ld != NULL) {
-		if ((opt & TERMIOS_FLUSH) && ld->ops->flush_buffer)
-			ld->ops->flush_buffer(tty);
-		tty_ldisc_deref(ld);
-	}
+		if (tty_write_lock(tty, 0) < 0)
+			goto retry_write_wait;
 
-	if (opt & TERMIOS_WAIT) {
-		tty_wait_until_sent(tty, 0);
-		if (signal_pending(current))
-			return -ERESTARTSYS;
-	}
+		/* Racing writer? */
+		if (tty_chars_in_buffer(tty)) {
+			tty_write_unlock(tty);
+			goto retry_write_wait;
+		}
 
-	tty_set_termios(tty, &tmp_termios);
+		ld = tty_ldisc_ref(tty);
+		if (ld != NULL) {
+			if ((opt & TERMIOS_FLUSH) && ld->ops->flush_buffer)
+				ld->ops->flush_buffer(tty);
+			tty_ldisc_deref(ld);
+		}
+
+		if ((opt & TERMIOS_WAIT) && tty->ops->wait_until_sent) {
+			tty->ops->wait_until_sent(tty, 0);
+			if (signal_pending(current)) {
+				tty_write_unlock(tty);
+				return -ERESTARTSYS;
+			}
+		}
+
+		tty_set_termios(tty, &tmp_termios);
+
+		tty_write_unlock(tty);
+	} else {
+		tty_set_termios(tty, &tmp_termios);
+	}
 
 	/* FIXME: Arguably if tmp_termios == tty->termios AND the
 	   actual requested termios was not tmp_termios then we may
-- 
2.30.2


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH 2/2] serial: 8250: Fix serial8250_tx_empty() race with DMA Tx
  2023-03-16 13:24 [PATCH 0/2] tty/serial: Fix tcsetattr() TCSADRAIN/FLUSH and write() races Ilpo Järvinen
  2023-03-16 13:24 ` [PATCH 1/2] tty: Prevent writing chars during tcsetattr TCSADRAIN/FLUSH Ilpo Järvinen
@ 2023-03-16 13:24 ` Ilpo Järvinen
  2023-03-17  8:09   ` David Laight
  1 sibling, 1 reply; 5+ messages in thread
From: Ilpo Järvinen @ 2023-03-16 13:24 UTC (permalink / raw)
  To: linux-serial, Greg Kroah-Hartman, Jiri Slaby, Heikki Krogerus,
	linux-kernel
  Cc: Ilpo Järvinen, stable

There's a potential race before THRE/TEMT deasserts when DMA Tx is
starting up (or the next batch of continuous Tx is being submitted).
This can lead to misdetecting Tx empty condition.

It is entirely normal for THRE/TEMT to be set for some time after the
DMA Tx had been setup in serial8250_tx_dma(). As Tx side is definitely
not empty at that point, it seems incorrect for serial8250_tx_empty()
claim Tx is empty.

Fix the race by also checking in serial8250_tx_empty() whether there's
DMA Tx active.

Note: This fix only addresses in-kernel race mainly to make using
TCSADRAIN/FLUSH robust. Userspace can still cause other races but they
seem userspace concurrency control problems.

Fixes: 9ee4b83e51f74 ("serial: 8250: Add support for dmaengine")
Cc: stable@vger.kernel.org
Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
---
 drivers/tty/serial/8250/8250.h      | 12 ++++++++++++
 drivers/tty/serial/8250/8250_port.c |  7 ++++++-
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/drivers/tty/serial/8250/8250.h b/drivers/tty/serial/8250/8250.h
index 287153d32536..1e8fe44a7099 100644
--- a/drivers/tty/serial/8250/8250.h
+++ b/drivers/tty/serial/8250/8250.h
@@ -365,6 +365,13 @@ static inline void serial8250_do_prepare_rx_dma(struct uart_8250_port *p)
 	if (dma->prepare_rx_dma)
 		dma->prepare_rx_dma(p);
 }
+
+static inline bool serial8250_tx_dma_running(struct uart_8250_port *p)
+{
+	struct uart_8250_dma *dma = p->dma;
+
+	return dma && dma->tx_running;
+}
 #else
 static inline int serial8250_tx_dma(struct uart_8250_port *p)
 {
@@ -380,6 +387,11 @@ static inline int serial8250_request_dma(struct uart_8250_port *p)
 	return -1;
 }
 static inline void serial8250_release_dma(struct uart_8250_port *p) { }
+
+static inline bool serial8250_tx_dma_running(struct uart_8250_port *p)
+{
+	return false;
+}
 #endif
 
 static inline int ns16550a_goto_highspeed(struct uart_8250_port *up)
diff --git a/drivers/tty/serial/8250/8250_port.c b/drivers/tty/serial/8250/8250_port.c
index fa43df05342b..4954c4f15fb2 100644
--- a/drivers/tty/serial/8250/8250_port.c
+++ b/drivers/tty/serial/8250/8250_port.c
@@ -2006,17 +2006,22 @@ static unsigned int serial8250_tx_empty(struct uart_port *port)
 {
 	struct uart_8250_port *up = up_to_u8250p(port);
 	unsigned long flags;
+	bool dma_tx_running;
 	u16 lsr;
 
 	serial8250_rpm_get(up);
 
 	spin_lock_irqsave(&port->lock, flags);
 	lsr = serial_lsr_in(up);
+	dma_tx_running = serial8250_tx_dma_running(up);
 	spin_unlock_irqrestore(&port->lock, flags);
 
 	serial8250_rpm_put(up);
 
-	return uart_lsr_tx_empty(lsr) ? TIOCSER_TEMT : 0;
+	if (uart_lsr_tx_empty(lsr) && !dma_tx_running)
+		return TIOCSER_TEMT;
+
+	return 0;
 }
 
 unsigned int serial8250_do_get_mctrl(struct uart_port *port)
-- 
2.30.2


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* RE: [PATCH 2/2] serial: 8250: Fix serial8250_tx_empty() race with DMA Tx
  2023-03-16 13:24 ` [PATCH 2/2] serial: 8250: Fix serial8250_tx_empty() race with DMA Tx Ilpo Järvinen
@ 2023-03-17  8:09   ` David Laight
  2023-03-17 10:51     ` Ilpo Järvinen
  0 siblings, 1 reply; 5+ messages in thread
From: David Laight @ 2023-03-17  8:09 UTC (permalink / raw)
  To: 'Ilpo Järvinen',
	linux-serial, Greg Kroah-Hartman, Jiri Slaby, Heikki Krogerus,
	linux-kernel
  Cc: stable

From: Ilpo Järvinen
> Sent: 16 March 2023 13:25
> 
> There's a potential race before THRE/TEMT deasserts when DMA Tx is
> starting up (or the next batch of continuous Tx is being submitted).
> This can lead to misdetecting Tx empty condition.
> 
> It is entirely normal for THRE/TEMT to be set for some time after the
> DMA Tx had been setup in serial8250_tx_dma(). As Tx side is definitely
> not empty at that point, it seems incorrect for serial8250_tx_empty()
> claim Tx is empty.
> 
> Fix the race by also checking in serial8250_tx_empty() whether there's
> DMA Tx active.
> 
> Note: This fix only addresses in-kernel race mainly to make using
> TCSADRAIN/FLUSH robust. Userspace can still cause other races but they
> seem userspace concurrency control problems.
> 
> Fixes: 9ee4b83e51f74 ("serial: 8250: Add support for dmaengine")
> Cc: stable@vger.kernel.org
> Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
> ---
>  drivers/tty/serial/8250/8250.h      | 12 ++++++++++++
>  drivers/tty/serial/8250/8250_port.c |  7 ++++++-
>  2 files changed, 18 insertions(+), 1 deletion(-)
> 
> diff --git a/drivers/tty/serial/8250/8250.h b/drivers/tty/serial/8250/8250.h
...
>  static inline int ns16550a_goto_highspeed(struct uart_8250_port *up)
> diff --git a/drivers/tty/serial/8250/8250_port.c b/drivers/tty/serial/8250/8250_port.c
> index fa43df05342b..4954c4f15fb2 100644
> --- a/drivers/tty/serial/8250/8250_port.c
> +++ b/drivers/tty/serial/8250/8250_port.c
> @@ -2006,17 +2006,22 @@ static unsigned int serial8250_tx_empty(struct uart_port *port)
>  {
>  	struct uart_8250_port *up = up_to_u8250p(port);
>  	unsigned long flags;
> +	bool dma_tx_running;
>  	u16 lsr;
> 
>  	serial8250_rpm_get(up);
> 
>  	spin_lock_irqsave(&port->lock, flags);
>  	lsr = serial_lsr_in(up);
> +	dma_tx_running = serial8250_tx_dma_running(up);
>  	spin_unlock_irqrestore(&port->lock, flags);
> 
>  	serial8250_rpm_put(up);
> 
> -	return uart_lsr_tx_empty(lsr) ? TIOCSER_TEMT : 0;
> +	if (uart_lsr_tx_empty(lsr) && !dma_tx_running)
> +		return TIOCSER_TEMT;
> +
> +	return 0;
>  }

Since checking whether dma is active is fast but reading
the lsr is slow it is probably better to generate the return
value inside the lock and test for dma first.
Something like:
	spin_lock()
	result = 0;
	if (!serial8250_tx_dma_running(up) &&
	    uart_lsr_tx_empty(serial_lsr_in(up)))
		result = TIOCSER_TEMT;
	}
	spin_unlock()
	...
	return result;

	David

-
Registered Address Lakeside, Bramley Road, Mount Farm, Milton Keynes, MK1 1PT, UK
Registration No: 1397386 (Wales)

^ permalink raw reply	[flat|nested] 5+ messages in thread

* RE: [PATCH 2/2] serial: 8250: Fix serial8250_tx_empty() race with DMA Tx
  2023-03-17  8:09   ` David Laight
@ 2023-03-17 10:51     ` Ilpo Järvinen
  0 siblings, 0 replies; 5+ messages in thread
From: Ilpo Järvinen @ 2023-03-17 10:51 UTC (permalink / raw)
  To: David Laight
  Cc: linux-serial, Greg Kroah-Hartman, Jiri Slaby, Heikki Krogerus,
	linux-kernel, stable

[-- Attachment #1: Type: text/plain, Size: 2689 bytes --]

On Fri, 17 Mar 2023, David Laight wrote:

> From: Ilpo Järvinen
> > Sent: 16 March 2023 13:25
> > 
> > There's a potential race before THRE/TEMT deasserts when DMA Tx is
> > starting up (or the next batch of continuous Tx is being submitted).
> > This can lead to misdetecting Tx empty condition.
> > 
> > It is entirely normal for THRE/TEMT to be set for some time after the
> > DMA Tx had been setup in serial8250_tx_dma(). As Tx side is definitely
> > not empty at that point, it seems incorrect for serial8250_tx_empty()
> > claim Tx is empty.
> > 
> > Fix the race by also checking in serial8250_tx_empty() whether there's
> > DMA Tx active.
> > 
> > Note: This fix only addresses in-kernel race mainly to make using
> > TCSADRAIN/FLUSH robust. Userspace can still cause other races but they
> > seem userspace concurrency control problems.
> > 
> > Fixes: 9ee4b83e51f74 ("serial: 8250: Add support for dmaengine")
> > Cc: stable@vger.kernel.org
> > Signed-off-by: Ilpo Järvinen <ilpo.jarvinen@linux.intel.com>
> > ---
> >  drivers/tty/serial/8250/8250.h      | 12 ++++++++++++
> >  drivers/tty/serial/8250/8250_port.c |  7 ++++++-
> >  2 files changed, 18 insertions(+), 1 deletion(-)
> > 
> > diff --git a/drivers/tty/serial/8250/8250.h b/drivers/tty/serial/8250/8250.h
> ...
> >  static inline int ns16550a_goto_highspeed(struct uart_8250_port *up)
> > diff --git a/drivers/tty/serial/8250/8250_port.c b/drivers/tty/serial/8250/8250_port.c
> > index fa43df05342b..4954c4f15fb2 100644
> > --- a/drivers/tty/serial/8250/8250_port.c
> > +++ b/drivers/tty/serial/8250/8250_port.c
> > @@ -2006,17 +2006,22 @@ static unsigned int serial8250_tx_empty(struct uart_port *port)
> >  {
> >  	struct uart_8250_port *up = up_to_u8250p(port);
> >  	unsigned long flags;
> > +	bool dma_tx_running;
> >  	u16 lsr;
> > 
> >  	serial8250_rpm_get(up);
> > 
> >  	spin_lock_irqsave(&port->lock, flags);
> >  	lsr = serial_lsr_in(up);
> > +	dma_tx_running = serial8250_tx_dma_running(up);
> >  	spin_unlock_irqrestore(&port->lock, flags);
> > 
> >  	serial8250_rpm_put(up);
> > 
> > -	return uart_lsr_tx_empty(lsr) ? TIOCSER_TEMT : 0;
> > +	if (uart_lsr_tx_empty(lsr) && !dma_tx_running)
> > +		return TIOCSER_TEMT;
> > +
> > +	return 0;
> >  }
> 
> Since checking whether dma is active is fast but reading
> the lsr is slow it is probably better to generate the return
> value inside the lock and test for dma first.
> Something like:
> 	spin_lock()
> 	result = 0;
> 	if (!serial8250_tx_dma_running(up) &&
> 	    uart_lsr_tx_empty(serial_lsr_in(up)))
> 		result = TIOCSER_TEMT;
> 	}
> 	spin_unlock()
> 	...
> 	return result;
> 
> 	David

Thanks, I'll change it to something along those lines.

-- 
 i.

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2023-03-17 10:51 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2023-03-16 13:24 [PATCH 0/2] tty/serial: Fix tcsetattr() TCSADRAIN/FLUSH and write() races Ilpo Järvinen
2023-03-16 13:24 ` [PATCH 1/2] tty: Prevent writing chars during tcsetattr TCSADRAIN/FLUSH Ilpo Järvinen
2023-03-16 13:24 ` [PATCH 2/2] serial: 8250: Fix serial8250_tx_empty() race with DMA Tx Ilpo Järvinen
2023-03-17  8:09   ` David Laight
2023-03-17 10:51     ` Ilpo Järvinen

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).