[PATCH v8] serial: 8250: fix use-after-free in IRQ chain handling

From: Jing Wu

Date: Wed Jul 08 2026 - 05:18:26 EST


serial_unlink_irq_chain() holds irq_chain_mutex and calls free_irq() +
kfree(i) on an empty port list. serial_link_irq_chain() used to release
irq_chain_mutex after serial_get_or_create_irq_info() but before acquiring
i->lock, so a concurrent unlink could see list_empty() as true while a
port was being added, free i, and cause a use-after-free.

Dropping irq_chain_mutex before request_irq() completes also lets another
port on the same IRQ join the chain and run the THRE test while IRQ
startup is still in progress, triggering "Unbalanced enable for IRQ"
because irq_shutdown() in the premature free_irq() path increments
desc->depth and breaks the disable_irq/enable_irq pairing in
serial8250_THRE_test().

Hold irq_chain_mutex across the whole first request_irq() completion
(including the error cleanup path) by taking it at the top of
serial_link_irq_chain() via guard(mutex)(&irq_chain_mutex).
serial_unlink_irq_chain() already holds irq_chain_mutex throughout, so
the race is closed.

With irq_chain_mutex now taken by the caller, replace the guard in
serial_get_or_create_irq_info() with lockdep_assert_held() and add
__must_hold(&irq_chain_mutex) for static analysis. Rename hash_mutex to
irq_chain_mutex to reflect its broader role beyond hash table locking.

Fixes: 768aec0b5bcc ("serial: 8250: fix shared interrupts issues with SMP and RT kernels")
Reported-by: Wang Zhaolong <wangzhaolong@xxxxxxxxx>
Tested-by: Wang Zhaolong <wangzhaolong@xxxxxxxxx>
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221579
Co-developed-by: Qiliang Yuan <yuanql9@xxxxxxxxxxxxxxx>
Signed-off-by: Qiliang Yuan <yuanql9@xxxxxxxxxxxxxxx>
Signed-off-by: Jing Wu <realwujing@xxxxxxxxx>
---
V7 -> V8:
- Remove Co-developed-by: Wang Zhaolong and his Signed-off-by. Wang
reported the bug and confirmed our v3 fixed his reproducer, which
warrants Tested-by, not co-developer credit.
- Add Tested-by: Wang Zhaolong.

V6 -> V7:
- Rename hash_mutex to irq_chain_mutex to reflect its broader role
beyond hash table locking.
- Drop unrelated blank-line and return-value changes.
- Add Co-developed-by: Qiliang Yuan.

V5 -> V6:
- Keep guard(mutex) and scoped_guard(spinlock_irq) style instead of
manual locking; the lock lifecycle fits the scope-based model.

V4 -> V5:
- Add __must_hold(&hash_mutex) annotation to
serial_get_or_create_irq_info() for static analysis.

V3 -> V4:
- Move cleanup under hash_mutex on request_irq() failure to prevent a
second port from joining the chain before the irq_info is cleaned up.
- Fix inaccurate description of irq_shutdown() in commit message.

V2 -> V3:
- Hold hash_mutex across the first request_irq() completion to prevent a
second port from joining the chain and running the shared-IRQ THRE test
while IRQ startup is still in progress.

V1 -> V2:
- Add Reported-by tag from Wang Zhaolong.

v7: https://lore.kernel.org/r/20260708-bug-221579-8250-shared-irq-race-v7-1-0da8d89f6072@xxxxxxxxx
v6: https://lore.kernel.org/r/20260707-bug-221579-8250-shared-irq-race-v6-1-f8c499a90bdd@xxxxxxxxx
v5: https://lore.kernel.org/r/20260624-bug-221579-8250-shared-irq-race-v5-1-15d841f89e1e@xxxxxxxxx
v4: https://lore.kernel.org/r/20260529-bug-221579-8250-shared-irq-race-v4-1-cfda63b4420f@xxxxxxxxx
v3: https://lore.kernel.org/r/20260529-bug-221579-8250-shared-irq-race-v3-1-fe4d430862a9@xxxxxxxxx
v2: https://lore.kernel.org/r/20260528-bug-221579-8250-shared-irq-race-v2-1-06531202e54d@xxxxxxxxx
v1: https://lore.kernel.org/r/20260528-bug-221579-8250-shared-irq-race-v1-1-30980cca02f3@xxxxxxxxx
---
drivers/tty/serial/8250/8250_core.c | 30 +++++++++++++++++++++++++-----
1 file changed, 25 insertions(+), 5 deletions(-)

diff --git a/drivers/tty/serial/8250/8250_core.c b/drivers/tty/serial/8250/8250_core.c
index a428e88938eb7..7e6bbac094091 100644
--- a/drivers/tty/serial/8250/8250_core.c
+++ b/drivers/tty/serial/8250/8250_core.c
@@ -50,7 +50,7 @@ struct irq_info {

#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); /* Serializes link/unlink on the IRQ chains */

static bool skip_txen_test;
module_param(skip_txen_test, bool, 0644);
@@ -131,10 +131,11 @@ static void serial_do_unlink(struct irq_info *i, struct uart_8250_port *up)
* - 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)
@@ -151,15 +152,34 @@ static struct irq_info *serial_get_or_create_irq_info(const struct uart_8250_por
return i;
}

+/*
+ * serial_link_irq_chain() hooks the given 8250 port into the IRQ chain.
+ *
+ * irq_chain_mutex must be held from the hash lookup through the first
+ * request_irq() completion. Dropping it earlier allows a concurrent
+ * serial_unlink_irq_chain() to race in after i->head is published but
+ * before the IRQ is fully set up — another port sharing the IRQ can then
+ * join the chain and run the shared-IRQ THRE test while IRQ startup is
+ * still in progress, triggering an "Unbalanced enable for IRQ" warning
+ * in kernel/irq/manage.c.
+ */
static int serial_link_irq_chain(struct uart_8250_port *up)
{
struct irq_info *i;
int ret;

+ guard(mutex)(&irq_chain_mutex);
+
i = serial_get_or_create_irq_info(up);
if (IS_ERR(i))
return PTR_ERR(i);

+ /*
+ * Serialise against the list manipulation in the interrupt handler
+ * and in serial_unlink_irq_chain(). irq_chain_mutex is still held
+ * which prevents serial_unlink_irq_chain() from entering and freeing
+ * the irq_info until the first request_irq() completes.
+ */
scoped_guard(spinlock_irq, &i->lock) {
if (i->head) {
list_add(&up->list, i->head);
@@ -171,7 +191,8 @@ static int serial_link_irq_chain(struct uart_8250_port *up)
i->head = &up->list;
}

- ret = request_irq(up->port.irq, serial8250_interrupt, up->port.irqflags, up->port.name, i);
+ ret = request_irq(up->port.irq, serial8250_interrupt,
+ up->port.irqflags, up->port.name, i);
if (ret < 0)
serial_do_unlink(i, up);

@@ -182,8 +203,7 @@ 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))

---
base-commit: eb3f4b7426cfd2b79d65b7d37155480b32259a11
change-id: 20260528-bug-221579-8250-shared-irq-race-581e4900a178

Best regards,
--
Jing Wu <realwujing@xxxxxxxxx>