[PATCH] printk: fold consecutive duplicate messages

From: 林濬哲

Date: Mon Sep 21 2026 - 01:04:11 EST


A kernel bug can flood the console with thousands of copies of the
same message, drowning out everything else. Fold consecutive
duplicates into a single "last message repeated N times" summary,
flushed when 10 repeats accumulate or a 1s window elapses.

The dedup key is built from facility, level and the format string
address; format parameters are not part of the key since va_list can
only be consumed once and hashing rendered text would put string
comparisons on the printk fast path.

Messages at LOGLEVEL_ERR and above are never folded, and dedup is
skipped after suppress_printk / panic take effect since the call site
sits behind those checks. The dedup state is protected by a raw
spinlock, and the summary is printed only after dropping the lock to
avoid recursive self-deadlock.

Disable with printk_dedup=0 on the kernel command line or via the
printk_dedup module parameter.

Known limitation: dedup_lock is not NMI safe; a message from NMI
context while another CPU holds the lock will spin.

Signed-off-by: 林濬哲 <m18667909625@xxxxxxx>
Assisted-by: AI coding assistant (disclosed per kernel AI guidelines)
---
kernel/printk/printk.c | 89 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 89 insertions(+)

diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index 6d3d18a50da7..0c93767d041f 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -20,6 +20,7 @@
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/kernel.h>
+#include <linux/hash.h>
#include <linux/mm.h>
#include <linux/tty.h>
#include <linux/tty_driver.h>
@@ -104,6 +105,91 @@ DEFINE_STATIC_SRCU(console_srcu);
*/
int __read_mostly suppress_printk;

+static u32 dedup_last_key;
+static u32 dedup_repeat;
+static bool dedup_active;
+static u64 dedup_window_start;
+#define PRINTK_DEDUP_WINDOW_NS 1000000000ULL /* 1s */
+static bool printk_dedup = true;
+module_param(printk_dedup, bool, 0644);
+MODULE_PARM_DESC(printk_dedup, "fold consecutive duplicate printk messages");
+/* protects the dedup state above; never held while printing */
+static DEFINE_RAW_SPINLOCK(dedup_lock);
+
+static int __init printk_dedup_setup(char *str)
+{
+ return kstrtobool(str, &printk_dedup);
+}
+early_param("printk_dedup", printk_dedup_setup);
+
+/*
+ * Detect consecutive duplicate printk messages and fold them away.
+ * Returns true if this message should be dropped.
+ *
+ * The dedup key is built from facility, level and the format string.
+ * Format parameters are intentionally not part of the key: va_list
+ * can only be consumed once, and hashing rendered text would put
+ * string comparisons on the printk fast path.
+ */
+static bool outputs_dedupe(int facility, int level, const char *fmt)
+{
+ unsigned long flags;
+ bool drop;
+ u64 now;
+ u32 key;
+ u32 n;
+
+ /* error and above must never be folded */
+ if (level <= LOGLEVEL_ERR || !printk_dedup)
+ return false;
+
+ key = hash_64((unsigned long)(fmt ? : ""), 32) ^
+ hash_32(facility ^ level, 32);
+ now = ktime_get_ns();
+
+ raw_spin_lock_irqsave(&dedup_lock, flags);
+
+ if (dedup_active && key == dedup_last_key) {
+ dedup_repeat++;
+ /*
+ * Force a flush when 10 repeats have accumulated or
+ * the 1s window has elapsed, so the summary shows up.
+ */
+ if (dedup_repeat >= 10 ||
+ now - dedup_window_start > PRINTK_DEDUP_WINDOW_NS) {
+ n = dedup_repeat;
+ dedup_repeat = 0;
+ dedup_window_start = now;
+ drop = false;
+ } else {
+ drop = true;
+ }
+ } else {
+ if (dedup_active && dedup_repeat) {
+ n = dedup_repeat;
+ drop = false;
+ } else {
+ n = 0;
+ drop = false;
+ }
+ dedup_last_key = key;
+ dedup_repeat = 0;
+ dedup_window_start = now;
+ dedup_active = true;
+ }
+
+ raw_spin_unlock_irqrestore(&dedup_lock, flags);
+
+ /*
+ * Print only after dropping the lock: pr_info() re-enters
+ * vprintk_emit() and would deadlock on dedup_lock otherwise.
+ */
+ if (n)
+ pr_info("last message repeated %u times\n", n);
+
+ return drop;
+}
+
#ifdef CONFIG_LOCKDEP
static struct lockdep_map console_lock_dep_map = {
.name = "console_lock"
@@ -2441,6 +2527,9 @@ asmlinkage int vprintk_emit(int facility, int level,
!panic_triggering_all_cpu_backtrace)
return 0;

+ if (outputs_dedupe(facility, level, fmt))
+ return 0;
+
printk_get_console_flush_type(&ft);

/* If called from the scheduler, we can not call up(). */
--
2.53.0