[PATCH] serial: 8250: fix use-after-free in IRQ chain handling
From: Qiliang Yuan
Date: Thu May 28 2026 - 11:26:53 EST
serial_unlink_irq_chain() holds hash_mutex and calls free_irq() + kfree(i)
when it sees an empty port list. serial_link_irq_chain() released
hash_mutex after serial_get_or_create_irq_info() but before acquiring
i->lock. This gap allowed a concurrent unlink to observe list_empty()
as true while a new port was still being added, free i, and trigger a
use-after-free.
The corrupted list/irq_info then causes an "Unbalanced enable for IRQ"
warning (kernel/irq/manage.c:774) because irq_shutdown() in the premature
free_irq() path hard-sets desc->depth to 1, breaking the disable_irq/
enable_irq pairing in serial8250_THRE_test().
Fix by pulling hash_mutex into serial_link_irq_chain() so that it covers
the i->head check and the list_add/INIT_LIST_HEAD under i->lock.
serial_unlink_irq_chain() already holds hash_mutex throughout, so the
race window is closed.
Closes: https://bugzilla.kernel.org/show_bug.cgi?id=221579
Fixes: 768aec0b5bcc ("serial: 8250: fix shared interrupts issues with SMP and RT kernels")
Signed-off-by: Qiliang Yuan <realwujing@xxxxxxxxx>
---
drivers/tty/serial/8250/8250_core.c | 46 ++++++++++++++++++++++++++++---------
1 file changed, 35 insertions(+), 11 deletions(-)
diff --git a/drivers/tty/serial/8250/8250_core.c b/drivers/tty/serial/8250/8250_core.c
index a428e88938eb7..dfe76223ce10c 100644
--- a/drivers/tty/serial/8250/8250_core.c
+++ b/drivers/tty/serial/8250/8250_core.c
@@ -134,7 +134,7 @@ static struct irq_info *serial_get_or_create_irq_info(const struct uart_8250_por
{
struct irq_info *i;
- guard(mutex)(&hash_mutex);
+ lockdep_assert_held(&hash_mutex);
hash_for_each_possible(irq_lists, i, node, up->port.irq)
if (i->irq == up->port.irq)
@@ -151,27 +151,51 @@ 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.
+ *
+ * hash_mutex must be held across checking i->head and adding the port to
+ * the list. Without this, a concurrent serial_unlink_irq_chain() can race
+ * in after hash_mutex is dropped but before i->lock is acquired, observe
+ * list_empty(i->head) as true, call free_irq() and kfree(i) — triggering a
+ * use-after-free and ultimately 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;
+ mutex_lock(&hash_mutex);
+
i = serial_get_or_create_irq_info(up);
- if (IS_ERR(i))
+ if (IS_ERR(i)) {
+ mutex_unlock(&hash_mutex);
return PTR_ERR(i);
+ }
- scoped_guard(spinlock_irq, &i->lock) {
- if (i->head) {
- list_add(&up->list, i->head);
-
- return 0;
- }
+ /*
+ * Serialise against the list manipulation in the interrupt handler
+ * and in serial_unlink_irq_chain(). hash_mutex is still held which
+ * prevents serial_unlink_irq_chain() from running concurrently.
+ */
+ spin_lock_irq(&i->lock);
+ if (i->head) {
+ list_add(&up->list, i->head);
+ spin_unlock_irq(&i->lock);
+ mutex_unlock(&hash_mutex);
- INIT_LIST_HEAD(&up->list);
- i->head = &up->list;
+ return 0;
}
- ret = request_irq(up->port.irq, serial8250_interrupt, up->port.irqflags, up->port.name, i);
+ INIT_LIST_HEAD(&up->list);
+ i->head = &up->list;
+ spin_unlock_irq(&i->lock);
+
+ mutex_unlock(&hash_mutex);
+
+ ret = request_irq(up->port.irq, serial8250_interrupt,
+ up->port.irqflags, up->port.name, i);
if (ret < 0)
serial_do_unlink(i, up);
---
base-commit: eb3f4b7426cfd2b79d65b7d37155480b32259a11
change-id: 20260528-bug-221579-8250-shared-irq-race-581e4900a178
Best regards,
--
Qiliang Yuan <realwujing@xxxxxxxxx>