#!/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 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