On Thu, May 14, 2026 at 1:07 PM Albert Esteve <[email protected]> wrote:
>
> From: Alessandro Carminati <[email protected]>
>
> Some unit tests intentionally trigger warning backtraces by passing bad
> parameters to kernel API functions. Such unit tests typically check the
> return value from such calls, not the existence of the warning backtrace.
>
> Such intentionally generated warning backtraces are neither desirable
> nor useful for a number of reasons:
> - They can result in overlooked real problems.
> - A warning that suddenly starts to show up in unit tests needs to be
> investigated and has to be marked to be ignored, for example by
> adjusting filter scripts. Such filters are ad hoc because there is
> no real standard format for warnings. On top of that, such filter
> scripts would require constant maintenance.
>
> Solve the problem by providing a means to suppress warning backtraces
> originating from the current kthread while executing test code. Since
> each KUnit test runs in its own kthread, this effectively scopes
> suppression to the test that enabled it. Limit changes to generic code
> to the absolute minimum.
>
> Implementation details:
> Suppression is integrated into the existing KUnit hooks infrastructure
> in test-bug.h, reusing the kunit_running static branch for zero
> overhead when no tests are running.
>
> Suppression is checked at three points in the warning path:
> - In warn_slowpath_fmt(), the check runs before any output, fully
> suppressing both message and backtrace. This covers architectures
> without __WARN_FLAGS.
> - In __warn_printk(), the check suppresses the warning message text.
> This covers architectures that define __WARN_FLAGS but not their own
> __WARN_printf (arm64, loongarch, parisc, powerpc, riscv, sh), where
> the message is printed before the trap enters __report_bug().
> - In __report_bug(), the check runs before __warn() is called,
> suppressing the backtrace and stack dump.
>
> To avoid double-counting on architectures where both __warn_printk()
> and __report_bug() run for the same warning, kunit_is_suppressed_warning()
> takes a bool parameter: true to increment the suppression counter
> (used in warn_slowpath_fmt and __report_bug), false to check only
> (used in __warn_printk).
>
> The suppression state is dynamically allocated via kunit_kzalloc() and
> tied to the KUnit test lifecycle via kunit_add_action(), ensuring
> automatic cleanup at test exit. Writer-side access to the global
> suppression list is serialized with a spinlock; readers use RCU.
>
> Two API forms are provided:
> - kunit_warning_suppress(test) { ... }: scoped, uses __cleanup for
> automatic teardown on scope exit, kunit_add_action() as safety net
> for abnormal exits (e.g. kthread_exit from failed assertions).
> Suppression handle is only accessible inside the block.
> - kunit_start/end_suppress_warning(test): direct functions returning
> an explicit handle, for retaining the handle within the test,
> or for cross-function usage.
>
> Signed-off-by: Guenter Roeck <[email protected]>
> Signed-off-by: Alessandro Carminati <[email protected]>
> Reviewed-by: Kees Cook <[email protected]>
> Reviewed-by: David Gow <[email protected]>
> Signed-off-by: Albert Esteve <[email protected]>
> ---
> include/kunit/test-bug.h | 26 ++++++++++
> include/kunit/test.h | 98 ++++++++++++++++++++++++++++++++++++++
> kernel/panic.c | 11 +++++
> lib/bug.c | 14 +++++-
> lib/kunit/Makefile | 3 +-
> lib/kunit/bug.c | 120
> +++++++++++++++++++++++++++++++++++++++++++++++
> lib/kunit/hooks-impl.h | 2 +
> 7 files changed, 271 insertions(+), 3 deletions(-)
>
> diff --git a/include/kunit/test-bug.h b/include/kunit/test-bug.h
> index 47aa8f21ccce8..99869029fc686 100644
> --- a/include/kunit/test-bug.h
> +++ b/include/kunit/test-bug.h
> @@ -10,6 +10,7 @@
> #define _KUNIT_TEST_BUG_H
>
> #include <linux/stddef.h> /* for NULL */
> +#include <linux/types.h> /* for bool */
>
> #if IS_ENABLED(CONFIG_KUNIT)
>
> @@ -23,6 +24,7 @@ DECLARE_STATIC_KEY_FALSE(kunit_running);
> extern struct kunit_hooks_table {
> __printf(3, 4) void (*fail_current_test)(const char*, int, const
> char*, ...);
> void *(*get_static_stub_address)(struct kunit *test, void
> *real_fn_addr);
> + bool (*is_suppressed_warning)(bool count);
> } kunit_hooks;
>
> /**
> @@ -60,9 +62,33 @@ static inline struct kunit *kunit_get_current_test(void)
> }
> \
> } while (0)
>
> +/**
> + * kunit_is_suppressed_warning() - Check if warnings are being suppressed
> + * by the current KUnit test.
> + * @count: if true, increment the suppression counter on match.
> + *
> + * Returns true if the current task has active warning suppression.
> + * Uses the kunit_running static branch for zero overhead when no tests run.
> + *
> + * A single WARN*() may traverse multiple call sites in the warning path
> + * (e.g., __warn_printk() and __report_bug()). Pass @count = true at the
> + * primary suppression point to count each warning exactly once, and
> + * @count = false at secondary points to suppress output without
> + * inflating the count.
> + */
> +static inline bool kunit_is_suppressed_warning(bool count)
> +{
> + if (!static_branch_unlikely(&kunit_running))
> + return false;
> +
> + return kunit_hooks.is_suppressed_warning &&
> + kunit_hooks.is_suppressed_warning(count);
> +}
> +
> #else
>
> static inline struct kunit *kunit_get_current_test(void) { return NULL; }
> +static inline bool kunit_is_suppressed_warning(bool count) { return false; }
>
> #define kunit_fail_current_test(fmt, ...) do {} while (0)
>
> diff --git a/include/kunit/test.h b/include/kunit/test.h
> index 9cd1594ab697d..be71612f61655 100644
> --- a/include/kunit/test.h
> +++ b/include/kunit/test.h
> @@ -1795,4 +1795,102 @@ do {
> \
> // include resource.h themselves if they need it.
> #include <kunit/resource.h>
>
> +/*
> + * Warning backtrace suppression API.
> + *
> + * Suppresses WARN*() backtraces on the current task while active. Two forms
> + * are provided:
> + *
> + * - Scoped: kunit_warning_suppress(test) { ... }
> + * Suppression is active for the duration of the block. On normal exit,
> + * the for-loop increment deactivates suppression. On early exit (break,
> + * return, goto), the __cleanup attribute fires. On kthread_exit() (e.g.,
> + * a failed KUnit assertion), kunit_add_action() cleans up at test
> + * teardown. The suppression handle is only accessible inside the block,
> + * so warning counts must be checked before the block exits.
> + *
> + * - Direct: kunit_start_suppress_warning() / kunit_end_suppress_warning()
> + * The underlying functions, returning an explicit handle pointer. Use
> + * when the handle needs to be retained (e.g., for post-suppression
> + * count checks) or passed across helper functions.
> + */
> +struct kunit_suppressed_warning;
> +
> +struct kunit_suppressed_warning *
> +kunit_start_suppress_warning(struct kunit *test);
> +void kunit_end_suppress_warning(struct kunit *test,
> + struct kunit_suppressed_warning *w);
> +int kunit_suppressed_warning_count(struct kunit_suppressed_warning *w);
> +void __kunit_suppress_auto_cleanup(struct kunit_suppressed_warning **wp);
> +bool kunit_has_active_suppress_warning(void);
> +
> +/**
> + * kunit_warning_suppress() - Suppress WARN*() backtraces for the duration
> + * of a block.
> + * @test: The test context object.
> + *
> + * Scoped form of the suppression API. Suppression starts when the block is
> + * entered and ends automatically when the block exits through any path. See
> + * the section comment above for the cleanup guarantees on each exit path.
> + * Fails the test if suppression is already active; nesting is not supported.
> + *
> + * The warning count can be checked inside the block via
> + * KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(). The handle is not accessible
> + * after the block exits.
> + *
> + * Example::
> + *
> + * kunit_warning_suppress(test) {
> + * trigger_warning();
> + * KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1);
> + * }
> + */
> +#define kunit_warning_suppress(test) \
> + for (struct kunit_suppressed_warning *__kunit_suppress \
> + __cleanup(__kunit_suppress_auto_cleanup) = \
> + kunit_start_suppress_warning(test); \
> + __kunit_suppress; \
> + kunit_end_suppress_warning(test, __kunit_suppress), \
> + __kunit_suppress = NULL)
> +
> +/**
> + * KUNIT_SUPPRESSED_WARNING_COUNT() - Returns the suppressed warning count.
> + *
> + * Returns the number of WARN*() calls suppressed since the current
> + * suppression block started, or 0 if the handle is NULL. Usable inside a
> + * kunit_warning_suppress() block.
> + */
> +#define KUNIT_SUPPRESSED_WARNING_COUNT() \
> + kunit_suppressed_warning_count(__kunit_suppress)
> +
> +/**
> + * KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT() - Sets an expectation that the
> + * suppressed warning count equals
> + * @expected.
> + * @test: The test context object.
> + * @expected: an expression that evaluates to the expected warning count.
> + *
> + * Sets an expectation that the number of suppressed WARN*() calls equals
> + * @expected. This is semantically equivalent to
> + * KUNIT_EXPECT_EQ(@test, KUNIT_SUPPRESSED_WARNING_COUNT(), @expected).
> + * See KUNIT_EXPECT_EQ() for more information.
> + */
> +#define KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, expected) \
> + KUNIT_EXPECT_EQ(test, KUNIT_SUPPRESSED_WARNING_COUNT(), expected)
> +
> +/**
> + * KUNIT_ASSERT_SUPPRESSED_WARNING_COUNT() - Sets an assertion that the
> + * suppressed warning count equals
> + * @expected.
> + * @test: The test context object.
> + * @expected: an expression that evaluates to the expected warning count.
> + *
> + * Sets an assertion that the number of suppressed WARN*() calls equals
> + * @expected. This is the same as KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(),
> + * except it causes an assertion failure (see KUNIT_ASSERT_TRUE()) when the
> + * assertion is not met.
> + */
> +#define KUNIT_ASSERT_SUPPRESSED_WARNING_COUNT(test, expected) \
> + KUNIT_ASSERT_EQ(test, KUNIT_SUPPRESSED_WARNING_COUNT(), expected)
> +
> #endif /* _KUNIT_TEST_H */
> diff --git a/kernel/panic.c b/kernel/panic.c
> index 20feada5319d4..213725b612aa1 100644
> --- a/kernel/panic.c
> +++ b/kernel/panic.c
> @@ -39,6 +39,7 @@
> #include <linux/sys_info.h>
> #include <trace/events/error_report.h>
> #include <asm/sections.h>
> +#include <kunit/test-bug.h>
>
> #define PANIC_TIMER_STEP 100
> #define PANIC_BLINK_SPD 18
> @@ -1124,6 +1125,11 @@ void warn_slowpath_fmt(const char *file, int line,
> unsigned taint,
> bool rcu = warn_rcu_enter();
> struct warn_args args;
>
> + if (kunit_is_suppressed_warning(true)) {
> + warn_rcu_exit(rcu);
> + return;
> + }
> +
> pr_warn(CUT_HERE);
>
> if (!fmt) {
> @@ -1146,6 +1152,11 @@ void __warn_printk(const char *fmt, ...)
> bool rcu = warn_rcu_enter();
> va_list args;
>
> + if (kunit_is_suppressed_warning(false)) {
> + warn_rcu_exit(rcu);
> + return;
> + }
> +
> pr_warn(CUT_HERE);
>
> va_start(args, fmt);
> diff --git a/lib/bug.c b/lib/bug.c
> index 224f4cfa4aa31..d99e369bc1103 100644
> --- a/lib/bug.c
> +++ b/lib/bug.c
> @@ -48,6 +48,7 @@
> #include <linux/rculist.h>
> #include <linux/ftrace.h>
> #include <linux/context_tracking.h>
> +#include <kunit/test-bug.h>
>
> extern struct bug_entry __start___bug_table[], __stop___bug_table[];
>
> @@ -209,8 +210,6 @@ static enum bug_trap_type __report_bug(struct bug_entry
> *bug, unsigned long buga
> return BUG_TRAP_TYPE_NONE;
> }
>
> - disable_trace_on_warning();
> -
> bug_get_file_line(bug, &file, &line);
> fmt = bug_get_format(bug);
>
> @@ -220,6 +219,17 @@ static enum bug_trap_type __report_bug(struct bug_entry
> *bug, unsigned long buga
> no_cut = bug->flags & BUGFLAG_NO_CUT_HERE;
> has_args = bug->flags & BUGFLAG_ARGS;
>
> +#ifdef CONFIG_KUNIT
Sashiko says:
"""
Is the CONFIG_KUNIT check sufficient here?
CONFIG_KUNIT is a tristate configuration option. When KUnit is built as a
module, the preprocessor macro CONFIG_KUNIT_MODULE is defined instead,
leaving CONFIG_KUNIT undefined.
Because lib/bug.c is compiled into the core kernel, this block will be
silently stripped out during a module build. This prevents warning
suppression from working on all architectures that rely on __report_bug().
Could this use IS_ENABLED(CONFIG_KUNIT) instead, or be dropped completely
since include/kunit/test-bug.h provides a safe stub?
"""
Ugh, it is right. I did not consider the module case. Not only is it
safe now as it says, but iirc we added it for performance, however,
since we now have static_branch, it is not really needed. I think
removing it is the right thing to do here.
> + /*
> + * Before the once logic so suppressed warnings do not consume
> + * the single-fire budget of WARN_ON_ONCE().
> + */
> + if (warning && kunit_is_suppressed_warning(true))
> + return BUG_TRAP_TYPE_WARN;
> +#endif
> +
> + disable_trace_on_warning();
> +
> if (warning && once) {
> if (done)
> return BUG_TRAP_TYPE_WARN;
> diff --git a/lib/kunit/Makefile b/lib/kunit/Makefile
> index 656f1fa35abcc..4592f9d0aa8dd 100644
> --- a/lib/kunit/Makefile
> +++ b/lib/kunit/Makefile
> @@ -10,7 +10,8 @@ kunit-objs += test.o \
> executor.o \
> attributes.o \
> device.o \
> - platform.o
> + platform.o \
> + bug.o
>
> ifeq ($(CONFIG_KUNIT_DEBUGFS),y)
> kunit-objs += debugfs.o
> diff --git a/lib/kunit/bug.c b/lib/kunit/bug.c
> new file mode 100644
> index 0000000000000..8579235c9ca68
> --- /dev/null
> +++ b/lib/kunit/bug.c
> @@ -0,0 +1,120 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * KUnit helpers for backtrace suppression
> + *
> + * Copyright (C) 2025 Alessandro Carminati <[email protected]>
> + * Copyright (C) 2024 Guenter Roeck <[email protected]>
> + */
> +
> +#include <kunit/resource.h>
> +#include <linux/export.h>
> +#include <linux/rculist.h>
> +#include <linux/sched.h>
> +#include <linux/sched/task.h>
> +#include <linux/spinlock.h>
> +
> +#include "hooks-impl.h"
> +
> +struct kunit_suppressed_warning {
> + struct list_head node;
> + struct task_struct *task;
> + struct kunit *test;
> + atomic_t counter;
> +};
> +
> +static LIST_HEAD(suppressed_warnings);
> +static DEFINE_SPINLOCK(suppressed_warnings_lock);
> +
> +static void kunit_suppress_warning_remove(struct kunit_suppressed_warning *w)
> +{
> + unsigned long flags;
> +
> + spin_lock_irqsave(&suppressed_warnings_lock, flags);
> + list_del_rcu(&w->node);
> + spin_unlock_irqrestore(&suppressed_warnings_lock, flags);
> + put_task_struct(w->task);
Sashiko says:
"""
Does this code introduce a use-after-free regression for concurrent RCU
readers?
Because the suppression handle is allocated using kunit_kzalloc() below,
the KUnit framework will automatically free it with a synchronous kfree()
at the end of the test.
Since the handle is unlinked using list_del_rcu() here, but there is no
synchronize_rcu() or kfree_rcu() between the list removal and the memory
free, a concurrent task evaluating warnings under rcu_read_lock() could
dereference the pointer after it has been freed.
Would it be safer to allocate the handle with kzalloc() and explicitly free
it using kfree_rcu() inside this cleanup action?
"""
It is taking a few iterations to get this right...
In the previous version we ruled out synchronize_rcu() because it is a
blocking call that can deadlock if exited while holding the RCU lock.
On the other hand, the suggested kfree_rcu(), only frees memory, but
we also need to release the task reference in the w struct after the
grace period. Reading
`Documentation/RCU/Design/Memory-Ordering/Tree-RCU-Memory-Ordering.rst`,
a solution could be to hold an `rcu_head` in the suppressed warning
struct and invoke call_rcu directly (and explicitly free as suggested
by Sashiko, so I'd need to change kunit_kzalloc() too).
I hope that clears all races.
> +}
> +
> +KUNIT_DEFINE_ACTION_WRAPPER(kunit_suppress_warning_cleanup,
> + kunit_suppress_warning_remove,
> + struct kunit_suppressed_warning *);
> +
> +bool kunit_has_active_suppress_warning(void)
> +{
> + return __kunit_is_suppressed_warning_impl(false);
> +}
> +EXPORT_SYMBOL_GPL(kunit_has_active_suppress_warning);
> +
> +struct kunit_suppressed_warning *
> +kunit_start_suppress_warning(struct kunit *test)
> +{
> + struct kunit_suppressed_warning *w;
> + unsigned long flags;
> + int ret;
> +
> + if (kunit_has_active_suppress_warning()) {
> + KUNIT_FAIL(test, "Another suppression block is already
> active");
> + return NULL;
> + }
> +
> + w = kunit_kzalloc(test, sizeof(*w), GFP_KERNEL);
> + if (!w) {
> + KUNIT_FAIL(test, "Failed to allocate suppression handle.");
> + return NULL;
> + }
> +
> + w->task = get_task_struct(current);
> + w->test = test;
> +
> + spin_lock_irqsave(&suppressed_warnings_lock, flags);
> + list_add_rcu(&w->node, &suppressed_warnings);
> + spin_unlock_irqrestore(&suppressed_warnings_lock, flags);
> +
> + ret = kunit_add_action_or_reset(test,
> + kunit_suppress_warning_cleanup, w);
> + if (ret) {
> + KUNIT_FAIL(test, "Failed to add suppression cleanup action.");
> + return NULL;
> + }
> +
> + return w;
> +}
> +EXPORT_SYMBOL_GPL(kunit_start_suppress_warning);
> +
> +void kunit_end_suppress_warning(struct kunit *test,
> + struct kunit_suppressed_warning *w)
> +{
> + if (!w)
> + return;
> + kunit_release_action(test, kunit_suppress_warning_cleanup, w);
> +}
> +EXPORT_SYMBOL_GPL(kunit_end_suppress_warning);
> +
> +void __kunit_suppress_auto_cleanup(struct kunit_suppressed_warning **wp)
> +{
> + if (*wp)
> + kunit_end_suppress_warning((*wp)->test, *wp);
> +}
> +EXPORT_SYMBOL_GPL(__kunit_suppress_auto_cleanup);
> +
> +int kunit_suppressed_warning_count(struct kunit_suppressed_warning *w)
> +{
> + return w ? atomic_read(&w->counter) : 0;
> +}
> +EXPORT_SYMBOL_GPL(kunit_suppressed_warning_count);
> +
> +bool __kunit_is_suppressed_warning_impl(bool count)
> +{
> + struct kunit_suppressed_warning *w;
> +
> + guard(rcu)();
> + list_for_each_entry_rcu(w, &suppressed_warnings, node) {
> + if (w->task == current) {
> + if (count)
> + atomic_inc(&w->counter);
> + return true;
> + }
> + }
> +
> + return false;
> +}
> diff --git a/lib/kunit/hooks-impl.h b/lib/kunit/hooks-impl.h
> index 4e71b2d0143ba..d8720f2616925 100644
> --- a/lib/kunit/hooks-impl.h
> +++ b/lib/kunit/hooks-impl.h
> @@ -19,6 +19,7 @@ void __printf(3, 4) __kunit_fail_current_test_impl(const
> char *file,
> int line,
> const char *fmt, ...);
> void *__kunit_get_static_stub_address_impl(struct kunit *test, void
> *real_fn_addr);
> +bool __kunit_is_suppressed_warning_impl(bool count);
>
> /* Code to set all of the function pointers. */
> static inline void kunit_install_hooks(void)
> @@ -26,6 +27,7 @@ static inline void kunit_install_hooks(void)
> /* Install the KUnit hook functions. */
> kunit_hooks.fail_current_test = __kunit_fail_current_test_impl;
> kunit_hooks.get_static_stub_address =
> __kunit_get_static_stub_address_impl;
> + kunit_hooks.is_suppressed_warning =
> __kunit_is_suppressed_warning_impl;
> }
>
> #endif /* _KUNIT_HOOKS_IMPL_H */
>
> --
> 2.53.0
>