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