From: Koba Ko <[email protected]>

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 <[email protected]>
Reviewed-by: Tushar Dave <[email protected]>
Reviewed-by: Kai-Heng Feng <[email protected]>
Signed-off-by: Koba Ko <[email protected]>
---
 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


Reply via email to