1#!/bin/bash 2# SPDX-License-Identifier: GPL-2.0 3# 4# Regression test for kmemleak's per-scan verbose dedup. 5# 6# Loads samples/kmemleak's helper module to generate orphan allocations 7# (some of which share an allocation backtrace), runs a few kmemleak 8# scans with verbose printing enabled, and verifies that no two 9# "unreferenced object" reports within a single scan share the same 10# backtrace - which would mean dedup failed to collapse them. 11# 12# This test is intentionally permissive: the kmemleak-test module's 13# leaks frequently get reported across many separate scans (per-CPU 14# chunk reuse, slab freelist pointers, kernel stack residue), so dedup 15# may never have anything to fold within one scan. That is not a 16# regression. The test only fails when it actually catches dedup not 17# happening on input that should have triggered it - i.e. two reports 18# with identical backtraces in the same scan. 19# 20# Author: Breno Leitao <leitao@debian.org> 21 22ksft_skip=4 23KMEMLEAK=/sys/kernel/debug/kmemleak 24VERBOSE_PARAM=/sys/module/kmemleak/parameters/verbose 25MODULE=kmemleak-test 26 27skip() { 28 echo "SKIP: $*" 29 exit $ksft_skip 30} 31 32fail() { 33 echo "FAIL: $*" 34 exit 1 35} 36 37pass() { 38 echo "PASS: $*" 39 exit 0 40} 41 42[ "$(id -u)" -eq 0 ] || skip "must run as root" 43[ -r "$KMEMLEAK" ] || skip "no kmemleak debugfs (CONFIG_DEBUG_KMEMLEAK)" 44[ -w "$VERBOSE_PARAM" ] || skip "kmemleak verbose param missing" 45modinfo "$MODULE" >/dev/null 2>&1 || 46 skip "$MODULE not built (CONFIG_SAMPLE_KMEMLEAK)" 47 48# The verdict depends entirely on dmesg contents, so a silently-empty 49# dmesg (dmesg_restrict=1 with CAP_SYSLOG dropped, restricted container, 50# etc.) would let the script report PASS without parsing anything. Probe 51# both read and clear up front and skip cleanly if either is denied. 52dmesg >/dev/null 2>&1 || 53 skip "cannot read dmesg (need CAP_SYSLOG or dmesg_restrict=0)" 54dmesg -C >/dev/null 2>&1 || 55 skip "cannot clear dmesg (need CAP_SYSLOG or dmesg_restrict=0)" 56 57# kmemleak can be present but disabled at runtime (boot arg kmemleak=off, 58# or it self-disabled after an internal error). In that state writes other 59# than "clear" return EPERM, so probe once and skip if so. 60if ! echo scan > "$KMEMLEAK" 2>/dev/null; then 61 skip "kmemleak is disabled (check dmesg or kmemleak= boot arg)" 62fi 63 64prev_verbose=$(cat "$VERBOSE_PARAM") 65# shellcheck disable=SC2317 # invoked indirectly via trap 66cleanup() { 67 echo "$prev_verbose" > "$VERBOSE_PARAM" 2>/dev/null 68 rmmod "$MODULE" 2>/dev/null 69 # Drain the leak set we generated. Subsequent selftests (e.g. 70 # tools/testing/selftests/net/netfilter/nft_interface_stress.sh) 71 # fail on any non-empty kmemleak report, so leaving the helper 72 # module's intentional leaks behind would poison the rest of a 73 # kselftest run. 74 # 75 # Caveat: kmemleak_clear() only greys objects that have already 76 # been reported (OBJECT_REPORTED && unreferenced_object()). Helper 77 # allocations that stayed "still referenced" throughout the test 78 # (stale pointers in per-CPU chunks, slab freelists, kernel stacks) 79 # were never reported and are therefore not greyed by this clear - 80 # they remain tracked and a later scan can still surface them. Such 81 # leftovers are inherent to the kmemleak-test sample module and are 82 # not specific to this test; consumers that fail on any kmemleak 83 # output (rather than on the test-specific backtraces) need to be 84 # robust to that, or this test should be excluded from the run. 85 echo clear > "$KMEMLEAK" 2>/dev/null 86} 87trap cleanup EXIT 88 89echo 1 > "$VERBOSE_PARAM" 90 91# Drain the existing leak set so the next scan only reports our objects. 92echo clear > "$KMEMLEAK" 93 94# Re-clear dmesg now (the up-front probe also cleared it, but anything 95# logged between then and here - module unload chatter, the probe scan, 96# the verbose-param write - would otherwise pollute the parse window). 97dmesg -C >/dev/null 98 99# If the module was left loaded by a previous aborted run, modprobe would 100# be a no-op and the init function would not run, so no new leaks would be 101# generated. Force a clean state first. 102rmmod "$MODULE" 2>/dev/null 103modprobe "$MODULE" || skip "failed to load $MODULE" 104# Removing the module orphans the list elements without freeing them. 105rmmod "$MODULE" || skip "failed to unload $MODULE" 106 107# Run a handful of scans so kmemleak has the chance to age and report 108# the orphans. We do not require any particular number to be reported: 109# the regression check below operates on whatever lands in dmesg. 110# 111# Note: with CONFIG_DEBUG_KMEMLEAK_AUTO_SCAN=y the kernel's own scan 112# thread can report and mark these orphans (OBJECT_REPORTED) before our 113# manual scans run, after which our scans will see nothing. The 114# lower-bound check below catches the case where that happens and the 115# manual scans also produce nothing. 116SCAN_COUNT=4 117SCAN_SLEEP=6 118for _ in $(seq 1 "$SCAN_COUNT"); do 119 echo scan > "$KMEMLEAK" 120 sleep "$SCAN_SLEEP" 121done 122 123# Strip the leading "[ nnn.nnnnnn] " dmesg timestamp prefix. Without 124# this, two identical stack frames printed from two reports in the same 125# scan would produce different per-frame strings (different timestamps) 126# and the duplicate-backtrace check below would not match them, silently 127# passing a real dedup regression. Doing the strip here makes the rest 128# of the parser timestamp-agnostic regardless of what dmesg defaults to. 129log=$(dmesg | sed 's/^\[[^]]*\] //') 130 131# After running the workload (modprobe + scans), dmesg should contain at 132# least the helper module's pr_info lines and our manual-scan output. An 133# empty capture here means dmesg succeeded earlier but is now denying us 134# the buffer (race with dmesg_restrict toggling, etc.); refuse to give a 135# verdict on no evidence. 136[ -n "$log" ] || skip "dmesg returned empty after running workload" 137 138# Lower bound: if kmemleak's own per-scan tally counted leaks but the 139# verbose path emitted no "unreferenced object" line, the verbose printer 140# itself is regressed - fail rather than silently passing on no input. 141new_leaks=$(echo "$log" | 142 sed -n 's/.*kmemleak: \([0-9]\+\) new suspected.*/\1/p' | 143 awk '{s+=$1} END{print s+0}') 144printed=$(echo "$log" | grep -c 'kmemleak: unreferenced object') 145if [ "$new_leaks" -gt 0 ] && [ "$printed" -eq 0 ]; then 146 fail "verbose path broken: $new_leaks leaks counted, 0 printed in $SCAN_COUNT scans" 147fi 148 149# Walk the log: split into per-scan chunks at "N new suspected memory 150# leaks" boundaries; within each chunk, capture each "unreferenced 151# object" report's backtrace and check that no backtrace is reported 152# more than once. A duplicate within a single scan means dedup failed 153# to collapse two leaks that share an allocation site. 154violations=$(echo "$log" | awk ' 155 function flush_block() { 156 if (in_block) { 157 # Skip empty backtraces: leaks with trace_handle == 0 158 # (early-boot allocations or stack_depot_save() failures 159 # under memory pressure) are intentionally not deduped, 160 # so multiple such reports in one scan are expected and 161 # must not be flagged as a regression. 162 if (bt != "") 163 seen[bt]++ 164 in_block = 0 165 collecting = 0 166 bt = "" 167 } 168 } 169 function check_and_reset( b) { 170 for (b in seen) 171 if (seen[b] > 1) 172 printf("backtrace seen %d times in one scan:\n%s\n", 173 seen[b], b) 174 delete seen 175 } 176 # Scan boundary: the per-scan summary line. 177 /kmemleak: [0-9]+ new suspected memory leaks/ { 178 flush_block() 179 check_and_reset() 180 next 181 } 182 # Start of a new "unreferenced object" report. 183 /kmemleak: unreferenced object/ { 184 flush_block() 185 in_block = 1 186 next 187 } 188 # Inside a report, the "backtrace (crc ...):" line switches us to 189 # backtrace-collecting mode. 190 in_block && /kmemleak:[[:space:]]+backtrace \(crc/ { 191 collecting = 1 192 next 193 } 194 # Once collecting, capture only deeply-indented "kmemleak: " lines 195 # (stack frames have 4+ spaces of indentation under "kmemleak: "; 196 # headers and the "... and N more" tail line have less). This stops 197 # unrelated kmemleak warns landing between reports from being lumped 198 # into the backtrace key, which would mask a genuine duplicate. 199 in_block && collecting && /kmemleak:[[:space:]]{4,}/ { 200 bt = bt $0 "\n" 201 next 202 } 203 END { 204 flush_block() 205 check_and_reset() 206 } 207') 208 209if [ -n "$violations" ]; then 210 echo "$violations" 211 fail "kmemleak dedup regression: same backtrace reported more than once in a single scan" 212fi 213 214# Count the dedup summary lines so the report distinguishes "dedup 215# actually fired" from "no same-backtrace leaks turned up to dedup". 216dedup_lines=$(echo "$log" | grep -c 'more object(s) with the same backtrace') 217 218if [ "$dedup_lines" -gt 0 ]; then 219 pass "no dedup violations across $SCAN_COUNT scans; dedup fired ($dedup_lines summary line(s) observed)" 220else 221 pass "no dedup violations across $SCAN_COUNT scans; dedup had nothing to collapse" 222fi 223