[PATCH v3] serial: 8250: fix shared IRQ startup race causing IRQ warning
From: Wang Zhaolong
Date: Wed Jul 08 2026 - 03:24:59 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. In that
setup, ttyS1 and ttyS3 share IRQ 3. A small userspace reproducer
synchronizes two threads before open(), waits for both open attempts, and
then closes both file descriptors. It 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 irq_chain_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
irq_chain_mutex, so the just-published irq_info cannot be observed by another
link attempt before it is unlinked again.
The lock used to only protect irq_lists hash walks, but it now also serializes
IRQ chain publication and the first request_irq() completion. Rename it to
irq_chain_mutex and document the locking requirement for
serial_get_or_create_irq_info() with __must_hold() and lockdep_assert_held().
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 v3:
- Rename hash_mutex to irq_chain_mutex now that it also serializes IRQ chain
publication and first request_irq() completion.
- Add __must_hold() and lockdep_assert_held() to document the locking
requirement for serial_get_or_create_irq_info().
- Verify again with the QEMU ttyS1/ttyS3 shared IRQ reproducer.
Changes in v2:
- Retitle the patch to describe the unbalanced IRQ enable warning.
- Move the code comment to the mutex acquisition site to document why the
lock must cover the first request_irq() completion.
- Drop the Assisted-by tag.
v2: https://lore.kernel.org/all/20260708031115.3757150-1-wangzhaolong@xxxxxxxxx/
v1: https://lore.kernel.org/all/20260527092052.2086342-1-wangzhaolong@xxxxxxxxx/
drivers/tty/serial/8250/8250_core.c | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/drivers/tty/serial/8250/8250_core.c b/drivers/tty/serial/8250/8250_core.c
index f49862d90eeb..41d87cdc69d7 100644
--- a/drivers/tty/serial/8250/8250_core.c
+++ b/drivers/tty/serial/8250/8250_core.c
@@ -48,11 +48,11 @@ struct irq_info {
struct list_head *head;
};
#define IRQ_HASH_BITS 5 /* Can be adjusted later */
static DEFINE_HASHTABLE(irq_lists, IRQ_HASH_BITS);
-static DEFINE_MUTEX(hash_mutex); /* Used to walk the hash */
+static DEFINE_MUTEX(irq_chain_mutex);
static bool skip_txen_test;
module_param(skip_txen_test, bool, 0644);
MODULE_PARM_DESC(skip_txen_test, "Skip checking for the TXEN bug at init time");
@@ -129,14 +129,15 @@ static void serial_do_unlink(struct irq_info *i, struct uart_8250_port *up)
* Either:
* - find the corresponding info in the hashtable and return it, or
* - allocate a new one, add it to the hashtable and return it.
*/
static struct irq_info *serial_get_or_create_irq_info(const struct uart_8250_port *up)
+ __must_hold(&irq_chain_mutex)
{
struct irq_info *i;
- guard(mutex)(&hash_mutex);
+ lockdep_assert_held(&irq_chain_mutex);
hash_for_each_possible(irq_lists, i, node, up->port.irq)
if (i->irq == up->port.irq)
return i;
@@ -154,10 +155,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 IRQ chain 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)(&irq_chain_mutex);
+
i = serial_get_or_create_irq_info(up);
if (IS_ERR(i))
return PTR_ERR(i);
scoped_guard(spinlock_irq, &i->lock) {
@@ -180,11 +189,11 @@ static int serial_link_irq_chain(struct uart_8250_port *up)
static void serial_unlink_irq_chain(struct uart_8250_port *up)
{
struct irq_info *i;
- guard(mutex)(&hash_mutex);
+ guard(mutex)(&irq_chain_mutex);
hash_for_each_possible(irq_lists, i, node, up->port.irq)
if (i->irq == up->port.irq) {
if (WARN_ON(i->head == NULL))
return;
--
2.54.0