[PATCH v2] serial: 8250: fix shared IRQ startup race causing IRQ warning

From: Wang Zhaolong

Date: Tue Jul 07 2026 - 23:14:34 EST


Concurrent startup of two 8250 ports sharing the same IRQ can trigger an
IRQ core warning:

Unbalanced enable for IRQ 3
WARNING: CPU: 0 PID: 580 at kernel/irq/manage.c:774 __enable_irq+0x3b/0x60
Call Trace:
enable_irq+0x8d/0x120
serial8250_do_startup+0x80d/0xa80
uart_port_startup+0x13d/0x440
uart_port_activate+0x5b/0xb0
tty_port_open+0xa1/0x120
uart_open+0x1e/0x30
tty_open+0x140/0x7a0

This is reproducible in QEMU with four legacy 8250/16550 ports where ttyS1
and ttyS3 share IRQ 3. A small userspace reproducer that synchronizes two
threads before open(), waits for both open attempts, and then closes both file
descriptors can trigger the warning almost immediately.

The regression was bisected to commit 64c79dfbc458 ("serial: 8250_pnp:
Support configurable reg shift property"). That change made QEMU's legacy
PNP serial ports take the shared-IRQ THRE test path in
serial8250_do_startup():

if (port->irqflags & IRQF_SHARED)
disable_irq_nosync(port->irq)
...
if (port->irqflags & IRQF_SHARED)
enable_irq(port->irq)

The disable_irq_nosync()/enable_irq() pair is locally balanced, but it can
race with the IRQ core startup path for the first 8250 port on the same IRQ.
One possible interleaving is:

CPU0, ttyS1 CPU1, ttyS3

serial_link_irq_chain()
hash_add(i)
i->head = &ttyS1
request_irq()
serial_link_irq_chain()
find i in irq_lists
list_add(&ttyS3, i->head)
serial8250_do_startup()
disable_irq_nosync(irq)
irq_startup()
desc->depth = 0
enable_irq(irq)
WARN: Unbalanced enable for IRQ 3

Hold hash_mutex in serial_link_irq_chain() until the first request_irq() has
completed. This prevents another 8250 port sharing the IRQ from joining the
chain and running the THRE test while the IRQ core is still starting the
interrupt. The request_irq() failure cleanup also remains covered by
hash_mutex, so the just-published irq_info cannot be observed by another link
attempt before it is unlinked again.

Fixes: 64c79dfbc458 ("serial: 8250_pnp: Support configurable reg shift property")
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221579
Cc: stable@xxxxxxxxxxxxxxx # 6.10+
Signed-off-by: Wang Zhaolong <wangzhaolong@xxxxxxxxx>
---

Changes in v2:
- Retitle the patch to describe the unbalanced IRQ enable warning.
- Move the code comment to the hash_mutex acquisition site to document why the
lock must cover the first request_irq() completion.
- Drop the Assisted-by tag.

drivers/tty/serial/8250/8250_core.c | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/drivers/tty/serial/8250/8250_core.c b/drivers/tty/serial/8250/8250_core.c
index a428e88938eb..dd202032cc7c 100644
--- a/drivers/tty/serial/8250/8250_core.c
+++ b/drivers/tty/serial/8250/8250_core.c
@@ -132,12 +132,10 @@ static void serial_do_unlink(struct irq_info *i, struct uart_8250_port *up)
*/
static struct irq_info *serial_get_or_create_irq_info(const struct uart_8250_port *up)
{
struct irq_info *i;

- guard(mutex)(&hash_mutex);
-
hash_for_each_possible(irq_lists, i, node, up->port.irq)
if (i->irq == up->port.irq)
return i;

i = kzalloc_obj(*i);
@@ -154,10 +152,18 @@ static struct irq_info *serial_get_or_create_irq_info(const struct uart_8250_por
static int serial_link_irq_chain(struct uart_8250_port *up)
{
struct irq_info *i;
int ret;

+ /*
+ * Keep the hash lock held until the first request_irq() completes.
+ * The first port publishes i->head before request_irq() starts the IRQ;
+ * a second port sharing the IRQ must not join the chain and run the
+ * THRE test while the IRQ core is still bringing the line up.
+ */
+ guard(mutex)(&hash_mutex);
+
i = serial_get_or_create_irq_info(up);
if (IS_ERR(i))
return PTR_ERR(i);

scoped_guard(spinlock_irq, &i->lock) {
--
2.54.0