xref: /linux/tools/testing/selftests/dax/dax-kmem-hotplug.sh (revision 4e69c1856bfd9ffb7e9d335a25842fa211628929)
1#!/bin/bash
2# SPDX-License-Identifier: GPL-2.0
3#
4# Exercise the dax/kmem "state" sysfs attribute:
5#   /sys/bus/dax/devices/daxX.Y/state  ->  unplugged | online | online_kernel | online_movable
6#
7# The test needs a dax device already bound to the kmem driver.
8#
9# This test mutates a device's memory: online/offline cycles migrate any
10# in-use pages, and the optional unbind subtest wedges the device until
11# reboot. The tester must identify the target device and opt into the
12# destructive unbind tests.
13#
14#   DAX_KMEM_TEST_DEV=daxX.Y   test this specific device
15#   DAX_KMEM_TEST_DEV=auto     auto-discover the first kmem-bound dax device
16#                              (best-effort: it may be a device in use!)
17#   DAX_KMEM_TEST_UNBIND=1     also run the destructive unbind-while-online test
18#
19# If DAX_KMEM_TEST_DEV is unset the whole test SKIPs.
20#
21# A dax device can be provisioned with the memmap= boot param, e.g.:
22#   memmap=2G!4G
23#
24# then, in the booted system:
25#
26#   ndctl create-namespace -m devdax -e namespace0.0 -f
27#   daxctl reconfigure-device -N -m system-ram dax0.0   # bind kmem
28#   DAX_KMEM_TEST_DEV=auto ./dax-kmem-hotplug.sh
29
30# shellcheck disable=SC1091
31DIR="$(dirname "$(readlink -f "$0")")"
32. "$DIR"/../kselftest/ktap_helpers.sh
33
34DAX_BASE=/sys/bus/dax/devices
35MEM_BASE=/sys/devices/system/memory
36
37memtotal_kb() { awk '/^MemTotal:/ {print $2}' /proc/meminfo; }
38get_state() { cat "$HP" 2>/dev/null; }
39# set_state STATE -- write a state to the state attribute; returns the
40# write's exit status (0 = accepted by the kernel)
41set_state() { echo "$1" > "$HP" 2>/dev/null; }
42
43is_kmem_dax() {
44	local drv
45	[ -e "$DAX_BASE/$1/state" ] || return 1
46	drv=$(readlink "$DAX_BASE/$1/driver" 2>/dev/null)
47	[ "$(basename "${drv:-}")" = kmem ]
48}
49
50find_kmem_dax() {
51	local d
52	for d in "$DAX_BASE"/dax*; do
53		is_kmem_dax "$(basename "$d")" || continue
54		basename "$d"
55		return 0
56	done
57	return 1
58}
59
60# find_device_blocks -- print every memoryN block backing this dax device.
61# The blocks are derived from the device's own range(s) in /proc/iomem (the
62# reserved resource is named after the device), so we act on *its* blocks
63# rather than guessing by NUMA node - the target node may also hold unrelated
64# (and non-offlineable) memory.
65find_device_blocks() {
66	local bs
67	bs=$(cat "$MEM_BASE/block_size_bytes" 2>/dev/null)	# hex, no leading 0x
68	[ -n "$bs" ] || return 1
69	grep -E " : ${DAX}\$" /proc/iomem | while read -r line; do
70		local range s e i
71		range=${line%% :*}; range=${range// /}
72		s=${range%-*}; e=${range#*-}
73		for (( i = 0x$s / 0x$bs; i <= 0x$e / 0x$bs; i++ )); do
74			echo "memory$i"
75		done
76	done
77}
78
79# find_device_block -- print the first online block backing this dax device.
80find_device_block() {
81	local b
82	for b in $(find_device_blocks); do
83		[ -f "$MEM_BASE/$b/state" ] || continue
84		[ "$(cat "$MEM_BASE/$b/state")" = online ] || continue
85		echo "$b"
86		return 0
87	done
88	return 1
89}
90
91ktap_print_header
92
93if [ "$UID" != 0 ]; then
94	ktap_skip_all "must be run as root"
95	exit "$KSFT_SKIP"
96fi
97
98# Device selection is opt-in - see the header for why.
99DEV_SEL=${DAX_KMEM_TEST_DEV:-}
100if [ -z "$DEV_SEL" ]; then
101	ktap_skip_all "set DAX_KMEM_TEST_DEV=<daxX.Y|auto> to opt in (mutates device memory)"
102	exit "$KSFT_SKIP"
103fi
104if [ "$DEV_SEL" = auto ]; then
105	DAX=$(find_kmem_dax)
106else
107	DAX=$DEV_SEL
108fi
109if [ -z "$DAX" ] || ! is_kmem_dax "$DAX"; then
110	ktap_skip_all "no kmem-bound dax device with a state attribute (${DEV_SEL})"
111	exit "$KSFT_SKIP"
112fi
113HP=$DAX_BASE/$DAX/state
114ORIG=$(get_state)
115
116# A failure to reach the baseline is environmental (memory in use), not an
117# interface failure, so skip rather than fail.
118set_state unplugged; rc=$?
119if [ "$rc" != 0 ] || [ "$(get_state)" != unplugged ]; then
120	ktap_skip_all "$DAX: cannot reach 'unplugged' baseline (memory in use?)"
121	[ -n "$ORIG" ] && set_state "$ORIG"
122	exit "$KSFT_SKIP"
123fi
124mt_unplugged=$(memtotal_kb)
125
126DRV=/sys/bus/dax/drivers/kmem
127AOB=$MEM_BASE/auto_online_blocks
128
129ktap_print_msg "using $DAX (initial state was: $ORIG)"
130ktap_set_plan 10
131
132# A public (N_MEMORY) kmem node onlined into a kernel zone (online/online_kernel)
133# collects unmovable allocations and can then never be offlined, which would
134# wedge the device for the rest of this test.  So this test only ever
135# successfully onlines online_movable, the one mode that is reliably unpluggable.
136
137set_state online_movable; rc=$?
138mt_online=$(memtotal_kb)
139if [ "$rc" = 0 ] && [ "$(get_state)" = online_movable ] && [ "$mt_online" -gt "$mt_unplugged" ]; then
140	ktap_test_pass "online_movable: state=online_movable, MemTotal $mt_unplugged -> $mt_online kB"
141else
142	ktap_test_fail "online_movable: rc=$rc state=$(get_state) MemTotal $mt_unplugged -> $mt_online"
143fi
144
145set_state online_movable; rc=$?
146if [ "$rc" = 0 ] && [ "$(get_state)" = online_movable ]; then
147	ktap_test_pass "online_movable idempotent"
148else
149	ktap_test_fail "online_movable idempotent: rc=$rc state=$(get_state)"
150fi
151
152# A different online type is rejected without an intervening unplug.  The write
153# is refused before any hotplug, so this never actually onlines a kernel zone.
154set_state online_kernel; rc=$?
155if [ "$rc" != 0 ] && [ "$(get_state)" = online_movable ]; then
156	ktap_test_pass "reject online_kernel without intervening unplug (no kernel-zone online)"
157else
158	ktap_test_fail "online_movable->online_kernel not rejected: rc=$rc state=$(get_state)"
159fi
160
161set_state unplugged; rc=$?
162mt=$(memtotal_kb)
163if [ "$rc" = 0 ] && [ "$(get_state)" = unplugged ] && [ "$mt" -lt "$mt_online" ]; then
164	ktap_test_pass "unplug from online_movable: MemTotal $mt_online -> $mt kB"
165else
166	ktap_test_fail "unplug from online_movable: rc=$rc state=$(get_state) MemTotal $mt_online -> $mt"
167fi
168
169before=$(get_state)
170set_state bogus_state; rc=$?
171if [ "$rc" != 0 ] && [ "$(get_state)" = "$before" ]; then
172	ktap_test_pass "reject invalid state string"
173else
174	ktap_test_fail "invalid state not rejected: rc=$rc state=$(get_state)"
175fi
176
177# An online_movable -> unplug cycle must re-acquire the per-range resources on
178# each online and release them on each unplug.  Assert every iteration grows
179# MemTotal past the baseline and returns exactly to it; memory left online after
180# unplug (off > baseline) is a partial-free failure.
181set_state unplugged
182cycle_ok=1; fail_i=0; on=0; off=0
183for i in 1 2 3; do
184	if ! set_state online_movable; then cycle_ok=0; fail_i=$i; break; fi
185	on=$(memtotal_kb)
186	if ! set_state unplugged; then cycle_ok=0; fail_i=$i; break; fi
187	off=$(memtotal_kb)
188	# online must grow past baseline, and unplug must return to it - a
189	# partial free (memory left online) is a failure, not just off == on.
190	if [ "$on" -le "$mt_unplugged" ] || [ "$off" -gt "$mt_unplugged" ]; then
191		cycle_ok=0; fail_i=$i; break
192	fi
193done
194if [ "$cycle_ok" = 1 ]; then
195	ktap_test_pass "online_movable/unplug cycle re-acquires resources (3x: added and freed each time)"
196else
197	ktap_test_fail "online_movable/unplug cycle regressed at iteration $fail_i (on=$on off=$off baseline=$mt_unplugged)"
198fi
199
200# Desync: toggle a block through the legacy per-block memoryN/state interface
201# behind the driver's back, then unplug the whole device via daxX.Y/state.
202#
203# The driver only updates daxX.Y/state on its own writes, so it still reports
204# online_movable while a block underneath is already offline.
205#
206# Whole-device unplug must still succeed (within reason, an actor changing a
207# device from online_movable to online_kernel can no longer guarantee unplug).
208# At the very least, an already-offline block should not produce an error.
209set_state unplugged
210set_state online_movable
211blk=$(find_device_block)
212if [ -n "$blk" ] && echo offline > "$MEM_BASE/$blk/state" 2>/dev/null; then
213	# daxX.Y/state is now stale (still online_movable); unplug the device.
214	set_state unplugged; rc=$?
215	mt=$(memtotal_kb)
216	if [ "$rc" = 0 ] && [ "$(get_state)" = unplugged ] && [ "$mt" -le "$mt_unplugged" ]; then
217		ktap_test_pass "unplug tolerates a block pre-offlined via memoryN/state ($blk)"
218	else
219		ktap_test_fail "desync unplug: rc=$rc state=$(get_state) MemTotal=$mt baseline=$mt_unplugged"
220	fi
221else
222	set_state unplugged 2>/dev/null
223	ktap_test_skip "could not locate a device block to offline for desync test"
224fi
225
226# change system default online policy while the device is unbound, and show
227# the new system default policy is utilized across bindings.
228set_state unplugged
229if [ -w "$AOB" ] && [ -w "$DRV/unbind" ] && [ -w "$DRV/bind" ]; then
230	orig_aob=$(cat "$AOB")
231	echo "$DAX" > "$DRV/unbind" 2>/dev/null
232	echo offline > "$AOB" 2>/dev/null
233	echo "$DAX" > "$DRV/bind" 2>/dev/null
234	sleep 1
235	st=$(get_state)
236	echo "$orig_aob" > "$AOB" 2>/dev/null		# restore system policy
237	if [ "$st" = offline ]; then
238		ktap_test_pass "online policy resolved at bind: auto_online_blocks=offline -> state=offline"
239	else
240		ktap_test_fail "bind-time policy not honored: state=$st (expected offline)"
241	fi
242	set_state unplugged 2>/dev/null
243else
244	ktap_test_skip "auto_online_blocks or driver bind/unbind not writable"
245fi
246
247# Blocks offlined out-of-band (via memoryN/state) leave daxX.Y/state stale
248# (still online_movable) while every block is actually offline.  A driver unbind
249# must still hot-remove the offline memory and free its resources rather than
250# trust the stale state and leak until reboot.  Unbind uses remove_memory(),
251# which never offlines, so removing already-offline blocks is non-destructive and
252# the device rebinds cleanly afterwards.
253if [ -w "$DRV/unbind" ] && [ -w "$DRV/bind" ]; then
254	set_state unplugged
255	set_state online_movable
256	offl_ok=1
257	for b in $(find_device_blocks); do
258		[ -f "$MEM_BASE/$b/state" ] || continue
259		[ "$(cat "$MEM_BASE/$b/state")" = online ] || continue
260		echo offline > "$MEM_BASE/$b/state" 2>/dev/null || offl_ok=0
261	done
262	# daxX.Y/state is now stale (still online_movable) while all blocks are
263	# offline; the unbind must hot-remove them anyway.
264	if [ "$offl_ok" = 1 ] && [ "$(get_state)" = online_movable ]; then
265		echo "$DAX" > "$DRV/unbind" 2>/dev/null
266		mt_after=$(memtotal_kb)
267		leaked=$(grep -cE " : ${DAX}\$" /proc/iomem)	# before rebind
268		echo "$DAX" > "$DRV/bind" 2>/dev/null		# restore for later steps
269		sleep 1
270		if [ "$mt_after" -le "$mt_unplugged" ] && [ "$leaked" = 0 ]; then
271			ktap_test_pass "unbind with stale online state hot-removes offlined blocks (no leak)"
272		else
273			ktap_test_fail "desync unbind leaked: MemTotal=$mt_after baseline=$mt_unplugged iomem_left=$leaked"
274		fi
275		set_state unplugged 2>/dev/null
276	else
277		ktap_test_skip "could not offline all device blocks for desync-unbind test"
278	fi
279else
280	ktap_test_skip "driver bind/unbind not writable for desync-unbind test"
281fi
282
283[ -n "$ORIG" ] && set_state "$ORIG"
284
285# DESTRUCTIVE and opt-in only (DAX_KMEM_TEST_UNBIND=1):
286#
287# unbinding the driver while memory is online causes the resources to leak - but
288# the unbind should not deadlock.  Instead the driver leaks it with a warning.
289
290# This leaves the memory online and the device unbound until reboot, so it runs
291# last and only when explicitly requested.  online_movable only: this test
292# never onlines a public node into a kernel zone.
293
294if [ "${DAX_KMEM_TEST_UNBIND:-}" = 1 ] && [ -w "$DRV/unbind" ]; then
295	set_state unplugged; set_state online_movable
296fi
297if [ "${DAX_KMEM_TEST_UNBIND:-}" = 1 ] && [ "$(get_state)" = online_movable ] &&
298   [ -w "$DRV/unbind" ]; then
299	mt_on=$(memtotal_kb)
300	dmesg -C 2>/dev/null
301	echo "$DAX" > "$DRV/unbind" 2>/dev/null
302	mt_after=$(memtotal_kb)
303	# The leaked "System RAM (kmem)" regions stay in the iomem tree; reading
304	# their names dereferences res_name, which a buggy unbind already freed.
305	# Walk /proc/iomem to provoke that use-after-free (caught by KASAN).
306	cat /proc/iomem > /dev/null 2>&1
307	splat=$(dmesg 2>/dev/null | grep -ciE "KASAN|BUG:|use-after-free|general protection|Oops|refcount_t")
308	if [ "$splat" = 0 ] && [ "$mt_after" -ge "$mt_on" ]; then
309		ktap_test_pass "unbind while online: memory left online, no UAF/oops (MemTotal $mt_on -> $mt_after kB)"
310	else
311		ktap_test_fail "unbind while online regressed: splat=$splat MemTotal $mt_on -> $mt_after kB"
312	fi
313else
314	ktap_test_skip "destructive unbind-while-online test (set DAX_KMEM_TEST_UNBIND=1)"
315fi
316
317ktap_finished
318