blob: 72ded5e6794c9b494b598aba10fed9129366c409 [file]
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
#
# Functional test for kmemleak's N-consecutive-scan leak confirmation
# (the min_unref_scans module parameter).
#
# kmemleak only reports an object once it has stayed unreferenced for
# min_unref_scans consecutive scans. A threshold of 1 reports on the first
# scan (historical behaviour); higher values filter transient false
# positives where a live object's only reference is briefly invisible to a
# single scan (e.g. an RCU tree update in flight while the scan runs). The
# test loads samples/kmemleak's helper module to create orphan allocations
# and, counting only those orphans (matched by their [kmemleak_test]
# backtrace so unrelated leaks already present on the system are ignored),
# checks that:
# - a freshly allocated object is greyed on its first scan (its checksum
# settles then), so nothing can be reported before that priming scan;
# each case below primes once first,
# - at min_unref_scans=1 one scan after priming reports the orphans,
# - raising the threshold to 2 needs two scans after priming: one is not
# enough, the second reports,
# - the parameter reads back what was written.
#
# The "one post-prime scan is not enough at min_unref_scans=2" check is the
# core regression test: raising min_unref_scans must push the report
# strictly later. Like ksft_kmemleak_dedup.sh, if the module yields no
# detectable orphan at all in the running environment the test skips rather
# than failing.
#
# Author: Breno Leitao <leitao@debian.org>
# KTAP output helpers (ktap_skip_all, ktap_exit_fail_msg, ktap_test_pass, ...).
DIR="$(dirname "$(readlink -f "$0")")"
# shellcheck source=../kselftest/ktap_helpers.sh
source "${DIR}"/../kselftest/ktap_helpers.sh
KMEMLEAK=/sys/kernel/debug/kmemleak
PARAM=/sys/module/kmemleak/parameters/min_unref_scans
MODULE=kmemleak-test
AGE=6 # seconds; must exceed kmemleak's 5s minimum object age
ktap_print_header
[ "$(id -u)" -eq 0 ] || { ktap_skip_all "must run as root"; exit "$KSFT_SKIP"; }
[ -r "$KMEMLEAK" ] ||
{ ktap_skip_all "no kmemleak debugfs (CONFIG_DEBUG_KMEMLEAK)"; exit "$KSFT_SKIP"; }
[ -w "$PARAM" ] ||
{ ktap_skip_all "min_unref_scans module parameter not present"; exit "$KSFT_SKIP"; }
modinfo "$MODULE" >/dev/null 2>&1 ||
{ ktap_skip_all "$MODULE not built (CONFIG_SAMPLE_KMEMLEAK)"; exit "$KSFT_SKIP"; }
# kmemleak can be present but disabled at runtime (kmemleak=off boot arg,
# or it self-disabled after an internal error); a "scan" then returns
# EPERM. Probe once and skip if so.
echo scan > "$KMEMLEAK" 2>/dev/null ||
{ ktap_skip_all "kmemleak is disabled (check dmesg or kmemleak= boot arg)"; exit "$KSFT_SKIP"; }
prev=$(cat "$PARAM")
# shellcheck disable=SC2317 # invoked indirectly via trap
cleanup() {
echo "$prev" > "$PARAM" 2>/dev/null # restore the parameter
echo scan=on > "$KMEMLEAK" 2>/dev/null # re-enable auto scan
rmmod "$MODULE" 2>/dev/null
echo clear > "$KMEMLEAK" 2>/dev/null
}
trap cleanup EXIT
# Stop the automatic scan thread: only our manual scans should advance an
# object's consecutive-unreferenced run. An auto scan landing between two
# manual scans would change the result and make the test flaky.
echo scan=off > "$KMEMLEAK" 2>/dev/null
# Create a fresh, aged set of orphan objects from the helper module's init
# path (its kmalloc/vmalloc/percpu allocations are dropped right away).
# Pre-existing reported leaks are greyed first ("clear") so only our
# orphans are counted. The module is left loaded on purpose: once it is
# unloaded its symbols are gone, so the orphan backtraces no longer resolve
# to [kmemleak_test] and could not be matched below.
gen_orphans() {
rmmod "$MODULE" 2>/dev/null
echo clear > "$KMEMLEAK"
modprobe "$MODULE" ||
{ ktap_skip_all "failed to load $MODULE"; exit "$KSFT_SKIP"; }
sleep "$AGE"
}
scan() { echo scan > "$KMEMLEAK"; }
# Number of helper-module orphans currently reported by kmemleak. Matching
# the module's own backtrace ([kmemleak_test]) keeps the count immune to
# unrelated leaks on the running system. kmemleak only lists an object here
# once it has been reported, so this reflects the confirmation gating.
count_orphans() {
c=$(grep -c '\[kmemleak_test\]' "$KMEMLEAK" 2>/dev/null)
echo "${c:-0}"
}
# 0) the parameter reads back what was written.
echo 3 > "$PARAM"
[ "$(cat "$PARAM")" = "3" ] || ktap_exit_fail_msg "min_unref_scans did not read back as 3"
# Priming scan: kmemleak greys a freshly allocated object on its first scan
# (its checksum settles then), so nothing can be reported until a second
# scan. Every case below runs this priming scan before counting.
prime() { scan; }
# 1) min_unref_scans=1: one scan after priming reports the orphans. This
# also establishes that the helper produces detectable orphans here.
echo 1 > "$PARAM"
gen_orphans
prime
scan
first=$(count_orphans)
[ "$first" -gt 0 ] ||
{ ktap_skip_all "$MODULE produced no detectable orphans (cannot test min_unref_scans)"; exit "$KSFT_SKIP"; }
# 2) min_unref_scans=2: after priming, one scan is not enough (still
# gated), the second reports. The gated-scan-zero check is the core
# regression.
echo 2 > "$PARAM"
gen_orphans
prime
scan; s1=$(count_orphans)
scan; s2=$(count_orphans)
[ "$s1" -eq 0 ] || ktap_exit_fail_msg "min_unref_scans=2: $s1 orphan(s) after 1 post-prime scan (must be 0)"
[ "$s2" -gt 0 ] || ktap_exit_fail_msg "min_unref_scans=2: no report after 2 post-prime scans (false negative)"
ktap_set_plan 1
ktap_test_pass "min_unref_scans=1 reported $first orphan(s) one scan after priming; =2 held them one scan longer ($s1 after one scan, $s2 after two); param read-back ok"
ktap_finished