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

Zhanpeng Zhang zhangzhanpeng.jasper at bytedance.com
Tue Jul 14 06:06:57 PDT 2026


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 at bytedance.com>
---
 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 at lists.infradead.org
 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 at microchip.com>
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)




More information about the linux-riscv mailing list