Re: [Kernel Bug] INFO: task hung in show_cons_active

From: Longxing Li

Date: Tue Jun 30 2026 - 08:31:35 EST


Dear maintainers,

Here's my fix and patch of the bug.

Fix Summary:

The deadlock occurs because unregister_console() holds console_list_lock
while __pr_flush() blocks on console_sem. show_cons_active() has the same
lock -> sem ordering (holds mutex, waits for sem), creating a potential
ABBA when any printer path needs the mutex.

Fix: restructure unregister_console() into three phases:

Phase 1 (list lock): Disable console (clear CON_ENABLED).
Phase 2 (NO lock): __pr_flush() waits for pending output.
Safe because console is already disabled.
Phase 3 (list lock): Remove from list + cleanup.

This ensures __pr_flush() is never called while holding console_list_lock,
eliminating the lock inversion.

Full patch is as follows and also attached in the link.

=====================================================
From: Longxing Li <coregee2000@xxxxxxxxx>
Date: Tue, 30 Jun 2026
Subject: [PATCH] printk: fix potential deadlock in unregister_console()

unregister_console() holds console_list_lock while calling __pr_flush(),
which blocks on console_sem. show_cons_active() holds console_mutex (the
same lock) while also waiting for console_sem. This creates a potential
ABBA deadlock when any console output path needs console_mutex.

Fix by restructuring unregister_console() so __pr_flush() is called WITHOUT
holding console_list_lock:

Phase 1 (list lock): Disable console (clear CON_ENABLED).
Phase 2 (no lock): __pr_flush() drains pending output.
Phase 3 (list lock): Remove from list + cleanup.

Reported-by: Longxing Li
Suggested-by: Greg KH <gregkh@xxxxxxxxxxxxxxxxxxx>
Signed-off-by: Longxing Li <coregee2000@xxxxxxxxx>
---
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -4235,28 +4235,15 @@
struct console *c;
int res;

lockdep_assert_console_list_lock_held();

- con_printk(KERN_INFO, console, "disabled\n");
-
- res = _braille_unregister_console(console);
- if (res < 0)
- return res;
- if (res > 0)
- return 0;
-
- if (!console_is_registered_locked(console))
- res = -ENODEV;
- else if (console_is_usable(console, console->flags, true))
- __pr_flush(console, 1000, true);
-
- /* Disable it unconditionally */
- console_srcu_write_flags(console, console->flags & ~CON_ENABLED);
-
- if (res < 0)
- return res;
+/*
+ * NOTE: The disable phase has been moved to
+ * __unregister_console_disable(). This function
+ * only handles list removal and cleanup (Phase 3).
+ */

/*
* Use the driver synchronization to ensure that the hardware is not
* in use while this console transitions to being unregistered.
*/
@@ -4320,17 +4307,77 @@
printk_kthreads_check_locked();

return res;
}

+
+/*
+ * __unregister_console_disable - Phase 1 of unregistration
+ * @console: console to disable
+ *
+ * Marks the console as disabled and prints the "disabled" message,
+ * both under console_list_lock().
+ *
+ * NOTE: This function does NOT call __pr_flush(). __pr_flush() blocks
+ * on console_sem. show_cons_active() holds console_mutex
+ * (== console_list_lock) while waiting for console_sem.
+ *
+ * Holding console_list_lock while blocking for console_sem creates a
+ * classic ABBA deadlock. The flush is deferred to Phase 2.
+ *
+ * Returns 0 on success, negative errno on error (-ENODEV).
+ */
+static int __unregister_console_disable(struct console *console)
+{
+ int brl_res;
+
+ lockdep_assert_console_list_lock_held();
+
+ if (!console_is_registered_locked(console))
+ return -ENODEV;
+
+ con_printk(KERN_INFO, console, "disabled\n");
+
+ brl_res = _braille_unregister_console(console);
+ if (brl_res < 0)
+ return brl_res;
+ if (brl_res > 0)
+ return 0;
+
+ console_srcu_write_flags(console, console->flags & ~CON_ENABLED);
+
+ return 0;
+}
int unregister_console(struct console *console)
{
int res;

+ /* Phase 1: disable console while holding list lock */
+ console_list_lock();
+ res = __unregister_console_disable(console);
+ console_list_unlock();
+ if (res)
+ return res;
+
+ /*
+ * Phase 2: flush WITHOUT holding console_list_lock.
+ *
+ * This is the key fix of the deadlock.
+ * __pr_flush() blocks on console_sem.
+ * Holding console_list_lock while blocking for
+ * console_sem creates an ABBA with show_cons_active().
+ *
+ * The console is already disabled in Phase 1 (CON_ENABLED=0),
+ * so no new printk output will be sent to it.
+ */
+ __pr_flush(console, 1000, true);
+
+ /* Phase 3: final removal and cleanup under list lock */
console_list_lock();
res = unregister_console_locked(console);
console_list_unlock();
+
return res;
}
EXPORT_SYMBOL(unregister_console);

