Linux Serial subsystem development
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "James Hilliard" <james.hilliard1@gmail.com>
Cc: linux-serial@vger.kernel.org
Subject: Re: [PATCH v4 2/4] serial: core: allow drivers to reserve flushed transmit storage
Date: Fri, 23 Oct 2026 23:43:16 +0000	[thread overview]
Message-ID: <sashiko-outbox-183428@kernel.org> (raw)
In-Reply-To: <20261023-submit-serial-8250-dma-cancel-v1-v4-2-e759de1839de@gmail.com>

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] serial: core: race and late unblock in uart_resume_port()
- [High] serial: core: stale xmit_blocked in uart_alloc_xmit_buf() when reusing xmit_buf
- [High] serial: core: permanent TX block if uart_xmit_unblock() runs during startup
- [Medium] serial: core: lost deferred write wakeup when restarting open port
--

commit f5e8cd060228c45857c337ffa1d0fd793601a7e0
Author: James Hilliard <james.hilliard1@gmail.com>

serial: core: allow drivers to reserve flushed transmit storage

This commit adds uart_xmit_block() and uart_xmit_unblock() so UART drivers can
reserve the transmit FIFO buffer after uart_flush_buffer() until any in-flight
asynchronous DMA transfer and its completion callbacks have retired. While
blocked, write_room() reports zero, write() and put_char() reject new data,
and write wakeups are deferred until the reservation is released.

> diff --git a/drivers/tty/serial/serial_core.c b/drivers/tty/serial/serial_core.c
> index c0deec0e6af41..e5756934b8f47 100644
> --- a/drivers/tty/serial/serial_core.c
> +++ b/drivers/tty/serial/serial_core.c
> @@ -119,10 +119,56 @@ void uart_write_wakeup(struct uart_port *port)

[ ... ]

> +void uart_xmit_unblock(struct uart_port *port)
> +{
> +	lockdep_assert_held_once(&port->lock);
> +	if (!port->state->xmit_blocked || !tty_port_initialized(&port->state->port))
> +		return;
> +	port->state->xmit_blocked = false;
> +	uart_write_wakeup(port);
> +}
> +EXPORT_SYMBOL_GPL(uart_xmit_unblock);

[Severity: High]
Can checking !tty_port_initialized(&port->state->port) in uart_xmit_unblock()
leave state->xmit_blocked permanently stuck at true if a flush and DMA
retirement complete during port startup?

In tty_port_open(), uart_set_info() (TIOCSSERIAL), and uart_do_autoconfig()
(TIOCSERCONFIG), uart_startup() -> uart_port_startup() allocates
state->port.xmit_buf, invokes uport->ops->startup(uport) (enabling interrupts
and DMA), and configures line settings before tty_port_set_initialized(port,
true) is called:

drivers/tty/tty_port.c:tty_port_open() {
	...
		clear_bit(TTY_IO_ERROR, &tty->flags);
		if (port->ops->activate) {
			int retval = port->ops->activate(port, tty);
			if (retval) {
				set_bit(TTY_IO_ERROR, &tty->flags);
				return retval;
			}
		}
		tty_port_set_initialized(port, true);
	...
}

During that window, tty_port_initialized() is still false, while TTY_IO_ERROR
is already cleared and neither uart_write(), uart_put_char(), nor
uart_flush_buffer() checks tty_port_initialized() or acquires port->mutex.

If TX DMA is started and flushed in this window (for example via RX echo and
a VINTR/VQUIT/VSUSP signal character in n_tty_receive_signal_char() ->
isig() -> tty_driver_flush_buffer(), or via a concurrent write() and
ioctl(TCFLSH, TCOFLUSH) on an existing descriptor), uart_flush_buffer() calls
port->ops->flush_buffer(port) -> uart_xmit_block().

If that DMA transfer finishes retiring and calls uart_xmit_unblock() before
tty_port_set_initialized(port, true) runs, uart_xmit_unblock() sees
!tty_port_initialized() and returns early without clearing
state->xmit_blocked. When tty_port_set_initialized(port, true) subsequently
executes in tty_port_open(), uart_set_info(), or uart_do_autoconfig(),
nothing calls uart_xmit_unblock(), leaving TX permanently wedged with
uart_write_room() returning 0 and uart_write()/uart_put_char() dropping all
writes.

[ ... ]

