All of lore.kernel.org
 help / color / mirror / Atom feed
From: KobaK <kobak@nvidia.com>
To: catalin.marinas@arm.com, will@kernel.org
Cc: corbet@lwn.net, skhan@linuxfoundation.org,
	linux-arm-kernel@lists.infradead.org, linux-doc@vger.kernel.org,
	linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org,
	fenghuay@nvidia.com, tdave@nvidia.com, kaihengf@nvidia.com,
	rupakr@nvidia.com, mohammedk@nvidia.com
Subject: [RFC PATCH v6 3/3] selftests: arm64: add arm_cpumod kselftest
Date: Mon, 17 Aug 2026 09:49:43 +0800	[thread overview]
Message-ID: <20260817014943.10-4-kobak@nvidia.com> (raw)
In-Reply-To: <20260817014943.10-1-kobak@nvidia.com>

From: Koba Ko <kobak@nvidia.com>

Add a kselftest for the arm_cpumod sysfs ABI. The test loads or reuses the
module and selects either one target CPU or a snapshot of all online
CPUs. In single-CPU mode, skip an offline target before module
validation. Observe the per-CPU online attribute first. If it is absent,
immediately require the CPU directory to still exist before treating the
CPU as non-hotpluggable and online. If the attribute exists, it must be
readable and contain 1.

After module load, recheck the online state before classifying a selected
CPU without a cpumod directory as an unsupported profile. Fail if the CPU
went offline or disappeared, log and skip CPUs that remain online without
a directory, and require at least one selected CPU to expose a supported
Grace or Vera profile. Return KSFT_SKIP if none do.

For supported CPUs, recognize the exposed Grace or Vera profile-specific
layout and validate the matching attributes. Only
ARM_CPUMOD_EXPECT_PROFILE supplies an external profile expectation.

Verify read paths by checking that attributes return decimal values and
that affected_cpus identifies the current CPU as a decimal CPU number.
Optional valid-write testing writes each current value back. Optional
invalid-write testing checks selected out-of-range values and confirms
that the prior value remains unchanged.

Both write paths are opt-in. Valid writeback can alter
implementation-defined CPU controls; invalid-write checks exercise
rejection through the same firmware-sensitive interface, but are expected
not to update state. Cover all-online-CPU mode through
/sys/devices/system/cpu/online and, when the test loaded the module,
verify cleanup by removing it and checking that the selected CPU subtrees
disappear. The test does not offline or online CPUs.

Reviewed-by: Fenghua Yu <fenghuay@nvidia.com>
Reviewed-by: Tushar Dave <tdave@nvidia.com>
Reviewed-by: Kai-Heng Feng <kaihengf@nvidia.com>
Signed-off-by: Koba Ko <kobak@nvidia.com>
---
 tools/testing/selftests/arm64/Makefile        |   2 +-
 .../selftests/arm64/arm_cpumod/Makefile       |   5 +
 .../selftests/arm64/arm_cpumod/arm_cpumod.sh  | 526 ++++++++++++++++++
 3 files changed, 532 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/arm64/arm_cpumod/Makefile
 create mode 100755 tools/testing/selftests/arm64/arm_cpumod/arm_cpumod.sh

diff --git a/tools/testing/selftests/arm64/Makefile b/tools/testing/selftests/arm64/Makefile
index e456f3b62fa13..0873be5af0369 100644
--- a/tools/testing/selftests/arm64/Makefile
+++ b/tools/testing/selftests/arm64/Makefile
@@ -4,7 +4,7 @@
 ARCH ?= $(shell uname -m 2>/dev/null || echo not)
 
 ifneq (,$(filter $(ARCH),aarch64 arm64))
-ARM64_SUBTARGETS ?= tags signal pauth fp mte bti abi gcs
+	ARM64_SUBTARGETS ?= tags signal pauth fp mte bti abi gcs arm_cpumod
 else
 ARM64_SUBTARGETS :=
 endif
