--- /dev/null
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Regression test for kmemleak's per-scan verbose dedup.
+#
+# Loads samples/kmemleak's helper module to generate orphan allocations
+# (some of which share an allocation backtrace), runs a few kmemleak
+# scans with verbose printing enabled, and verifies that no two
+# "unreferenced object" reports within a single scan share the same
+# backtrace - which would mean dedup failed to collapse them.
+#
+# This test is intentionally permissive: the kmemleak-test module's
+# leaks frequently get reported across many separate scans (per-CPU
+# chunk reuse, slab freelist pointers, kernel stack residue), so dedup
+# may never have anything to fold within one scan. That is not a
+# regression. The test only fails when it actually catches dedup not
+# happening on input that should have triggered it - i.e. two reports
+# with identical backtraces in the same scan.
+#
+# Author: Breno Leitao <leitao@debian.org>
+
+ksft_skip=4
+KMEMLEAK=/sys/kernel/debug/kmemleak
+VERBOSE_PARAM=/sys/module/kmemleak/parameters/verbose
+MODULE=kmemleak-test
+
+skip() {
+ echo "SKIP: $*"
+ exit $ksft_skip
+}
+
+fail() {
+ echo "FAIL: $*"
+ exit 1
+}
+
+pass() {
+ echo "PASS: $*"
+ exit 0
+}
+
+[ "$(id -u)" -eq 0 ] || skip "must run as root"
+[ -r "$KMEMLEAK" ] || skip "no kmemleak debugfs (CONFIG_DEBUG_KMEMLEAK)"
+[ -w "$VERBOSE_PARAM" ] || skip "kmemleak verbose param missing"
+modinfo "$MODULE" >/dev/null 2>&1 ||
+ skip "$MODULE not built (CONFIG_SAMPLE_KMEMLEAK)"
+
+# The verdict depends entirely on dmesg contents, so a silently-empty
+# dmesg (dmesg_restrict=1 with CAP_SYSLOG dropped, restricted container,
+# etc.) would let the script report PASS without parsing anything. Probe
+# both read and clear up front and skip cleanly if either is denied.
+dmesg >/dev/null 2>&1 ||
+ skip "cannot read dmesg (need CAP_SYSLOG or dmesg_restrict=0)"
+dmesg -C >/dev/null 2>&1 ||
+ skip "cannot clear dmesg (need CAP_SYSLOG or dmesg_restrict=0)"
+
+# kmemleak can be present but disabled at runtime (boot arg kmemleak=off,
+# or it self-disabled after an internal error). In that state writes other
+# than "clear" return EPERM, so probe once and skip if so.
+if ! echo scan > "$KMEMLEAK" 2>/dev/null; then
+ skip "kmemleak is disabled (check dmesg or kmemleak= boot arg)"
+fi
+
+prev_verbose=$(cat "$VERBOSE_PARAM")
+# shellcheck disable=SC2317 # invoked indirectly via trap
+cleanup() {
+ echo "$prev_verbose" > "$VERBOSE_PARAM" 2>/dev/null
+ rmmod "$MODULE" 2>/dev/null
+ # Drain the leak set we generated. Subsequent selftests (e.g.
+ # tools/testing/selftests/net/netfilter/nft_interface_stress.sh)
+ # fail on any non-empty kmemleak report, so leaving the helper
+ # module's intentional leaks behind would poison the rest of a
+ # kselftest run.
+ #
+ # Caveat: kmemleak_clear() only greys objects that have already
+ # been reported (OBJECT_REPORTED && unreferenced_object()). Helper
+ # allocations that stayed "still referenced" throughout the test
+ # (stale pointers in per-CPU chunks, slab freelists, kernel stacks)
+ # were never reported and are therefore not greyed by this clear -
+ # they remain tracked and a later scan can still surface them. Such
+ # leftovers are inherent to the kmemleak-test sample module and are
+ # not specific to this test; consumers that fail on any kmemleak
+ # output (rather than on the test-specific backtraces) need to be
+ # robust to that, or this test should be excluded from the run.
+ echo clear > "$KMEMLEAK" 2>/dev/null
+}
+trap cleanup EXIT
+
+echo 1 > "$VERBOSE_PARAM"
+
+# Drain the existing leak set so the next scan only reports our objects.
+echo clear > "$KMEMLEAK"
+
+# Re-clear dmesg now (the up-front probe also cleared it, but anything
+# logged between then and here - module unload chatter, the probe scan,
+# the verbose-param write - would otherwise pollute the parse window).
+dmesg -C >/dev/null
+
+# If the module was left loaded by a previous aborted run, modprobe would
+# be a no-op and the init function would not run, so no new leaks would be
+# generated. Force a clean state first.
+rmmod "$MODULE" 2>/dev/null
+modprobe "$MODULE" || skip "failed to load $MODULE"
+# Removing the module orphans the list elements without freeing them.
+rmmod "$MODULE" || skip "failed to unload $MODULE"
+
+# Run a handful of scans so kmemleak has the chance to age and report
+# the orphans. We do not require any particular number to be reported:
+# the regression check below operates on whatever lands in dmesg.
+#
+# Note: with CONFIG_DEBUG_KMEMLEAK_AUTO_SCAN=y the kernel's own scan
+# thread can report and mark these orphans (OBJECT_REPORTED) before our
+# manual scans run, after which our scans will see nothing. The
+# lower-bound check below catches the case where that happens and the
+# manual scans also produce nothing.
+SCAN_COUNT=4
+SCAN_SLEEP=6
+for _ in $(seq 1 "$SCAN_COUNT"); do
+ echo scan > "$KMEMLEAK"
+ sleep "$SCAN_SLEEP"
+done
+
+# Strip the leading "[ nnn.nnnnnn] " dmesg timestamp prefix. Without
+# this, two identical stack frames printed from two reports in the same
+# scan would produce different per-frame strings (different timestamps)
+# and the duplicate-backtrace check below would not match them, silently
+# passing a real dedup regression. Doing the strip here makes the rest
+# of the parser timestamp-agnostic regardless of what dmesg defaults to.
+log=$(dmesg | sed 's/^\[[^]]*\] //')
+
+# After running the workload (modprobe + scans), dmesg should contain at
+# least the helper module's pr_info lines and our manual-scan output. An
+# empty capture here means dmesg succeeded earlier but is now denying us
+# the buffer (race with dmesg_restrict toggling, etc.); refuse to give a
+# verdict on no evidence.
+[ -n "$log" ] || skip "dmesg returned empty after running workload"
+
+# Lower bound: if kmemleak's own per-scan tally counted leaks but the
+# verbose path emitted no "unreferenced object" line, the verbose printer
+# itself is regressed - fail rather than silently passing on no input.
+new_leaks=$(echo "$log" |
+ sed -n 's/.*kmemleak: \([0-9]\+\) new suspected.*/\1/p' |
+ awk '{s+=$1} END{print s+0}')
+printed=$(echo "$log" | grep -c 'kmemleak: unreferenced object')
+if [ "$new_leaks" -gt 0 ] && [ "$printed" -eq 0 ]; then
+ fail "verbose path broken: $new_leaks leaks counted, 0 printed in $SCAN_COUNT scans"
+fi
+
+# Walk the log: split into per-scan chunks at "N new suspected memory
+# leaks" boundaries; within each chunk, capture each "unreferenced
+# object" report's backtrace and check that no backtrace is reported
+# more than once. A duplicate within a single scan means dedup failed
+# to collapse two leaks that share an allocation site.
+violations=$(echo "$log" | awk '
+ function flush_block() {
+ if (in_block) {
+ # Skip empty backtraces: leaks with trace_handle == 0
+ # (early-boot allocations or stack_depot_save() failures
+ # under memory pressure) are intentionally not deduped,
+ # so multiple such reports in one scan are expected and
+ # must not be flagged as a regression.
+ if (bt != "")
+ seen[bt]++
+ in_block = 0
+ collecting = 0
+ bt = ""
+ }
+ }
+ function check_and_reset( b) {
+ for (b in seen)
+ if (seen[b] > 1)
+ printf("backtrace seen %d times in one scan:\n%s\n",
+ seen[b], b)
+ delete seen
+ }
+ # Scan boundary: the per-scan summary line.
+ /kmemleak: [0-9]+ new suspected memory leaks/ {
+ flush_block()
+ check_and_reset()
+ next
+ }
+ # Start of a new "unreferenced object" report.
+ /kmemleak: unreferenced object/ {
+ flush_block()
+ in_block = 1
+ next
+ }
+ # Inside a report, the "backtrace (crc ...):" line switches us to
+ # backtrace-collecting mode.
+ in_block && /kmemleak:[[:space:]]+backtrace \(crc/ {
+ collecting = 1
+ next
+ }
+ # Once collecting, capture only deeply-indented "kmemleak: " lines
+ # (stack frames have 4+ spaces of indentation under "kmemleak: ";
+ # headers and the "... and N more" tail line have less). This stops
+ # unrelated kmemleak warns landing between reports from being lumped
+ # into the backtrace key, which would mask a genuine duplicate.
+ in_block && collecting && /kmemleak:[[:space:]]{4,}/ {
+ bt = bt $0 "\n"
+ next
+ }
+ END {
+ flush_block()
+ check_and_reset()
+ }
+')
+
+if [ -n "$violations" ]; then
+ echo "$violations"
+ fail "kmemleak dedup regression: same backtrace reported more than once in a single scan"
+fi
+
+# Count the dedup summary lines so the report distinguishes "dedup
+# actually fired" from "no same-backtrace leaks turned up to dedup".
+dedup_lines=$(echo "$log" | grep -c 'more object(s) with the same backtrace')
+
+if [ "$dedup_lines" -gt 0 ]; then
+ pass "no dedup violations across $SCAN_COUNT scans; dedup fired ($dedup_lines summary line(s) observed)"
+else
+ pass "no dedup violations across $SCAN_COUNT scans; dedup had nothing to collapse"
+fi