Re: [PATCH v11 1/4] bug/kunit: Core support for suppressing warning backtraces

Albert Esteve <[email protected]>
Newsgroups org.kernel.vger.workflows,org.freedesktop.lists.dri-devel,org.infradead.lists.linux-riscv,org.kernel.vger.linux-arch,org.kernel.vger.linux-doc,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest
Message-ID <CADSE00L3LuzrkUbuDj=TFRueeNY6cmgFgN8BsKsBdKNzW4ppiQ@mail.gmail.com>
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
>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.