/**

=====================================================
https://drive.google.com/file/d/1PeNlaVVXwhgyc7ZU9CaQeUwOGbshOpc_/view?usp=drive_link


Greg KH <gregkh@xxxxxxxxxxxxxxxxxxx> 于2026年6月22日周一 22:02写道:
>
> On Mon, Jun 22, 2026 at 09:53:02PM +0800, Longxing Li wrote:
> > Dear Linux kernel developers and maintainers,
> >
> > Thank you for the feedback. I've added some analysis on this bug, and
> > also added the original report, which is at the bottom of this email.
> >
> > Analysis Summary:
> >
> > The hang involves two tasks blocking on console_sem while holding
> > console_mutex, and a third task path (unregister_console) that
> > acquires console_mutex and then blocks on console_sem via
> > __pr_flush(). The core problem is a lock inversion: __pr_flush()
> > blocks on console_sem while holding console_list_lock (->
> > console_mutex).
> >
> > This can lead to a deadlock when any code path holds console_sem and
> > subsequently needs console_mutex.
>
> Great, can you create patches for this issue as you seem to be able to
> reproduce the issue well? That way you get credit for the fix and we
> can understand the proposal here much better.
>
> thanks,
>
> greg k-h
From: Longxing Li <coregee2000@xxxxxxxxx>
Date: Tue, 30 Jun 2026
Subject: [PATCH] printk: fix potential deadlock in unregister_console()

unregister_console() holds console_list_lock while calling __pr_flush(),
which blocks on console_sem. show_cons_active() holds console_mutex (the
same lock) while also waiting for console_sem. This creates a potential
ABBA deadlock when any console output path needs console_mutex.

Fix by restructuring unregister_console() so __pr_flush() is called WITHOUT
holding console_list_lock:

Phase 1 (list lock): Disable console (clear CON_ENABLED).
Phase 2 (no lock): __pr_flush() drains pending output.
Phase 3 (list lock): Remove from list + cleanup.

Reported-by: Longxing Li
Suggested-by: Greg KH <gregkh@xxxxxxxxxxxxxxxxxxx>
Signed-off-by: Longxing Li <coregee2000@xxxxxxxxx>
---
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -4235,28 +4235,15 @@
struct console *c;
int res;

lockdep_assert_console_list_lock_held();

- con_printk(KERN_INFO, console, "disabled\n");
-
- res = _braille_unregister_console(console);
- if (res < 0)
- return res;
- if (res > 0)
- return 0;
-
- if (!console_is_registered_locked(console))
- res = -ENODEV;
- else if (console_is_usable(console, console->flags, true))
- __pr_flush(console, 1000, true);
-
- /* Disable it unconditionally */
- console_srcu_write_flags(console, console->flags & ~CON_ENABLED);
-
- if (res < 0)
- return res;
+/*
+ * NOTE: The disable phase has been moved to
+ * __unregister_console_disable(). This function
+ * only handles list removal and cleanup (Phase 3).
+ */

/*
* Use the driver synchronization to ensure that the hardware is not
* in use while this console transitions to being unregistered.
*/
@@ -4320,17 +4307,77 @@
printk_kthreads_check_locked();

return res;
}

+
+/*
+ * __unregister_console_disable - Phase 1 of unregistration
+ * @console: console to disable
+ *
+ * Marks the console as disabled and prints the "disabled" message,
+ * both under console_list_lock().
+ *
+ * NOTE: This function does NOT call __pr_flush(). __pr_flush() blocks
+ * on console_sem. show_cons_active() holds console_mutex
+ * (== console_list_lock) while waiting for console_sem.
+ *
+ * Holding console_list_lock while blocking for console_sem creates a
+ * classic ABBA deadlock. The flush is deferred to Phase 2.
+ *
+ * Returns 0 on success, negative errno on error (-ENODEV).
+ */
+static int __unregister_console_disable(struct console *console)
+{
+ int brl_res;
+
+ lockdep_assert_console_list_lock_held();
+
+ if (!console_is_registered_locked(console))
+ return -ENODEV;
+
+ con_printk(KERN_INFO, console, "disabled\n");
+
+ brl_res = _braille_unregister_console(console);
+ if (brl_res < 0)
+ return brl_res;
+ if (brl_res > 0)
+ return 0;
+
+ console_srcu_write_flags(console, console->flags & ~CON_ENABLED);
+
+ return 0;
+}
int unregister_console(struct console *console)
{
int res;

+ /* Phase 1: disable console while holding list lock */
+ console_list_lock();
+ res = __unregister_console_disable(console);
+ console_list_unlock();
+ if (res)
+ return res;
+
+ /*
+ * Phase 2: flush WITHOUT holding console_list_lock.
+ *
+ * This is the key fix of the deadlock.
+ * __pr_flush() blocks on console_sem.
+ * Holding console_list_lock while blocking for
+ * console_sem creates an ABBA with show_cons_active().
+ *
+ * The console is already disabled in Phase 1 (CON_ENABLED=0),
+ * so no new printk output will be sent to it.
+ */
+ __pr_flush(console, 1000, true);
+
+ /* Phase 3: final removal and cleanup under list lock */
console_list_lock();
res = unregister_console_locked(console);
console_list_unlock();
+
return res;
}
EXPORT_SYMBOL(unregister_console);

/**