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