diff --git a/tools/testing/selftests/arm64/arm_cpumod/Makefile b/tools/testing/selftests/arm64/arm_cpumod/Makefile
new file mode 100644
index 0000000000000..db0341cdbc621
--- /dev/null
+++ b/tools/testing/selftests/arm64/arm_cpumod/Makefile
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_PROGS := arm_cpumod.sh
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/arm64/arm_cpumod/arm_cpumod.sh b/tools/testing/selftests/arm64/arm_cpumod/arm_cpumod.sh
new file mode 100755
index 0000000000000..51c620a74c7d4
--- /dev/null
+++ b/tools/testing/selftests/arm64/arm_cpumod/arm_cpumod.sh
@@ -0,0 +1,526 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0-only
+
+readonly KSFT_SKIP=4
+readonly MODULE_NAME="arm_cpumod"
+readonly CPU_ID="${ARM_CPUMOD_CPU:-0}"
+readonly MODULE_PATH="${ARM_CPUMOD_KO:-}"
+readonly ENABLE_WRITES="${ARM_CPUMOD_ENABLE_WRITES:-0}"
+readonly ENABLE_INVALID_WRITES="${ARM_CPUMOD_ENABLE_INVALID_WRITES:-0}"
+readonly ALL_CPUS="${ARM_CPUMOD_ALL_CPUS:-0}"
+readonly EXPECT_PROFILE="${ARM_CPUMOD_EXPECT_PROFILE:-auto}"
+readonly CPU_ONLINE_PATH="/sys/devices/system/cpu/online"
+readonly COMMON_ATTRS=(affected_cpus pf_dis pf_mode)
+readonly COMMON_RW_ATTRS=(pf_dis pf_mode)
+readonly GRACE_ATTRS=(cbusy_filter_threshold cbusy_filter_window cmc_min_ways)
+readonly GRACE_RW_ATTRS=(cbusy_filter_threshold cbusy_filter_window cmc_min_ways)
+readonly VERA_ATTRS=(l2spr_cmc_max_ways)
+readonly VERA_RW_ATTRS=(l2spr_cmc_max_ways)
+
+loaded_by_test=0
+reused_loaded_module=0
+detected_profile=""
+current_cpu="${CPU_ID}"
+verified_cpus=0
+CPU_LIST=()
+
+log()
+{
+	echo "${MODULE_NAME} selftest: $*"
+}
+
+skip()
+{
+	log "$*"
+	exit "${KSFT_SKIP}"
+}
+
+fail()
+{
+	log "$*"
+	exit 1
+}
+
+cpu_dir()
+{
+	local cpu="$1"
+
+	printf '/sys/devices/system/cpu/cpu%s' "${cpu}"
+}
+
+cpu_is_online()
+{
+	local cpu="$1"
+	local dir="$(cpu_dir "${cpu}")"
+	local online="${dir}/online"
+	local value
+
+	if [ ! -e "${online}" ]; then
+		[ -d "${dir}" ] || return 1
+		return 0
+	fi
+
+	[ -r "${online}" ] || return 1
+	value=$(cat "${online}") || return 1
+	[ "${value}" = "1" ]
+}
+
+cpumod_dir()
+{
+	local cpu="$1"
+
+	printf '%s/cpumod' "$(cpu_dir "${cpu}")"
+}
+
+current_cpumod_dir()
+{
+	cpumod_dir "${current_cpu}"
+}
+
+cleanup()
+{
+	if [ "${loaded_by_test}" -eq 1 ]; then
+		rmmod "${MODULE_NAME}" >/dev/null 2>&1 || true
+	fi
+}
+trap cleanup EXIT
+
+require_cmd()
+{
+	command -v "$1" >/dev/null 2>&1 || skip "missing required command: $1"
+}
+
+module_loaded()
+{
+	grep -q "^${MODULE_NAME} " /proc/modules
+}
+
+wait_for_path_state()
+{
+	local expect_present="$1"
+	local target="$2"
+	local i
+
+	for i in $(seq 1 50); do
+		if [ "${expect_present}" = "present" ]; then
+			[ -e "${target}" ] && return 0
+		else
+			[ ! -e "${target}" ] && return 0
+		fi
+		sleep 0.1
+	done
+
+	return 1
+}
+
+read_attr()
+{
+	local attr="$1"
+	local file="$(current_cpumod_dir)/${attr}"
+	local value
+
+	value=$(cat "${file}") || fail "failed to read ${file}"
+	printf '%s' "${value}"
+}
+
+attr_exists()
+{
+	local attr="$1"
+
+	[ -f "$(current_cpumod_dir)/${attr}" ]
+}
+
+require_attr_present()
+{
+	local attr="$1"
+
+	attr_exists "${attr}" || fail "missing sysfs attribute $(current_cpumod_dir)/${attr}"
+}
+
+require_attr_absent()
+{
+	local attr="$1"
+
+	attr_exists "${attr}" && fail "unexpected sysfs attribute $(current_cpumod_dir)/${attr}"
+}
+
+check_decimal_attr()
+{
+	local attr="$1"
+	local value
+
+	value=$(read_attr "${attr}")
+	case "${value}" in
+	''|*[!0-9]*)
+		fail "${attr} returned non-decimal value: ${value}"
+		;;
+	esac
+}
+
+expand_online_cpus()
+{
+	local cpu_list="$1"
+	local part
+	local start
+	local start_num
+	local end
+	local end_num
+	local cpu
+	local parts
+
+	CPU_LIST=()
+	IFS=, read -ra parts <<< "${cpu_list}"
+	for part in "${parts[@]}"; do
+		part="${part//[[:space:]]/}"
+		[ -n "${part}" ] || fail "empty CPU range in ${cpu_list}"
+
+		if [[ "${part}" == *-* ]]; then
+			start="${part%-*}"
+			end="${part#*-}"
+			[[ "${start}" =~ ^[0-9]+$ ]] || fail "invalid CPU range start: ${part}"
+			[[ "${end}" =~ ^[0-9]+$ ]] || fail "invalid CPU range end: ${part}"
+			start_num=$((10#${start}))
+			end_num=$((10#${end}))
+			(( start_num <= end_num )) || fail "invalid descending CPU range: ${part}"
+
+			for ((cpu = start_num; cpu <= end_num; cpu++)); do
+				CPU_LIST+=("${cpu}")
+			done
+		else
+			[[ "${part}" =~ ^[0-9]+$ ]] || fail "invalid CPU entry: ${part}"
+			CPU_LIST+=("$((10#${part}))")
+		fi
+	done
+
+	[ "${#CPU_LIST[@]}" -gt 0 ] || fail "no online CPUs parsed from ${cpu_list}"
+}
+
+select_cpus()
+{
+	if [ "${ALL_CPUS}" = "1" ]; then
+		[ -r "${CPU_ONLINE_PATH}" ] || skip "missing ${CPU_ONLINE_PATH}"
+		expand_online_cpus "$(cat "${CPU_ONLINE_PATH}")"
+	else
+		cpu_is_online "${CPU_ID}" || \
+			skip "cpu${CPU_ID} is offline or its online state is unreadable"
+		CPU_LIST=("${CPU_ID}")
+	fi
+}
+
+detect_profile()
+{
+	local has_grace
+	local has_vera
+	local attr
+	local i
+	local dir="$(current_cpumod_dir)"
+
+	for i in $(seq 1 50); do
+		has_grace=0
+		has_vera=0
+
+		for attr in "${GRACE_ATTRS[@]}"; do
+			if attr_exists "${attr}"; then
+				has_grace=1
+				break
+			fi
+		done
+
+		for attr in "${VERA_ATTRS[@]}"; do
+			if attr_exists "${attr}"; then
+				has_vera=1
+				break
+			fi
+		done
+
+		if [ "${has_grace}" -eq 1 ] && [ "${has_vera}" -eq 1 ]; then
+			fail "mixed Grace/Vera-specific attributes under ${dir}"
+		fi
+
+		if [ "${has_grace}" -eq 1 ]; then
+			detected_profile="grace"
+			break
+		fi
+
+		if [ "${has_vera}" -eq 1 ]; then
+			detected_profile="vera"
+			break
+		fi
+
+		sleep 0.1
+	done
+
+	[ -n "${detected_profile}" ] || \
+		fail "unable to detect Grace or Vera sysfs layout under ${dir}"
+
+	if [ "${EXPECT_PROFILE}" != "auto" ] && [ "${EXPECT_PROFILE}" != "${detected_profile}" ]; then
+		fail "expected ${EXPECT_PROFILE} sysfs layout, detected ${detected_profile}"
+	fi
+}
+
+check_requirements()
+{
+	case "${CPU_ID}" in
+	''|*[!0-9]*)
+		fail "ARM_CPUMOD_CPU must be a decimal CPU index"
+		;;
+	esac
+
+	case "${EXPECT_PROFILE}" in
+	auto|grace|vera)
+		;;
+	*)
+		fail "ARM_CPUMOD_EXPECT_PROFILE must be auto, grace, or vera"
+		;;
+	esac
+
+	case "${ENABLE_WRITES}" in
+	0|1)
+		;;
+	*)
+		fail "ARM_CPUMOD_ENABLE_WRITES must be 0 or 1"
+		;;
+	esac
+
+	case "${ENABLE_INVALID_WRITES}" in
+	0|1)
+		;;
+	*)
+		fail "ARM_CPUMOD_ENABLE_INVALID_WRITES must be 0 or 1"
+		;;
+	esac
+
+	case "${ALL_CPUS}" in
+	0|1)
+		;;
+	*)
+		fail "ARM_CPUMOD_ALL_CPUS must be 0 or 1"
+		;;
+	esac
+
+	[ "$(uname -m)" = "aarch64" ] || skip "requires an arm64 host"
+	[ "$(id -u)" -eq 0 ] || skip "must be run as root"
+	select_cpus
+	for current_cpu in "${CPU_LIST[@]}"; do
+		[ -d "$(cpu_dir "${current_cpu}")" ] || skip "missing CPU directory $(cpu_dir "${current_cpu}")"
+	done
+	require_cmd rmmod
+
+	if [ -n "${MODULE_PATH}" ]; then
+		[ -r "${MODULE_PATH}" ] || skip "ARM_CPUMOD_KO is not readable: ${MODULE_PATH}"
+		require_cmd insmod
+	else
+		require_cmd modprobe
+		modprobe -n "${MODULE_NAME}" >/dev/null 2>&1 || \
+			skip "set ARM_CPUMOD_KO or install ${MODULE_NAME} into /lib/modules"
+	fi
+}
+
+load_module()
+{
+	if module_loaded; then
+		if [ -n "${MODULE_PATH}" ]; then
+			skip "${MODULE_NAME} is already loaded; unload it before using ARM_CPUMOD_KO"
+		fi
+
+		reused_loaded_module=1
+		log "${MODULE_NAME} already loaded, reusing existing instance"
+		return 0
+	fi
+
+	if [ -n "${MODULE_PATH}" ]; then
+		insmod "${MODULE_PATH}" || fail "insmod failed for ${MODULE_PATH}"
+		module_loaded || fail "${MODULE_NAME} did not appear in /proc/modules after insmod"
+	else
+		modprobe "${MODULE_NAME}" || fail "modprobe failed for ${MODULE_NAME}"
+		module_loaded || skip "${MODULE_NAME} did not appear in /proc/modules after modprobe; built-in or non-unloadable setup is unsupported"
+	fi
+
+	loaded_by_test=1
+}
+
+check_sysfs_layout()
+{
+	local attr
+	local expected_cpu
+	local value
+	local dir="$(current_cpumod_dir)"
+
+	wait_for_path_state present "${dir}" || \
+		fail "cpumod sysfs directory did not appear at ${dir}"
+
+	for attr in "${COMMON_ATTRS[@]}"; do
+		require_attr_present "${attr}"
+	done
+
+	detect_profile
+
+	case "${detected_profile}" in
+	grace)
+		for attr in "${GRACE_ATTRS[@]}"; do
+			require_attr_present "${attr}"
+		done
+		for attr in "${VERA_ATTRS[@]}"; do
+			require_attr_absent "${attr}"
+		done
+		;;
+	vera)
+		for attr in "${GRACE_ATTRS[@]}"; do
+			require_attr_absent "${attr}"
+		done
+		for attr in "${VERA_ATTRS[@]}"; do
+			require_attr_present "${attr}"
+		done
+		;;
+	esac
+
+	expected_cpu=$((10#${current_cpu}))
+	value=$(read_attr affected_cpus)
+	[ "${value}" = "${expected_cpu}" ] || \
+		fail "cpu${current_cpu} affected_cpus expected ${expected_cpu}, got ${value}"
+
+	for attr in "${COMMON_RW_ATTRS[@]}"; do
+		check_decimal_attr "${attr}"
+	done
+
+	case "${detected_profile}" in
+	grace)
+		for attr in "${GRACE_RW_ATTRS[@]}"; do
+			check_decimal_attr "${attr}"
+		done
+		;;
+	vera)
+		for attr in "${VERA_RW_ATTRS[@]}"; do
+			check_decimal_attr "${attr}"
+		done
+		;;
+	esac
+}
+
+writeback_same_value()
+{
+	local attr="$1"
+	local before
+	local after
+
+	before=$(read_attr "${attr}")
+	printf '%s\n' "${before}" > "$(current_cpumod_dir)/${attr}" || \
+		fail "failed to write back cpu${current_cpu}/${attr}=${before}"
+	after=$(read_attr "${attr}")
+	[ "${after}" = "${before}" ] || \
+		fail "cpu${current_cpu}/${attr} changed across writeback: before=${before} after=${after}"
+}
+
+check_writeback_same_value()
+{
+	local attr
+
+	[ "${ENABLE_WRITES}" = "1" ] || return 0
+
+	for attr in "${COMMON_RW_ATTRS[@]}"; do
+		writeback_same_value "${attr}"
+	done
+
+	case "${detected_profile}" in
+	grace)
+		for attr in "${GRACE_RW_ATTRS[@]}"; do
+			writeback_same_value "${attr}"
+		done
+		;;
+	vera)
+		for attr in "${VERA_RW_ATTRS[@]}"; do
+			writeback_same_value "${attr}"
+		done
+		;;
+	esac
+}
+
+check_invalid_write()
+{
+	local attr="$1"
+	local value="$2"
+	local before
+	local after
+	local file="$(current_cpumod_dir)/${attr}"
+
+	before=$(read_attr "${attr}")
+	if printf '%s\n' "${value}" > "${file}" 2>/dev/null; then
+		fail "cpu${current_cpu}/${attr} accepted invalid value ${value}"
+	fi
+	after=$(read_attr "${attr}")
+	[ "${after}" = "${before}" ] || \
+		fail "cpu${current_cpu}/${attr} changed after invalid write: before=${before} after=${after}"
+}
+
+check_invalid_writes()
+{
+	[ "${ENABLE_INVALID_WRITES}" = "1" ] || return 0
+
+	check_invalid_write pf_dis 2
+	check_invalid_write pf_mode 10
+	check_invalid_write pf_mode 99
+
+	case "${detected_profile}" in
+	grace)
+		check_invalid_write cbusy_filter_threshold 4
+		check_invalid_write cbusy_filter_window 4
+		check_invalid_write cmc_min_ways 8
+		;;
+	vera)
+		check_invalid_write l2spr_cmc_max_ways 8
+		;;
+	esac
+}
+
+check_cpu()
+{
+	current_cpu="$1"
+	detected_profile=""
+	check_sysfs_layout
+	log "cpu${current_cpu}: detected ${detected_profile} sysfs layout"
+	check_writeback_same_value
+	check_invalid_writes
+}
+
+unload_and_verify_cleanup()
+{
+	local cpu
+	local dir
+
+	if [ "${loaded_by_test}" -ne 1 ]; then
+		log "module was already loaded; leaving it in place and skipping unload cleanup check"
+		return 0
+	fi
+
+	rmmod "${MODULE_NAME}" || fail "rmmod failed for ${MODULE_NAME}"
+	loaded_by_test=0
+	for cpu in "${CPU_LIST[@]}"; do
+		dir="$(cpumod_dir "${cpu}")"
+		wait_for_path_state absent "${dir}" || \
+			fail "${dir} still present after unload"
+	done
+}
+
+check_requirements
+load_module
+for current_cpu in "${CPU_LIST[@]}"; do
+	if [ ! -d "$(cpumod_dir "${current_cpu}")" ]; then
+		cpu_is_online "${current_cpu}" || \
+			fail "cpu${current_cpu} went offline or its online state became unreadable"
+		log "cpu${current_cpu}: no cpumod directory; unsupported CPU profile, skipping"
+		continue
+	fi
+
+	check_cpu "${current_cpu}"
+	verified_cpus=$((verified_cpus + 1))
+done
+unload_and_verify_cleanup
+[ "${verified_cpus}" -gt 0 ] || \
+	skip "no selected CPU exposes a supported cpumod profile"
+
+if [ "${reused_loaded_module}" -eq 1 ]; then
+	log "PASS verified_cpus=${verified_cpus} expect_profile=${EXPECT_PROFILE} writes=${ENABLE_WRITES} invalid_writes=${ENABLE_INVALID_WRITES} all_cpus=${ALL_CPUS} (reused pre-loaded module; unload cleanup check skipped)"
+else
+	log "PASS verified_cpus=${verified_cpus} expect_profile=${EXPECT_PROFILE} writes=${ENABLE_WRITES} invalid_writes=${ENABLE_INVALID_WRITES} all_cpus=${ALL_CPUS}"
+fi
+exit 0
-- 
2.43.0


  parent reply	other threads:[~2026-08-17  1:50 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-17  1:49 [RFC PATCH v6 0/3] arm64: expose CPU prefetch and cache modulation controls KobaK
2026-08-17  1:49 ` [RFC PATCH v6 1/3] arm64: add CPU prefetch and cache modulation sysfs interface KobaK
2026-08-17  1:49 ` [RFC PATCH v6 2/3] Documentation/arch/arm64: document arm_cpumod KobaK
2026-08-17  1:49 ` KobaK [this message]
2026-08-17  2:21 ` [RFC PATCH v6 0/3] arm64: expose CPU prefetch and cache modulation controls KobaK

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260817014943.10-4-kobak@nvidia.com \
    --to=kobak@nvidia.com \
    --cc=catalin.marinas@arm.com \
    --cc=corbet@lwn.net \
    --cc=fenghuay@nvidia.com \
    --cc=kaihengf@nvidia.com \
    --cc=linux-arm-kernel@lists.infradead.org \
    --cc=linux-doc@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=mohammedk@nvidia.com \
    --cc=rupakr@nvidia.com \
    --cc=skhan@linuxfoundation.org \
    --cc=tdave@nvidia.com \
    --cc=will@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.