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
next prev 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox