Add a RISC-V IOMMU QoS smoke test for the resctrl devices assignment ABI and the per-IOMMU global qosid sysfs attribute.
Probe for a usable IOMMU group with a non-default child-group assignment without disturbing groups already assigned by the system. Cover valid assignment and reset, devices-file readback, malformed and missing group IDs, strict decimal parsing, tasks-file rejection, group removal and pseudo-lock protection, and cleanup before child-group removal. When the global qosid attribute is present, cover its readback format, valid rewrite and restore, malformed input, missing separators, range checking, and integer overflow. Keep child-group coverage optional so an environment without allocatable resctrl resources can still exercise the root IOMMU QoS paths. Signed-off-by: Zhanpeng Zhang <[email protected]> --- MAINTAINERS | 1 + tools/testing/selftests/iommu/Makefile | 2 + .../selftests/iommu/iommu_qos_smoke.sh | 649 ++++++++++++++++++ 3 files changed, 652 insertions(+) create mode 100755 tools/testing/selftests/iommu/iommu_qos_smoke.sh diff --git a/MAINTAINERS b/MAINTAINERS index 162ad5a1780f..9facf8ac79a2 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -23289,6 +23289,7 @@ L: [email protected] S: Maintained F: Documentation/ABI/testing/sysfs-class-iommu-riscv-iommu F: drivers/resctrl/cbqri_iommu.c +F: tools/testing/selftests/iommu/iommu_qos_smoke.sh RISC-V MICROCHIP SUPPORT M: Conor Dooley <[email protected]> diff --git a/tools/testing/selftests/iommu/Makefile b/tools/testing/selftests/iommu/Makefile index 84abeb2f0949..2d22a10c17fc 100644 --- a/tools/testing/selftests/iommu/Makefile +++ b/tools/testing/selftests/iommu/Makefile @@ -7,4 +7,6 @@ TEST_GEN_PROGS := TEST_GEN_PROGS += iommufd TEST_GEN_PROGS += iommufd_fail_nth +TEST_PROGS += iommu_qos_smoke.sh + include ../lib.mk diff --git a/tools/testing/selftests/iommu/iommu_qos_smoke.sh b/tools/testing/selftests/iommu/iommu_qos_smoke.sh new file mode 100755 index 000000000000..1a81bca39460 --- /dev/null +++ b/tools/testing/selftests/iommu/iommu_qos_smoke.sh @@ -0,0 +1,649 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0-only +# Smoke test for RISC-V IOMMU QoS resctrl integration. +# +# This intentionally tests the IOMMU group QoS control path: +# resctrl/devices -> iommu_group:<id> parser -> IOMMU group QoS assignment +# explicit reset to the default group before resctrl group removal +# and the per-IOMMU global default QoS ID sysfs path used by BARE mode. + +KSFT_PASS=0 +KSFT_FAIL=1 +KSFT_SKIP=4 + +RESCTRL=/sys/fs/resctrl +RESCTRL_FS=resctrl +IOMMU_GROUPS=/sys/kernel/iommu_groups +LAST_STATUS=$RESCTRL/info/last_cmd_status +ROOT_DEVICES=$RESCTRL/devices +IOMMU_CLASS=/sys/class/iommu + +pass_count=0 +fail_count=0 +skip_count=0 +group="" +child_group="" +qosid_file="" +tmp_dir="" +lock_dir="" +lock_taken=0 +mounted_by_test=0 +qosid_rcid="" +qosid_mcid="" + +log() +{ + printf '%s\n' "$*" +} + +pass() +{ + pass_count=$((pass_count + 1)) + log "PASS: $*" +} + +fail() +{ + fail_count=$((fail_count + 1)) + log "FAIL: $*" +} + +skip() +{ + log "SKIP: $*" + exit $KSFT_SKIP +} + +skip_optional() +{ + skip_count=$((skip_count + 1)) + log "SKIP: $*" +} + +cleanup() +{ + if [ -n "$child_group" ] && [ -d "$child_group" ]; then + if [ -n "$group" ]; then + printf '%s\n' "iommu_group:$group" > "$ROOT_DEVICES" 2>/dev/null || true + fi + rmdir "$child_group" 2>/dev/null || true + fi + + if [ "$mounted_by_test" = "1" ]; then + umount "$RESCTRL" 2>/dev/null || true + fi + + if [ "$lock_taken" = "1" ] && [ -n "$lock_dir" ]; then + rmdir "$lock_dir" 2>/dev/null || true + fi + + if [ -n "$tmp_dir" ]; then + rm -rf "$tmp_dir" + fi +} + +need_root() +{ + [ "$(id -u)" = "0" ] || skip "requires root" +} + +init_tmp_dir() +{ + tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/iommu_qos_smoke.XXXXXX") || + skip "failed to create temporary directory" +} + +take_global_lock() +{ + lock_dir=${TMPDIR:-/tmp}/iommu_qos_smoke.lock + + if mkdir "$lock_dir" 2>"$tmp_dir/lock.err"; then + lock_taken=1 + pass "acquired global IOMMU QoS test lock" + return + fi + + skip "another IOMMU QoS smoke test instance is running" +} + +ensure_resctrl_mounted() +{ + if ! grep -qw resctrl /proc/filesystems; then + skip "resctrl filesystem is not available" + fi + + mkdir -p "$RESCTRL" 2>/dev/null || true + + if grep -qs " $RESCTRL resctrl " /proc/mounts; then + pass "resctrl already mounted" + return + fi + + if mount -t resctrl "$RESCTRL_FS" "$RESCTRL" 2>"$tmp_dir/mount.err"; then + mounted_by_test=1 + pass "mounted resctrl" + else + log "mount error: $(cat "$tmp_dir/mount.err" 2>/dev/null)" + skip "failed to mount resctrl" + fi +} + +ensure_devices_file() +{ + [ -e "$ROOT_DEVICES" ] || skip "resctrl devices file is not available" + [ -w "$ROOT_DEVICES" ] || skip "$ROOT_DEVICES is not writable" +} + +create_child_group() +{ + child_group=$RESCTRL/iommu_qos_test_$$ + + if mkdir "$child_group" 2>"$tmp_dir/mkdir.err"; then + pass "created child resctrl group" + return + fi + + log "mkdir error: $(cat "$tmp_dir/mkdir.err" 2>/dev/null)" + child_group="" + skip "failed to create a child resctrl group" +} + +iommu_group_is_assigned() +{ + candidate=$1 + + find "$RESCTRL" -type f -name devices \ + -exec grep -qx "iommu_group:$candidate" {} \; -print \ + 2>/dev/null | grep -q . +} + +pick_iommu_group() +{ + populated=0 + unsupported=0 + + [ -d "$IOMMU_GROUPS" ] || skip "no IOMMU groups sysfs directory" + + for path in "$IOMMU_GROUPS"/*; do + [ -d "$path" ] || continue + case "${path##*/}" in + *[!0-9]*|'') + continue + ;; + esac + + [ -d "$path/devices" ] || continue + if ! find "$path/devices" -mindepth 1 -maxdepth 1 2>/dev/null | + grep -q .; then + continue + fi + + populated=$((populated + 1)) + candidate=${path##*/} + if iommu_group_is_assigned "$candidate"; then + log "skip IOMMU group $candidate with an existing resctrl assignment" + continue + fi + + if printf '%s\n' "iommu_group:$candidate" > "$child_group/devices" \ + 2>"$tmp_dir/group_$candidate.err"; then + group=$candidate + if ! printf '%s\n' "iommu_group:$group" > "$ROOT_DEVICES" \ + 2>"$tmp_dir/group_${candidate}_reset.err"; then + log "reset error:" + cat "$tmp_dir/group_${candidate}_reset.err" 2>/dev/null + fail "failed to restore probed IOMMU group $group" + return 1 + fi + pass "selected group $group using non-default QoS assignment" + return 0 + fi + + if last_status_is_iommu_qos_unsupported; then + unsupported=$((unsupported + 1)) + continue + fi + + log "group $candidate write error:" + cat "$tmp_dir/group_$candidate.err" 2>/dev/null + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "IOMMU group $candidate failed QoS capability probing" + done + + [ "$populated" -gt 0 ] || skip "no populated IOMMU group found" + if [ "$unsupported" -eq "$populated" ]; then + skip "no populated IOMMU group supports QoS ID programming" + fi + + return 1 +} + +last_status_contains() +{ + pattern=$1 + + [ -f "$LAST_STATUS" ] || return 1 + grep -qi "$pattern" "$LAST_STATUS" +} + +last_status_is_iommu_qos_unsupported() +{ + [ -f "$LAST_STATUS" ] || return 1 + grep -Eiq \ + 'IOMMU group .* QoS \(-95\)|IOMMU QoSID.*not supported|not supported' \ + "$LAST_STATUS" +} + +pick_missing_iommu_group() +{ + max=0 + + for path in "$IOMMU_GROUPS"/*; do + [ -d "$path" ] || continue + id=${path##*/} + case "$id" in + *[!0-9]*|'') + continue + ;; + esac + if [ "$id" -gt "$max" ]; then + max=$id + fi + done + + if [ "$max" -ge 2147483647 ]; then + skip "cannot choose a missing IOMMU group id" + fi + + missing=$((max + 1)) + while [ -e "$IOMMU_GROUPS/$missing" ]; do + if [ "$missing" -ge 2147483647 ]; then + skip "cannot choose a missing IOMMU group id" + fi + missing=$((missing + 1)) + done +} + +pick_qosid_file() +{ + for file in "$IOMMU_CLASS"/*/qosid; do + [ -e "$file" ] || continue + qosid_file=$file + return 0 + done + + return 1 +} + +parse_qosid() +{ + line=$1 + + case "$line" in + rcid=[0-9]*" "mcid=[0-9]*) + qosid_rcid=${line%% *} + qosid_rcid=${qosid_rcid#rcid=} + qosid_mcid=${line##* } + qosid_mcid=${qosid_mcid#mcid=} + ;; + *) + return 1 + ;; + esac + + case "$qosid_rcid" in + *[!0-9]*|'') + return 1 + ;; + esac + case "$qosid_mcid" in + *[!0-9]*|'') + return 1 + ;; + esac + + return 0 +} + +test_malformed_group_rejected() +{ + if printf '%s\n' 'iommu_group:bad' > "$ROOT_DEVICES" \ + 2>"$tmp_dir/badtoken.err"; then + fail "malformed IOMMU group token was accepted" + return + fi + + if last_status_contains "IOMMU group parsing error"; then + pass "malformed IOMMU group token is rejected with useful status" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "malformed IOMMU group rejection status is unclear" + fi +} + +test_trailing_separator_rejected() +{ + if printf '%s\n' "iommu_group:$group," > "$ROOT_DEVICES" \ + 2>"$tmp_dir/trailing_separator.err"; then + fail "IOMMU group token with trailing separator was accepted" + return + fi + + if last_status_contains "Device list parsing error"; then + pass "trailing device-list separator is rejected" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "trailing separator rejection status is unclear" + fi +} + +test_missing_group_rejected() +{ + pick_missing_iommu_group + + if printf '%s\n' "iommu_group:$missing" > "$ROOT_DEVICES" \ + 2>"$tmp_dir/missing.err"; then + fail "missing IOMMU group was accepted" + return + fi + + if last_status_contains "No IOMMU group $missing"; then + pass "missing IOMMU group is rejected with useful status" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "missing IOMMU group rejection status is unclear" + fi +} + +test_non_decimal_group_rejected() +{ + if printf '%s\n' "iommu_group:0x$group" > "$ROOT_DEVICES" \ + 2>"$tmp_dir/non_decimal.err"; then + fail "non-decimal IOMMU group ID was accepted" + return + fi + + if last_status_contains "IOMMU group parsing error"; then + pass "non-decimal IOMMU group ID is rejected" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "non-decimal IOMMU group rejection status is unclear" + fi +} + +test_tasks_rejects_iommu_token() +{ + if printf '%s\n' "iommu_group:$group" > "$RESCTRL/tasks" \ + 2>"$tmp_dir/tasks_token.err"; then + fail "tasks accepted an IOMMU group token" + return + fi + + if last_status_contains "Task list parsing error pid iommu_group:$group"; then + pass "tasks rejects IOMMU group tokens" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "tasks rejection status is unclear" + fi +} + +test_global_qosid_sysfs() +{ + if ! pick_qosid_file; then + skip_optional "no RISC-V IOMMU qosid sysfs file is available" + return + fi + + if ! line=$(cat "$qosid_file" 2>"$tmp_dir/qosid_read.err"); then + log "read error: $(cat "$tmp_dir/qosid_read.err" 2>/dev/null)" + fail "failed to read IOMMU global qosid" + return + fi + + if parse_qosid "$line"; then + pass "read IOMMU global qosid from sysfs" + else + log "qosid content: $line" + fail "IOMMU global qosid has unexpected format" + return + fi + + if printf '%s\n' "$line" > "$qosid_file" \ + 2>"$tmp_dir/qosid_rewrite.err"; then + pass "IOMMU global qosid accepts readback format" + else + log "write error: $(cat "$tmp_dir/qosid_rewrite.err" 2>/dev/null)" + fail "failed to rewrite IOMMU global qosid readback" + return + fi + + if printf '%s\n' "bad" > "$qosid_file" 2>"$tmp_dir/qosid_bad.err"; then + fail "IOMMU global qosid accepted malformed input" + return + fi + pass "IOMMU global qosid rejects malformed input" + + if printf '%s\n' "rcid=0mcid=1" > "$qosid_file" \ + 2>"$tmp_dir/qosid_separator.err"; then + fail "IOMMU global qosid accepted missing token separator" + return + fi + pass "IOMMU global qosid requires a token separator" + + if printf '%s\n' "rcid=4096 mcid=0" > "$qosid_file" \ + 2>"$tmp_dir/qosid_range.err"; then + fail "IOMMU global qosid accepted out-of-range RCID" + return + fi + pass "IOMMU global qosid rejects out-of-range RCID" + + if printf '%s\n' "rcid=4294967296 mcid=0" > "$qosid_file" \ + 2>"$tmp_dir/qosid_overflow.err"; then + fail "IOMMU global qosid accepted overflowed RCID" + return + fi + pass "IOMMU global qosid rejects overflowed RCID" + + if printf '%s\n' "rcid=$qosid_rcid mcid=$qosid_mcid" > "$qosid_file" \ + 2>"$tmp_dir/qosid_restore.err"; then + pass "IOMMU global qosid accepts current RCID/MCID" + else + log "write error: $(cat "$tmp_dir/qosid_restore.err" 2>/dev/null)" + fail "failed to write current IOMMU global qosid" + return + fi + + if line=$(cat "$qosid_file" 2>/dev/null) && + [ "$line" = "rcid=$qosid_rcid mcid=$qosid_mcid" ]; then + pass "IOMMU global qosid readback matches restored value" + else + log "qosid content: $line" + fail "IOMMU global qosid readback mismatch" + fi +} + +test_valid_group_write() +{ + if printf '%s\n' "iommu_group:$group" > "$ROOT_DEVICES" \ + 2>"$tmp_dir/valid.err"; then + pass "write iommu_group:$group to root resctrl devices" + else + log "write error: $(cat "$tmp_dir/valid.err" 2>/dev/null)" + fail "valid IOMMU group write failed" + return + fi + + if last_status_contains ^ok; then + pass "last_cmd_status reports ok" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "last_cmd_status is not ok after valid write" + fi + + # The root control group represents the default QoS IDs. The kernel + # applies the reset but intentionally does not keep a software binding + # for default assignments, so the root devices file should stay empty. + if grep -qx "iommu_group:$group" "$ROOT_DEVICES"; then + log "devices content:" + cat "$ROOT_DEVICES" 2>/dev/null + fail "root devices unexpectedly shows default iommu_group:$group" + else + pass "root devices omits default iommu_group:$group" + fi + +} + +test_child_group_write() +{ + if printf '%s\n' "iommu_group:$group" > "$child_group/devices" \ + 2>"$tmp_dir/child.err"; then + pass "write iommu_group:$group to child resctrl group" + else + log "write error: $(cat "$tmp_dir/child.err" 2>/dev/null)" + fail "valid child IOMMU group write failed" + return + fi + + if last_status_contains ^ok; then + pass "last_cmd_status reports ok after child group write" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "last_cmd_status is not ok after child group write" + fi + + if grep -qx "iommu_group:$group" "$child_group/devices"; then + pass "child devices shows iommu_group:$group after set/get" + else + log "child devices content:" + cat "$child_group/devices" 2>/dev/null + fail "child devices does not show iommu_group:$group" + fi + + if grep -qx "iommu_group:$group" "$ROOT_DEVICES"; then + log "root devices content:" + cat "$ROOT_DEVICES" 2>/dev/null + fail "root devices still shows iommu_group:$group after child move" + else + pass "root devices no longer shows iommu_group:$group after child move" + fi + +} + +test_child_pseudo_locksetup_rejected() +{ + [ -d "$child_group" ] || return + + if [ ! -e "$child_group/mode" ]; then + skip_optional "resctrl mode file is not available" + return + fi + + if printf '%s\n' "pseudo-locksetup" > "$child_group/mode" \ + 2>"$tmp_dir/pseudo_locksetup.err"; then + printf '%s\n' "shareable" > "$child_group/mode" 2>/dev/null || true + fail "pseudo-locksetup accepted a group with assigned devices" + return + fi + + if last_status_contains \ + "Move devices out before entering pseudo-locksetup group"; then + pass "pseudo-locksetup rejects group with assigned devices" + elif last_status_contains "Unknown or unsupported mode"; then + skip_optional "pseudo-locksetup mode is not supported" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "pseudo-locksetup rejection status is unclear" + fi +} + +test_child_group_rmdir() +{ + [ -d "$child_group" ] || return + + if rmdir "$child_group" 2>"$tmp_dir/rmdir_busy.err"; then + fail "rmdir child resctrl group with IOMMU group was accepted" + # The child group no longer exists, so cleanup() cannot reset this. + if ! printf '%s\n' "iommu_group:$group" > "$ROOT_DEVICES" \ + 2>"$tmp_dir/rmdir_unexpected_move_root.err"; then + log "move error after unexpected rmdir:" + cat "$tmp_dir/rmdir_unexpected_move_root.err" 2>/dev/null + fi + child_group="" + return + fi + + if last_status_contains "Move devices out before removing group"; then + pass "rmdir rejects child group while IOMMU group is assigned" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "rmdir rejection status is unclear" + fi + + if printf '%s\n' "iommu_group:$group" > "$ROOT_DEVICES" \ + 2>"$tmp_dir/rmdir_move_root.err"; then + pass "move iommu_group:$group back to root before rmdir" + else + log "move error: $(cat "$tmp_dir/rmdir_move_root.err" 2>/dev/null)" + fail "failed to move iommu_group:$group back to root" + return + fi + + if rmdir "$child_group" 2>"$tmp_dir/rmdir.err"; then + pass "rmdir child resctrl group after moving IOMMU group out" + else + log "rmdir error: $(cat "$tmp_dir/rmdir.err" 2>/dev/null)" + fail "failed to rmdir child resctrl group after moving IOMMU group out" + return + fi + child_group="" + + if last_status_contains ^ok; then + pass "last_cmd_status reports ok after child group rmdir" + else + log "last_cmd_status: $(cat "$LAST_STATUS" 2>/dev/null)" + fail "last_cmd_status is not ok after child group rmdir" + fi + + if grep -qx "iommu_group:$group" "$ROOT_DEVICES"; then + log "root devices content:" + cat "$ROOT_DEVICES" 2>/dev/null + fail "root devices still shows iommu_group:$group after default move" + else + pass "root devices does not show iommu_group:$group after default move" + fi + +} + +trap cleanup EXIT +need_root +init_tmp_dir +take_global_lock +ensure_resctrl_mounted +ensure_devices_file +create_child_group +if ! pick_iommu_group; then + log "FAIL: $fail_count failure(s), $pass_count pass(es)" + exit $KSFT_FAIL +fi +test_malformed_group_rejected +test_trailing_separator_rejected +test_missing_group_rejected +test_non_decimal_group_rejected +test_tasks_rejects_iommu_token +test_global_qosid_sysfs +test_valid_group_write +test_child_group_write +test_child_pseudo_locksetup_rejected +test_child_group_rmdir + +if [ "$fail_count" -gt 0 ]; then + log "FAIL: $fail_count failure(s), $pass_count pass(es)" + exit $KSFT_FAIL +fi + +if [ "$skip_count" -gt 0 ]; then + log "PASS: $pass_count checks passed, $skip_count optional check(s) skipped" +else + log "PASS: $pass_count checks passed" +fi +exit $KSFT_PASS -- 2.50.1 (Apple Git-155)