> @@ -260,6 +309,7 @@ static int uart_alloc_xmit_buf(struct tty_port *port)
>  	uport = uart_port_ref_lock(state, &flags);
>  	if (!state->port.xmit_buf) {
>  		state->port.xmit_buf = buf;
> +		state->xmit_blocked = false;
>  		kfifo_init(&state->port.xmit_fifo, state->port.xmit_buf,
>  				PAGE_SIZE);
>  		uart_port_unlock_deref(uport, flags);

[Severity: High]
What happens in uart_alloc_xmit_buf() when state->port.xmit_buf is already
allocated and the else branch is taken?

In uart_alloc_xmit_buf(), state->xmit_blocked is only cleared when
state->port.xmit_buf is NULL:

drivers/tty/serial/serial_core.c:uart_alloc_xmit_buf() {
	...
	uport = uart_port_ref_lock(state, &flags);
	if (!state->port.xmit_buf) {
		state->port.xmit_buf = buf;
		state->xmit_blocked = false;
		kfifo_init(&state->port.xmit_fifo, state->port.xmit_buf,
				PAGE_SIZE);
		uart_port_unlock_deref(uport, flags);
	} else {
		uart_port_unlock_deref(uport, flags);
		kfree(buf);
	}
}

If uart_flush_buffer() sets state->xmit_blocked = true on an open port and
uart_suspend_port() then runs, uart_suspend_port() sets
tty_port_set_initialized(port, false) and calls ops->shutdown(uport) to
retire DMA without freeing state->port.xmit_buf, leaving state->xmit_blocked
set to true.

If the port is then closed while tty_port_initialized(port) is false,
tty_port_shutdown_locked() returns early without calling
uart_tty_port_shutdown() or uart_free_xmit_buf():

drivers/tty/tty_port.c:tty_port_shutdown_locked() {
	if (!tty_port_initialized(port))
		return;
	...
}

This can be reached without uart_resume_port() having run its unblock path,
for example when:
- a driver's suspend or freeze callback fails after calling
  uart_suspend_port() or serial8250_suspend_port() (such as
  omap8250_suspend(), pci1xxxx_suspend(), or imx_uart_suspend()), so the PM
  core skips the driver's resume callback;
- a driver's resume callback fails before calling uart_resume_port() or
  serial8250_resume_port() (such as omap8250_resume(), pci1xxxx_resume(),
  bcm2835aux_resume(), brcmuart_resume(), loongson_uart_resume(), or
  uniphier_uart_resume());
- a PCMCIA serial port (serial_cs.c) is suspended at runtime via sysfs
  pm_state and then closed and reopened; or
- a console port is suspended with console_suspend_enabled == true and
  resumed after console_suspend_enabled is set to false, skipping the
  (console_suspend_enabled || !uart_console(uport)) block in
  uart_resume_port().

On a subsequent open (uart_open() -> tty_port_open() -> uart_port_activate()
-> uart_startup() -> uart_port_startup() -> uart_alloc_xmit_buf()) or
reconfiguration (uart_set_info() -> uart_startup() -> uart_port_startup()),
uart_alloc_xmit_buf() sees state->port.xmit_buf != NULL, takes the else
branch, and leaves state->xmit_blocked set to true with no active DMA
transfer to ever call uart_xmit_unblock(). Could state->xmit_blocked remain
stuck true and permanently block all transmit operations on the port?

[Severity: Medium]
Also in uart_alloc_xmit_buf(), if state->xmit_blocked was true and a new
xmit_buf is allocated while the TTY remains open, could clearing
state->xmit_blocked = false directly without calling uart_write_wakeup() lose
a deferred write wakeup?

When uart_flush_buffer() runs on a port whose driver calls uart_xmit_block(),
uart_write_wakeup() suppresses tty_port_tty_wakeup(&state->port) and defers
waking writers on tty->write_wait and one-shot TTY_DO_WRITE_WAKEUP line
discipline callbacks until the reservation is released.

If the open port is then shut down and restarted via TIOCSERCONFIG
(uart_do_autoconfig()) or TIOCSSERIAL (uart_set_info() when change_port or
change_irq is true), or if uart_resume_port() fails ops->startup(uport) and
calls uart_shutdown(tty, state) followed by a later open() on the still-open
TTY, uart_shutdown() clears TTY_PORT_INITIALIZED before ops->shutdown(uport)
(making any driver uart_xmit_unblock() call a no-op) and frees xmit_buf via
uart_free_xmit_buf().

When uart_startup() -> uart_port_startup() -> uart_alloc_xmit_buf() then
allocates a new xmit_buf, it sets state->xmit_blocked = false without calling
uart_write_wakeup(), and nothing in the startup path triggers a TX wakeup
because the newly allocated FIFO is empty. Wouldn't writers sleeping on
tty->write_wait or line disciplines waiting for write_wakeup() stall?

[ ... ]

> @@ -2500,6 +2551,8 @@ int uart_resume_port(struct uart_driver *drv, struct uart_port *uport)
>  					ops->start_tx(uport);
>  				}
>  				tty_port_set_initialized(port, true);
> +				scoped_guard(uart_port_lock_irq, uport)
> +					uart_xmit_unblock(uport);
>  			} else {
>  				/*
>  				 * Failed to resume - maybe hardware went away?

[Severity: High]
In uart_resume_port(), is calling uart_xmit_unblock(uport) after
ops->startup(uport), uart_change_line_settings(), and ops->start_tx(uport)
have already run and dropped uport->lock safe against a concurrent
uart_flush_buffer()?

Looking at the resume sequence in uart_resume_port():

drivers/tty/serial/serial_core.c:uart_resume_port() {
	...
			ret = ops->startup(uport);
			if (ret == 0) {
				if (tty)
					uart_change_line_settings(tty, state, NULL);
				uart_rs485_config(uport);
				scoped_guard(uart_port_lock_irq, uport) {
					if (!(uport->rs485.flags & SER_RS485_ENABLED))
						ops->set_mctrl(uport, uport->mctrl);
					ops->start_tx(uport);
				}
				tty_port_set_initialized(port, true);
				scoped_guard(uart_port_lock_irq, uport)
					uart_xmit_unblock(uport);
			}
}

This placement appears to create two issues:

1. If state->xmit_blocked was false prior to suspend, ops->start_tx(uport) can
   start a new TX DMA transfer from state->port.xmit_buf (using data queued
   before suspend or written after ops->startup()). When the first
   scoped_guard(uart_port_lock_irq, uport) exits, uport->lock is released
   before tty_port_set_initialized(port, true) and the second scoped_guard
   calling uart_xmit_unblock(uport).

   Because uart_flush_buffer() only serializes on uport->lock and does not
   take port->mutex, a concurrent uart_flush_buffer() in that unlocked window
   (for example via tcflush(TCOFLUSH), serdev_device_write_flush(), or an RX
   signal character in n_tty_receive_signal_char() ->
   tty_driver_flush_buffer()) will reset xmit_fifo and call
   port->ops->flush_buffer(port) -> uart_xmit_block(uport) to reserve
   state->port.xmit_buf for the newly started post-resume DMA transfer.

   Immediately afterward, uart_resume_port() sets
   tty_port_set_initialized(port, true), re-acquires uport->lock, and calls
   uart_xmit_unblock(uport), which cannot distinguish a stale pre-suspend
   reservation from the new post-resume reservation. Doesn't this clear
   state->xmit_blocked = false and wake writers to overwrite
   state->port.xmit_buf while the post-resume TX DMA transfer is still in
   flight?

2. Conversely, if state->xmit_blocked was true before suspend (and the old
   transfer was already retired by ops->shutdown(uport) in
   uart_suspend_port()), state->xmit_blocked remains true while
   uart_change_line_settings() runs during resume. If uport->hw_stopped
   transitions from true to false in uart_change_line_settings(),
   __uart_start(state) sees state->xmit_blocked && !port->x_char and returns
   early without starting TX.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261023-submit-serial-8250-dma-cancel-v1-v4-0-e759de1839de@gmail.com?part=2

  reply	other threads:[~2026-10-23 23:43 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-23 22:41 [PATCH v4 0/4] serial: 8250: serialize DMA cancellation with buffer reuse James Hilliard
2026-10-23 22:41 ` [PATCH v4 1/4] serial: 8250_omap: separate locked TX DMA completion processing James Hilliard
2026-10-23 22:49   ` sashiko-bot
2026-10-23 22:41 ` [PATCH v4 2/4] serial: core: allow drivers to reserve flushed transmit storage James Hilliard
2026-10-23 23:43   ` sashiko-bot [this message]
2026-10-23 22:41 ` [PATCH v4 3/4] serial: 8250: drain canceled TX DMA before reusing its buffer James Hilliard
2026-10-23 22:57   ` sashiko-bot
2026-10-23 22:41 ` [PATCH v4 4/4] serial: 8250: retire canceled RX DMA before reusing the receive buffer James Hilliard
2026-10-23 23:01   ` sashiko-bot

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-183428@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=james.hilliard1@gmail.com \
    --cc=linux-serial@vger.kernel.org \
    --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