[RFC PATCH 7/7] selftests/iommu: Add RISC-V IOMMU QoS smoke test

From: Zhanpeng Zhang

Date: Tue Jul 14 2026 - 09:22:17 EST


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 <zhangzhanpeng.jasper@xxxxxxxxxxxxx>
---
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: linux-riscv@xxxxxxxxxxxxxxxxxxx
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 <conor.dooley@xxxxxxxxxxxxx>
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)