From 85347718ab0dd7ede9c3e1dcff2d604c7073df05 Mon Sep 17 00:00:00 2001 From: Alessandro Carminati Date: Thu, 14 May 2026 13:06:37 +0200 Subject: bug/kunit: Core support for suppressing warning backtraces 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. Link: https://lore.kernel.org/r/20260514-kunit_add_support-v11-1-b36a530a6d8f@redhat.com Signed-off-by: Guenter Roeck Signed-off-by: Alessandro Carminati Reviewed-by: Kees Cook Reviewed-by: David Gow Signed-off-by: Albert Esteve Signed-off-by: Shuah Khan --- lib/bug.c | 14 +++++- lib/kunit/Makefile | 3 +- lib/kunit/bug.c | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/kunit/hooks-impl.h | 2 + 4 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 lib/kunit/bug.c (limited to 'lib') diff --git a/lib/bug.c b/lib/bug.c index 224f4cfa4aa3..d99e369bc110 100644 --- a/lib/bug.c +++ b/lib/bug.c @@ -48,6 +48,7 @@ #include #include #include +#include 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 + /* + * 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 656f1fa35abc..4592f9d0aa8d 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 000000000000..8579235c9ca6 --- /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 + * Copyright (C) 2024 Guenter Roeck + */ + +#include +#include +#include +#include +#include +#include + +#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); +} + +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 4e71b2d0143b..d8720f261692 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 */ -- cgit From bbc960d009a6315f484944506bbb13165069ccc8 Mon Sep 17 00:00:00 2001 From: Guenter Roeck Date: Thu, 14 May 2026 13:06:38 +0200 Subject: kunit: Add backtrace suppression self-tests Add unit tests to verify that warning backtrace suppression works. Tests cover both API forms: - Scoped: kunit_warning_suppress() with in-block count verification and post-block inactivity check. - Direct functions: kunit_start/end_suppress_warning() with sequential independent suppression blocks and per-block counts. Furthermore, tests verify incremental warning counting, that kunit_has_active_suppress_warning() transitions correctly around suppression boundaries, and that suppression active in the test kthread does not leak to a separate kthread. If backtrace suppression does _not_ work, the unit tests will likely trigger unsuppressed backtraces, which should actually help to get the affected architectures / platforms fixed. Link: https://lore.kernel.org/r/20260514-kunit_add_support-v11-2-b36a530a6d8f@redhat.com Tested-by: Linux Kernel Functional Testing Acked-by: Dan Carpenter Reviewed-by: Kees Cook Signed-off-by: Guenter Roeck Signed-off-by: Alessandro Carminati Reviewed-by: David Gow Signed-off-by: Albert Esteve Signed-off-by: Shuah Khan --- lib/kunit/Makefile | 1 + lib/kunit/backtrace-suppression-test.c | 198 +++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 lib/kunit/backtrace-suppression-test.c (limited to 'lib') diff --git a/lib/kunit/Makefile b/lib/kunit/Makefile index 4592f9d0aa8d..2e8a6b71a2ab 100644 --- a/lib/kunit/Makefile +++ b/lib/kunit/Makefile @@ -22,6 +22,7 @@ obj-$(if $(CONFIG_KUNIT),y) += hooks.o obj-$(CONFIG_KUNIT_TEST) += kunit-test.o obj-$(CONFIG_KUNIT_TEST) += platform-test.o +obj-$(CONFIG_KUNIT_TEST) += backtrace-suppression-test.o # string-stream-test compiles built-in only. ifeq ($(CONFIG_KUNIT_TEST),y) diff --git a/lib/kunit/backtrace-suppression-test.c b/lib/kunit/backtrace-suppression-test.c new file mode 100644 index 000000000000..7a2a59c6a780 --- /dev/null +++ b/lib/kunit/backtrace-suppression-test.c @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * KUnit test for suppressing warning tracebacks. + * + * Copyright (C) 2024, Guenter Roeck + * Author: Guenter Roeck + */ + +#include +#include +#include +#include + +static void backtrace_suppression_test_warn_direct(struct kunit *test) +{ + if (!IS_ENABLED(CONFIG_BUG)) + kunit_skip(test, "requires CONFIG_BUG"); + + kunit_warning_suppress(test) { + WARN(1, "This backtrace should be suppressed"); + /* + * Count must be checked inside the scope; the handle + * is not accessible after the block exits. + */ + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + } + KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning()); +} + +static noinline void trigger_backtrace_warn(void) +{ + WARN(1, "This backtrace should be suppressed"); +} + +static void backtrace_suppression_test_warn_indirect(struct kunit *test) +{ + if (!IS_ENABLED(CONFIG_BUG)) + kunit_skip(test, "requires CONFIG_BUG"); + + kunit_warning_suppress(test) { + trigger_backtrace_warn(); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + } +} + +static void backtrace_suppression_test_warn_multi(struct kunit *test) +{ + if (!IS_ENABLED(CONFIG_BUG)) + kunit_skip(test, "requires CONFIG_BUG"); + + kunit_warning_suppress(test) { + WARN(1, "This backtrace should be suppressed"); + trigger_backtrace_warn(); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 2); + } +} + +static void backtrace_suppression_test_warn_on_direct(struct kunit *test) +{ + if (!IS_ENABLED(CONFIG_BUG)) + kunit_skip(test, "requires CONFIG_BUG"); + if (!IS_ENABLED(CONFIG_DEBUG_BUGVERBOSE) && !IS_ENABLED(CONFIG_KALLSYMS)) + kunit_skip(test, "requires CONFIG_DEBUG_BUGVERBOSE or CONFIG_KALLSYMS"); + + kunit_warning_suppress(test) { + WARN_ON(1); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + } +} + +static noinline void trigger_backtrace_warn_on(void) +{ + WARN_ON(1); +} + +static void backtrace_suppression_test_warn_on_indirect(struct kunit *test) +{ + if (!IS_ENABLED(CONFIG_BUG)) + kunit_skip(test, "requires CONFIG_BUG"); + if (!IS_ENABLED(CONFIG_DEBUG_BUGVERBOSE)) + kunit_skip(test, "requires CONFIG_DEBUG_BUGVERBOSE"); + + kunit_warning_suppress(test) { + trigger_backtrace_warn_on(); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + } +} + +static void backtrace_suppression_test_count(struct kunit *test) +{ + if (!IS_ENABLED(CONFIG_BUG)) + kunit_skip(test, "requires CONFIG_BUG"); + + kunit_warning_suppress(test) { + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 0); + + WARN(1, "suppressed"); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 1); + + WARN(1, "suppressed again"); + KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 2); + } +} + +static void backtrace_suppression_test_active_state(struct kunit *test) +{ + KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning()); + + kunit_warning_suppress(test) { + KUNIT_EXPECT_TRUE(test, kunit_has_active_suppress_warning()); + } + + KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning()); + + kunit_warning_suppress(test) { + KUNIT_EXPECT_TRUE(test, kunit_has_active_suppress_warning()); + } + + KUNIT_EXPECT_FALSE(test, kunit_has_active_suppress_warning()); +} + +static void backtrace_suppression_test_multi_scope(struct kunit *test) +{ + struct kunit_suppressed_warning *sw1, *sw2; + + if (!IS_ENABLED(CONFIG_BUG)) + kunit_skip(test, "requires CONFIG_BUG"); + if (!IS_ENABLED(CONFIG_DEBUG_BUGVERBOSE)) + kunit_skip(test, "requires CONFIG_DEBUG_BUGVERBOSE"); + + sw1 = kunit_start_suppress_warning(test); + trigger_backtrace_warn_on(); + WARN(1, "suppressed by sw1"); + kunit_end_suppress_warning(test, sw1); + + sw2 = kunit_start_suppress_warning(test); + WARN(1, "suppressed by sw2"); + kunit_end_suppress_warning(test, sw2); + + KUNIT_EXPECT_EQ(test, kunit_suppressed_warning_count(sw1), 2); + KUNIT_EXPECT_EQ(test, kunit_suppressed_warning_count(sw2), 1); +} + +struct cross_kthread_data { + bool was_active; + struct completion done; +}; + +static int cross_kthread_fn(void *data) +{ + struct cross_kthread_data *d = data; + + d->was_active = kunit_has_active_suppress_warning(); + complete(&d->done); + while (!kthread_should_stop()) + schedule(); + return 0; +} + +static void backtrace_suppression_test_cross_kthread(struct kunit *test) +{ + struct cross_kthread_data data; + struct task_struct *task; + + data.was_active = false; + init_completion(&data.done); + + kunit_warning_suppress(test) { + task = kthread_run(cross_kthread_fn, &data, "kunit-cross-test"); + KUNIT_ASSERT_FALSE(test, IS_ERR(task)); + wait_for_completion(&data.done); + kthread_stop(task); + } + + KUNIT_EXPECT_FALSE(test, data.was_active); +} + +static struct kunit_case backtrace_suppression_test_cases[] = { + KUNIT_CASE(backtrace_suppression_test_warn_direct), + KUNIT_CASE(backtrace_suppression_test_warn_indirect), + KUNIT_CASE(backtrace_suppression_test_warn_multi), + KUNIT_CASE(backtrace_suppression_test_warn_on_direct), + KUNIT_CASE(backtrace_suppression_test_warn_on_indirect), + KUNIT_CASE(backtrace_suppression_test_count), + KUNIT_CASE(backtrace_suppression_test_active_state), + KUNIT_CASE(backtrace_suppression_test_multi_scope), + KUNIT_CASE(backtrace_suppression_test_cross_kthread), + {} +}; + +static struct kunit_suite backtrace_suppression_test_suite = { + .name = "backtrace-suppression-test", + .test_cases = backtrace_suppression_test_cases, +}; +kunit_test_suites(&backtrace_suppression_test_suite); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("KUnit test to verify warning backtrace suppression"); -- cgit