From 801ade22c4fc078ea9b11e1f3fe3aae8b8bef886 Mon Sep 17 00:00:00 2001 From: Daniel Schwierzeck Date: Fri, 21 Aug 2026 10:45:39 +0200 Subject: [PATCH 01/89] cpufreq: airoha: fix probe failure with correct error check dev_pm_domain_attach_list() returns the number of attached PM domains on success (a positive count), or a negative error code on failure. The driver checks "if (ret)" which treats a successful attachment of one or more PM domains as an error, causing the probe to fail with return code 1. Fix by checking "if (ret < 0)" like all other users of this API. Cc: stable@vger.kernel.org Fixes: 84cf9e541ccc ("cpufreq: airoha: Add EN7581 CPUFreq SMCCC driver") Signed-off-by: Daniel Schwierzeck Signed-off-by: Christian Marangi Reviewed-by: Zhongqiu Han Signed-off-by: Viresh Kumar --- drivers/cpufreq/airoha-cpufreq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/cpufreq/airoha-cpufreq.c b/drivers/cpufreq/airoha-cpufreq.c index 3e7770860d13..8283c528d0c1 100644 --- a/drivers/cpufreq/airoha-cpufreq.c +++ b/drivers/cpufreq/airoha-cpufreq.c @@ -64,7 +64,7 @@ static int airoha_cpufreq_probe(struct platform_device *pdev) /* Attach PM for OPP */ ret = dev_pm_domain_attach_list(cpu_dev, &attach_data, &priv->pd_list); - if (ret) + if (ret < 0) goto clear_opp_config; cpufreq_dt = platform_device_register_simple("cpufreq-dt", -1, NULL, 0); From 886c9e80a232393f3d852a78f8933b1d476006cc Mon Sep 17 00:00:00 2001 From: Andy Shevchenko Date: Thu, 13 Aug 2026 10:17:21 +0200 Subject: [PATCH 02/89] intel_idle: Replace min_t() with the better alternatives Almost always use of min_t() is wrong. Replace it with min() and min3() as all types are int and there is no point to use typed variant of it. Signed-off-by: Andy Shevchenko Link: https://patch.msgid.link/20260813081721.2645937-1-andriy.shevchenko@linux.intel.com Signed-off-by: Rafael J. Wysocki --- drivers/idle/intel_idle.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/drivers/idle/intel_idle.c b/drivers/idle/intel_idle.c index 651408df9c24..0c7aa3236950 100644 --- a/drivers/idle/intel_idle.c +++ b/drivers/idle/intel_idle.c @@ -1792,9 +1792,7 @@ static bool __init intel_idle_cst_usable(void) { int cstate, limit; - limit = min_t(int, min_t(int, CPUIDLE_STATE_MAX, max_cstate + 1), - acpi_state_table.count); - + limit = min3(CPUIDLE_STATE_MAX, max_cstate + 1, acpi_state_table.count); for (cstate = 1; cstate < limit; cstate++) { struct acpi_processor_cx *cx = &acpi_state_table.states[cstate]; @@ -1938,7 +1936,7 @@ static void __init intel_idle_init_cstates_acpi_lpi(struct cpuidle_driver *drv) static void __init intel_idle_init_cstates_acpi_cst(struct cpuidle_driver *drv) { - int cstate, limit = min_t(int, CPUIDLE_STATE_MAX, acpi_state_table.count); + int cstate, limit = min(CPUIDLE_STATE_MAX, acpi_state_table.count); /* * If limit > 0, intel_idle_cst_usable() has returned 'true', so all of @@ -2018,7 +2016,7 @@ static bool __init intel_idle_off_by_default_lpi(unsigned int flags, u32 mwait_h static bool __init intel_idle_off_by_default_cst(unsigned int flags, u32 mwait_hint) { - int cstate, limit = min_t(int, CPUIDLE_STATE_MAX, acpi_state_table.count); + int cstate, limit = min(CPUIDLE_STATE_MAX, acpi_state_table.count); /* * If limit > 0, intel_idle_cst_usable() has returned 'true', so all of From 2094ae12eed8e83dd91a631ac0a582fee5934c36 Mon Sep 17 00:00:00 2001 From: Andy Shevchenko Date: Thu, 13 Aug 2026 10:26:24 +0200 Subject: [PATCH 03/89] intel_idle: Use 2-argument strscpy() Use 2-argument strscpy(), which is not only shorter but also provides an additional check that destination buffer is an array. Signed-off-by: Andy Shevchenko Link: https://patch.msgid.link/20260813082624.2650581-1-andriy.shevchenko@linux.intel.com Signed-off-by: Rafael J. Wysocki --- drivers/idle/intel_idle.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/idle/intel_idle.c b/drivers/idle/intel_idle.c index 0c7aa3236950..a045a384b949 100644 --- a/drivers/idle/intel_idle.c +++ b/drivers/idle/intel_idle.c @@ -1904,7 +1904,7 @@ static void __init intel_idle_init_cstates_acpi_lpi(struct cpuidle_driver *drv) state = &drv->states[drv->state_count++]; scnprintf(state->name, CPUIDLE_NAME_LEN, "C%d_LPI", index + 1); - strscpy(state->desc, lpi_state->desc, CPUIDLE_DESC_LEN); + strscpy(state->desc, lpi_state->desc); state->exit_latency = lpi_state->wake_latency; state->target_residency = lpi_state->min_residency; state->flags = MWAIT2flg(lpi_state->address); @@ -1954,7 +1954,7 @@ static void __init intel_idle_init_cstates_acpi_cst(struct cpuidle_driver *drv) state = &drv->states[drv->state_count++]; snprintf(state->name, CPUIDLE_NAME_LEN, "C%d_ACPI", cstate); - strscpy(state->desc, cx->desc, CPUIDLE_DESC_LEN); + strscpy(state->desc, cx->desc); state->exit_latency = cx->latency; /* * For C1-type C-states use the same number for both the exit From 18f64d8ca6dfd93c05bfeeda6171a8493746de23 Mon Sep 17 00:00:00 2001 From: Srinivas Pandruvada Date: Wed, 19 Aug 2026 10:40:20 -0700 Subject: [PATCH 04/89] powercap: intel_rapl: Add rugged Panther Lake support Add RAPL support for rugged Panther Lake. Signed-off-by: Srinivas Pandruvada Link: https://patch.msgid.link/20260819174020.1184329-1-sathyanarayanan.kuppuswamy@linux.intel.com Signed-off-by: Rafael J. Wysocki --- drivers/powercap/intel_rapl_msr.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/powercap/intel_rapl_msr.c b/drivers/powercap/intel_rapl_msr.c index a34543e66446..2c1b799d3c54 100644 --- a/drivers/powercap/intel_rapl_msr.c +++ b/drivers/powercap/intel_rapl_msr.c @@ -456,6 +456,7 @@ static const struct x86_cpu_id rapl_ids[] = { X86_MATCH_VFM(INTEL_EMERALDRAPIDS_X, &rapl_defaults_spr_server), X86_MATCH_VFM(INTEL_LUNARLAKE_M, &rapl_defaults_core), X86_MATCH_VFM(INTEL_PANTHERLAKE_L, &rapl_defaults_core_pl4_pmu), + X86_MATCH_VFM(INTEL_PANTHERLAKE_R, &rapl_defaults_core_pl4_pmu), X86_MATCH_VFM(INTEL_WILDCATLAKE_L, &rapl_defaults_core_pl4_pmu), X86_MATCH_VFM(INTEL_NOVALAKE, &rapl_defaults_core_pl4), X86_MATCH_VFM(INTEL_NOVALAKE_L, &rapl_defaults_core_pl4), From a23fca4ae137ec8e7620cc4f00b468b89cf5a7bd Mon Sep 17 00:00:00 2001 From: K Prateek Nayak Date: Wed, 9 Sep 2026 10:26:50 +0000 Subject: [PATCH 05/89] cpufreq/amd-pstate-ut: Fix amd_pstate_ut_check_freq failure with 'Requested CPU Min frequency' BIOS option Since commit 608a76b65288 ("cpufreq/amd-pstate: Add support for the "Requested CPU Min frequency" BIOS option"), amd-pstate driver sets policy->min to frequency corresponding to bios_min_perf if a valid BIOS programmed min frequency value is detected. amd_pstate_ut_check_freq expects policy->min to always match lowest_nonlinear_freq which does not hold true on platforms with user configured BIOS min freq. Update the test case to compare policy->min to bios_min_freq on platforms that set it. Final comparison is adjusted to account for insane values by clamping the result within the supported frequency range. While at it, move freq_to_perf() and perf_to_freq() helpers to internal header to allow their use from amd-pstate-ut. Reviewed-by: Mario Limonciello Fixes: 608a76b65288 ("cpufreq/amd-pstate: Add support for the "Requested CPU Min frequency" BIOS option") Signed-off-by: K Prateek Nayak Link: https://lore.kernel.org/r/20260909102650.4582-1-kprateek.nayak@amd.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate-ut.c | 23 ++++++++++++++++++++++- drivers/cpufreq/amd-pstate.c | 13 ------------- drivers/cpufreq/amd-pstate.h | 14 ++++++++++++++ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/drivers/cpufreq/amd-pstate-ut.c b/drivers/cpufreq/amd-pstate-ut.c index e23773680e05..c2c1a166b3a9 100644 --- a/drivers/cpufreq/amd-pstate-ut.c +++ b/drivers/cpufreq/amd-pstate-ut.c @@ -226,11 +226,14 @@ static int amd_pstate_ut_check_freq(u32 index) for_each_online_cpu(cpu) { struct cpufreq_policy *policy __free(put_cpufreq_policy) = NULL; struct amd_cpudata *cpudata; + union perf_cached perf; policy = cpufreq_cpu_get(cpu); if (!policy) continue; + cpudata = policy->driver_data; + perf = READ_ONCE(cpudata->perf); if (!((policy->cpuinfo.max_freq >= cpudata->nominal_freq) && (cpudata->nominal_freq > cpudata->lowest_nonlinear_freq) && @@ -242,7 +245,25 @@ static int amd_pstate_ut_check_freq(u32 index) return -EINVAL; } - if (cpudata->lowest_nonlinear_freq != policy->min) { + if (perf.bios_min_perf) { + u32 bios_min_freq = perf_to_freq(perf, cpudata->nominal_freq, + perf.bios_min_perf); + + /* + * User set bios_min_freq cannot be trusted to + * be within the driver limits. Clamp it similar + * to cpufreq_verify_within_cpu_limits(). + */ + bios_min_freq = clamp_t(u32, bios_min_freq, + policy->cpuinfo.min_freq, + policy->cpuinfo.max_freq); + + if (bios_min_freq != policy->min) { + pr_err("%s cpu%d bios_min_freq=%d policy_min=%d, they should be equal!\n", + __func__, cpu, bios_min_freq, policy->min); + return -EINVAL; + } + } else if (cpudata->lowest_nonlinear_freq != policy->min) { pr_err("%s cpu%d cpudata_lowest_nonlinear_freq=%d policy_min=%d, they should be equal!\n", __func__, cpu, cpudata->lowest_nonlinear_freq, policy->min); return -EINVAL; diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 8bfd46d60843..ae1673b36ac4 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -145,19 +145,6 @@ static struct quirk_entry quirk_amd_7k62 = { .lowest_freq = 550, }; -static inline u8 freq_to_perf(union perf_cached perf, u32 nominal_freq, unsigned int freq_val) -{ - u32 perf_val = DIV_ROUND_UP_ULL((u64)freq_val * perf.nominal_perf, nominal_freq); - - return (u8)clamp(perf_val, perf.lowest_perf, perf.highest_perf); -} - -static inline u32 perf_to_freq(union perf_cached perf, u32 nominal_freq, u8 perf_val) -{ - return DIV_ROUND_UP_ULL((u64)nominal_freq * perf_val, - perf.nominal_perf); -} - static int __init dmi_matched_7k62_bios_bug(const struct dmi_system_id *dmi) { /** diff --git a/drivers/cpufreq/amd-pstate.h b/drivers/cpufreq/amd-pstate.h index 9f5a81976eae..578465187b7a 100644 --- a/drivers/cpufreq/amd-pstate.h +++ b/drivers/cpufreq/amd-pstate.h @@ -159,6 +159,20 @@ enum amd_pstate_mode { AMD_PSTATE_GUIDED, AMD_PSTATE_MAX, }; + +static inline u8 freq_to_perf(union perf_cached perf, u32 nominal_freq, unsigned int freq_val) +{ + u32 perf_val = DIV_ROUND_UP_ULL((u64)freq_val * perf.nominal_perf, nominal_freq); + + return (u8)clamp(perf_val, perf.lowest_perf, perf.highest_perf); +} + +static inline u32 perf_to_freq(union perf_cached perf, u32 nominal_freq, u8 perf_val) +{ + return DIV_ROUND_UP_ULL((u64)nominal_freq * perf_val, + perf.nominal_perf); +} + const char *amd_pstate_get_mode_string(enum amd_pstate_mode mode); int amd_pstate_get_status(void); int amd_pstate_update_status(const char *buf, size_t size); From db40a7877cf14fc34fa27ee479eb6561d10648cf Mon Sep 17 00:00:00 2001 From: "Mario Limonciello (AMD)" Date: Tue, 18 Aug 2026 23:11:51 -0500 Subject: [PATCH 06/89] cpufreq/amd-pstate: Add per SoC and per core type EPP tuning values On newer SoCs the behavior of the platform has changed, and using the same EPP values for everything will yield worse results than expected. Add a x86_cpu_id match table that allows setting policies on a per-SoC/per-core type basis. If a SoC is missing, fallback to legacy values that are used. Changes also: - Change epp_values array and amd_pstate_cpu_epp_values() from unsigned int to u8 - Add static_assert to ensure epp_values has rows for all CPU types - Fix show_energy_performance_preference() loop to exclude uninitialized EPP_INDEX_CUSTOM and EPP_INDEX_DYNAMIC slots - Add debug print statements - Remove amd_pstate_get_epp_from_platform_profile and inline the logic - Fix cleanup path in amd_pstate_set_dynamic_epp - Change show_energy_performance_preference to use for loop with preference tracking - Add debug prints in amd_pstate_init_epp_values Behavioral change: show_energy_performance_preference() now returns a numeric EPP value via sysfs when the cached EPP does not match any named preference, instead of returning -EINVAL as before. This provides visibility into custom EPP values set by hardware or BIOS. No new platforms are added in this commit. Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Signed-off-by: Mario Limonciello (AMD) --- drivers/cpufreq/amd-pstate.c | 183 ++++++++++++++++++++++++++++------- drivers/cpufreq/amd-pstate.h | 2 + 2 files changed, 148 insertions(+), 37 deletions(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index ae1673b36ac4..8047f168b6d7 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -55,10 +55,10 @@ #define AMD_PSTATE_TRANSITION_DELAY 1000 #define AMD_PSTATE_FAST_CPPC_TRANSITION_DELAY 600 -#define AMD_CPPC_EPP_PERFORMANCE 0x00 -#define AMD_CPPC_EPP_BALANCE_PERFORMANCE 0x80 -#define AMD_CPPC_EPP_BALANCE_POWERSAVE 0xBF -#define AMD_CPPC_EPP_POWERSAVE 0xFF +#define AMD_CPPC_EPP_LEGACY_PERFORMANCE 0x00 +#define AMD_CPPC_EPP_LEGACY_BALANCE_PERFORMANCE 0x80 +#define AMD_CPPC_EPP_LEGACY_BALANCE_POWERSAVE 0xBF +#define AMD_CPPC_EPP_LEGACY_POWERSAVE 0xFF static const char * const amd_pstate_mode_string[] = { [AMD_PSTATE_UNDEFINED] = "undefined", @@ -129,14 +129,86 @@ static const char * const energy_perf_strings[] = { }; static_assert(ARRAY_SIZE(energy_perf_strings) == EPP_INDEX_MAX); -static unsigned int epp_values[] = { - [EPP_INDEX_DEFAULT] = 0, - [EPP_INDEX_PERFORMANCE] = AMD_CPPC_EPP_PERFORMANCE, - [EPP_INDEX_BALANCE_PERFORMANCE] = AMD_CPPC_EPP_BALANCE_PERFORMANCE, - [EPP_INDEX_BALANCE_POWERSAVE] = AMD_CPPC_EPP_BALANCE_POWERSAVE, - [EPP_INDEX_POWERSAVE] = AMD_CPPC_EPP_POWERSAVE, +/* + * The numeric EPP value programmed for each named preference. First dimension + * is CPU type (TOPO_CPU_TYPE_ANY for non-hybrid, TOPO_CPU_TYPE_PERFORMANCE/ + * EFFICIENCY/LOW_POWER for hybrid). The initializer holds the legacy values + * used as the fallback on any platform not listed in amd_pstate_epp_soc_ids[]; + * amd_pstate_init_epp_values() overwrites slots at boot when the running SoC + * has a per-SoC (and potentially per-CPU-type) override. + */ +static u8 epp_values[][EPP_INDEX_MAX] = { + /* + * Initialize all CPU types to legacy defaults. + * amd_pstate_init_epp_values() will fix these up + * based on the platform during boot. + */ + [TOPO_CPU_TYPE_ANY ... TOPO_CPU_TYPE_LOW_POWER] = { + [EPP_INDEX_DEFAULT] = 0, + [EPP_INDEX_PERFORMANCE] = AMD_CPPC_EPP_LEGACY_PERFORMANCE, + [EPP_INDEX_BALANCE_PERFORMANCE] = AMD_CPPC_EPP_LEGACY_BALANCE_PERFORMANCE, + [EPP_INDEX_BALANCE_POWERSAVE] = AMD_CPPC_EPP_LEGACY_BALANCE_POWERSAVE, + [EPP_INDEX_POWERSAVE] = AMD_CPPC_EPP_LEGACY_POWERSAVE, + }, +}; +static_assert(ARRAY_SIZE(epp_values) == TOPO_CPU_TYPE_LOW_POWER + 1, + "epp_values must have entries for all CPU types up to TOPO_CPU_TYPE_LOW_POWER"); + +/* + * Get the EPP value row for a given CPU, accounting for hybrid CPU types. + * Non-hybrid systems use TOPO_CPU_TYPE_ANY; hybrid systems use the CPU's + * actual type (PERFORMANCE/EFFICIENCY/LOW_POWER). + */ +static inline u8 *amd_pstate_cpu_epp_values(enum x86_topology_cpu_type cpu_type) +{ + switch (cpu_type) { + case TOPO_CPU_TYPE_PERFORMANCE: + case TOPO_CPU_TYPE_EFFICIENCY: + case TOPO_CPU_TYPE_LOW_POWER: + return epp_values[cpu_type]; + default: + return epp_values[TOPO_CPU_TYPE_ANY]; + } +} + +/** + * struct amd_pstate_epp_values - EPP values for the four named preferences + * @performance: value for the "performance" preference + * @balance_performance: value for the "balance_performance" preference + * @balance_power: value for the "balance_power" preference + * @power: value for the "power" preference + */ +struct amd_pstate_epp_values { + u8 performance; + u8 balance_performance; + u8 balance_power; + u8 power; +}; + +/** + * struct amd_pstate_epp_soc - per-CPU-type EPP overrides for hybrid systems + * @performance_core: values for TOPO_CPU_TYPE_PERFORMANCE cores + * @efficiency_core: values for TOPO_CPU_TYPE_EFFICIENCY cores + * @low_power_core: values for TOPO_CPU_TYPE_LOW_POWER cores + * + * Referenced from amd_pstate_epp_soc_ids[] to give a hybrid platform its own + * numeric EPP values for the four named preferences, with distinct values per + * CPU type. Non-hybrid systems are not listed in the table and always use the + * legacy defaults. + */ +struct amd_pstate_epp_soc { + struct amd_pstate_epp_values performance_core; + struct amd_pstate_epp_values efficiency_core; + struct amd_pstate_epp_values low_power_core; +}; + +/* + * Per-CPU-type EPP overrides for hybrid systems. Only hybrid SoCs should be + * listed here; non-hybrid systems always use the legacy defaults. + */ +static const struct x86_cpu_id amd_pstate_epp_soc_ids[] __initconst = { + {} }; -static_assert(ARRAY_SIZE(epp_values) == EPP_INDEX_MAX - 2); typedef int (*cppc_mode_transition_fn)(int); @@ -1055,6 +1127,7 @@ static int amd_pstate_cpu_init(struct cpufreq_policy *policy) return -ENOMEM; cpudata->cpu = policy->cpu; + cpudata->cpu_type = cpu_data(policy->cpu).topo.cpu_type; ret = amd_pstate_init_perf(cpudata); if (ret) @@ -1182,13 +1255,16 @@ static int amd_pstate_power_supply_notifier(struct notifier_block *nb, static int amd_pstate_get_epp_from_platform_profile(struct cpufreq_policy *policy, enum platform_profile_option profile) { + struct amd_cpudata *cpudata = policy->driver_data; + u8 *values = amd_pstate_cpu_epp_values(cpudata->cpu_type); + switch (profile) { case PLATFORM_PROFILE_PERFORMANCE: - return AMD_CPPC_EPP_PERFORMANCE; + return values[EPP_INDEX_PERFORMANCE]; case PLATFORM_PROFILE_BALANCED: return amd_pstate_get_balanced_epp(policy); case PLATFORM_PROFILE_LOW_POWER: - return AMD_CPPC_EPP_POWERSAVE; + return values[EPP_INDEX_POWERSAVE]; default: break; } @@ -1398,6 +1474,7 @@ ssize_t store_energy_performance_preference(struct cpufreq_policy *policy, const char *buf, size_t count) { struct amd_cpudata *cpudata = policy->driver_data; + u8 *values = amd_pstate_cpu_epp_values(cpudata->cpu_type); ssize_t ret; bool raw_epp = false; u8 epp; @@ -1432,12 +1509,13 @@ ssize_t store_energy_performance_preference(struct cpufreq_policy *policy, } if (ret) - epp = epp_values[ret]; + epp = values[ret]; else epp = cpudata->epp_default_dc; } - if (epp > 0 && cpudata->policy == CPUFREQ_POLICY_PERFORMANCE) { + if (epp > 0 && epp != values[EPP_INDEX_PERFORMANCE] && + cpudata->policy == CPUFREQ_POLICY_PERFORMANCE) { pr_debug("EPP cannot be set under performance policy\n"); return -EBUSY; } @@ -1462,34 +1540,31 @@ EXPORT_SYMBOL_FOR_PSTATE_UT(store_energy_performance_preference); ssize_t show_energy_performance_preference(struct cpufreq_policy *policy, char *buf) { struct amd_cpudata *cpudata = policy->driver_data; - u8 preference, epp; + u8 *values = amd_pstate_cpu_epp_values(cpudata->cpu_type); + u8 epp; + int i; epp = FIELD_GET(AMD_CPPC_EPP_PERF_MASK, cpudata->cppc_req_cached); if (!cpudata->dynamic_epp && cpudata->raw_epp) return sysfs_emit(buf, "%u\n", epp); - switch (epp) { - case AMD_CPPC_EPP_PERFORMANCE: - preference = EPP_INDEX_PERFORMANCE; - break; - case AMD_CPPC_EPP_BALANCE_PERFORMANCE: - preference = EPP_INDEX_BALANCE_PERFORMANCE; - break; - case AMD_CPPC_EPP_BALANCE_POWERSAVE: - preference = EPP_INDEX_BALANCE_POWERSAVE; - break; - case AMD_CPPC_EPP_POWERSAVE: - preference = EPP_INDEX_POWERSAVE; - break; - default: - return -EINVAL; + /* + * Map the cached EPP value back to a named preference. Skip the + * "default" slot (index 0) so an EPP of 0 reports as "performance". + * Stop at POWERSAVE; CUSTOM and DYNAMIC are not initialized in epp_values. + */ + for (i = EPP_INDEX_PERFORMANCE; i <= EPP_INDEX_POWERSAVE; i++) { + const char *name = energy_perf_strings[i]; + + if (epp == values[i]) { + if (cpudata->dynamic_epp) + return sysfs_emit(buf, "dynamic(profile:%s)\n", name); + return sysfs_emit(buf, "%s\n", name); + } } - if (cpudata->dynamic_epp) - return sysfs_emit(buf, "dynamic(profile:%s)\n", energy_perf_strings[preference]); - - return sysfs_emit(buf, "%s\n", energy_perf_strings[preference]); + return sysfs_emit(buf, "%u\n", epp); } EXPORT_SYMBOL_FOR_PSTATE_UT(show_energy_performance_preference); @@ -1881,6 +1956,7 @@ static int amd_pstate_epp_cpu_init(struct cpufreq_policy *policy) return -ENOMEM; cpudata->cpu = policy->cpu; + cpudata->cpu_type = cpu_data(policy->cpu).topo.cpu_type; ret = amd_pstate_init_perf(cpudata); if (ret) @@ -1931,9 +2007,11 @@ static int amd_pstate_epp_cpu_init(struct cpufreq_policy *policy) cpudata->epp_default_ac = cpudata->epp_default_dc = default_epp; cpudata->current_profile = PLATFORM_PROFILE_PERFORMANCE; } else { + u8 *values = amd_pstate_cpu_epp_values(cpudata->cpu_type); + policy->policy = CPUFREQ_POLICY_POWERSAVE; - cpudata->epp_default_ac = AMD_CPPC_EPP_PERFORMANCE; - cpudata->epp_default_dc = AMD_CPPC_EPP_BALANCE_PERFORMANCE; + cpudata->epp_default_ac = values[EPP_INDEX_PERFORMANCE]; + cpudata->epp_default_dc = values[EPP_INDEX_BALANCE_PERFORMANCE]; cpudata->current_profile = PLATFORM_PROFILE_BALANCED; } @@ -2232,6 +2310,34 @@ static bool amd_cppc_supported(void) return true; } +/* + * Resolve the numeric EPP values for hybrid systems. Only hybrid SoCs are listed + * in amd_pstate_epp_soc_ids[]; non-hybrid systems always use the legacy defaults. + */ +static inline void __init amd_pstate_set_epp_values(enum x86_topology_cpu_type type, + const struct amd_pstate_epp_values *core) +{ + epp_values[type][EPP_INDEX_PERFORMANCE] = core->performance; + epp_values[type][EPP_INDEX_BALANCE_PERFORMANCE] = core->balance_performance; + epp_values[type][EPP_INDEX_BALANCE_POWERSAVE] = core->balance_power; + epp_values[type][EPP_INDEX_POWERSAVE] = core->power; +} + +static void __init amd_pstate_init_epp_values(void) +{ + const struct x86_cpu_id *id = x86_match_cpu(amd_pstate_epp_soc_ids); + const struct amd_pstate_epp_soc *soc; + + if (!id || !id->driver_data) + return; + + soc = (const struct amd_pstate_epp_soc *)id->driver_data; + + amd_pstate_set_epp_values(TOPO_CPU_TYPE_PERFORMANCE, &soc->performance_core); + amd_pstate_set_epp_values(TOPO_CPU_TYPE_EFFICIENCY, &soc->efficiency_core); + amd_pstate_set_epp_values(TOPO_CPU_TYPE_LOW_POWER, &soc->low_power_core); +} + static int __init amd_pstate_init(void) { struct device *dev_root; @@ -2259,6 +2365,9 @@ static int __init amd_pstate_init(void) /* check if this machine need CPPC quirks */ dmi_check_system(amd_pstate_quirks_table); + /* resolve per-SoC EPP values for the named preferences */ + amd_pstate_init_epp_values(); + /* * determine the driver mode from the command line or kernel config. * If no command line input is provided, cppc_state will be AMD_PSTATE_UNDEFINED. diff --git a/drivers/cpufreq/amd-pstate.h b/drivers/cpufreq/amd-pstate.h index 578465187b7a..c7189d177b81 100644 --- a/drivers/cpufreq/amd-pstate.h +++ b/drivers/cpufreq/amd-pstate.h @@ -146,6 +146,8 @@ struct amd_cpudata { enum platform_profile_option current_profile; struct device *ppdev; char *profile_name; + + enum x86_topology_cpu_type cpu_type; }; /* From 114a157e8d30601d024b53564a0fe54202f6567f Mon Sep 17 00:00:00 2001 From: "Mario Limonciello (AMD)" Date: Tue, 18 Aug 2026 23:12:16 -0500 Subject: [PATCH 07/89] cpufreq/amd-pstate: Add EPP tunings for Zen6 client platforms Zen6 client platforms perform better with individual tunings for different core types. Add initial EPP tuning values for those platforms. Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Signed-off-by: Mario Limonciello (AMD) --- drivers/cpufreq/amd-pstate.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 8047f168b6d7..4344cc93904b 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -206,7 +206,33 @@ struct amd_pstate_epp_soc { * Per-CPU-type EPP overrides for hybrid systems. Only hybrid SoCs should be * listed here; non-hybrid systems always use the legacy defaults. */ +static const struct amd_pstate_epp_soc epp_soc_zen6_client __initconst = { + .performance_core = { + .performance = 25, + .balance_performance = 51, + .balance_power = 64, + .power = 64, + }, + .efficiency_core = { + .performance = 25, + .balance_performance = 51, + .balance_power = 64, + .power = 115, + }, + .low_power_core = { + .performance = 25, + .balance_performance = 51, + .balance_power = 64, + .power = 115, + }, +}; + static const struct x86_cpu_id amd_pstate_epp_soc_ids[] __initconst = { + X86_MATCH_VENDOR_FAM_MODEL(AMD, 0x1A, 0x80, &epp_soc_zen6_client), + X86_MATCH_VENDOR_FAM_MODEL(AMD, 0x1A, 0x81, &epp_soc_zen6_client), + X86_MATCH_VENDOR_FAM_MODEL(AMD, 0x1A, 0x84, &epp_soc_zen6_client), + X86_MATCH_VENDOR_FAM_MODEL(AMD, 0x1A, 0x85, &epp_soc_zen6_client), + X86_MATCH_VENDOR_FAM_MODEL(AMD, 0x1A, 0xe0, &epp_soc_zen6_client), {} }; From 350de8c394d4e61df6ebe31d8ddd928a69766fd3 Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Mon, 31 Aug 2026 16:39:47 -0500 Subject: [PATCH 08/89] cpufreq/amd-pstate: Show a warning if missing EPP tunings EPP tunings are going to be more important going forward. Show a warning if on a heterogenous platform without them. Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Suggested-by: Christian Ludloff Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 4344cc93904b..83960a6336b9 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -2354,8 +2354,12 @@ static void __init amd_pstate_init_epp_values(void) const struct x86_cpu_id *id = x86_match_cpu(amd_pstate_epp_soc_ids); const struct amd_pstate_epp_soc *soc; - if (!id || !id->driver_data) + if (!id || !id->driver_data) { + if (cpu_feature_enabled(X86_FEATURE_ZEN6) && + cpu_feature_enabled(X86_FEATURE_AMD_HTR_CORES)) + pr_warn_once("No EPP tunings found for platform\n"); return; + } soc = (const struct amd_pstate_epp_soc *)id->driver_data; From c8663e0457e6e6c72006478f4b6da99060030ec1 Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Sat, 5 Sep 2026 03:07:06 +0200 Subject: [PATCH 09/89] cpufreq/amd-pstate: Remove obsolete amd_dynamic_epp documentation The amd_dynamic_epp command-line parameter was removed. Dynamic EPP is now selected through the per-policy energy_performance_preference attribute, which is already documented. Fixes: 32692fcf61ed ("cpufreq/amd-pstate: Remove "amd_dynamic_epp" cmdline and "dynamic_epp" sysfs") Assisted-by: LLM Signed-off-by: Karl Mehltretter Reviewed-by: K Prateek Nayak Reviewed-by: Randy Dunlap Link: https://lore.kernel.org/r/20260905010706.57380-1-kmehltretter@gmail.com Signed-off-by: Mario Limonciello --- Documentation/admin-guide/kernel-parameters.txt | 7 ------- Documentation/admin-guide/pm/amd-pstate.rst | 6 ------ 2 files changed, 13 deletions(-) diff --git a/Documentation/admin-guide/kernel-parameters.txt b/Documentation/admin-guide/kernel-parameters.txt index 68647ff4bdd2..378ba224948c 100644 --- a/Documentation/admin-guide/kernel-parameters.txt +++ b/Documentation/admin-guide/kernel-parameters.txt @@ -501,13 +501,6 @@ Kernel parameters disable Disable amd-pstate preferred core. - amd_dynamic_epp= - [X86] - disable - Disable amd-pstate dynamic EPP. - enable - Enable amd-pstate dynamic EPP. - amijoy.map= [HW,JOY] Amiga joystick support Map of devices attached to JOY0DAT and JOY1DAT Format: , diff --git a/Documentation/admin-guide/pm/amd-pstate.rst b/Documentation/admin-guide/pm/amd-pstate.rst index 6fd72ba52df2..4c5d3d8acf64 100644 --- a/Documentation/admin-guide/pm/amd-pstate.rst +++ b/Documentation/admin-guide/pm/amd-pstate.rst @@ -481,12 +481,6 @@ For systems that support ``amd-pstate`` preferred core, the core rankings will always be advertised by the platform. But OS can choose to ignore that via the kernel parameter ``amd_prefcore=disable``. -``amd_dynamic_epp`` - -When AMD pstate is in auto mode, dynamic EPP will control whether the kernel -autonomously changes the EPP mode. The default is disabled. It can be enabled -with the kernel parameter ``amd_dynamic_epp=enable``. - User Space Interface in ``sysfs`` - General =========================================== From 04cb4f75f314e137f0a0c38f9b4fd5c2b4b5667d Mon Sep 17 00:00:00 2001 From: Bradley Morgan Date: Thu, 13 Aug 2026 07:43:42 +0000 Subject: [PATCH 10/89] clocksource/drivers/timer-ti-dm: Unregister CPU PM notifier outside of the timer lock omap_dm_timer_remove() calls cpu_pm_unregister_notifier() with dm_timer_lock held and interrupts disabled. Nothing sleeps in there today, but it pins the helper into a context where it can never be allowed to sleep, which is in the way of restoring the RCU grace period on the cpu_pm notifier chain. Do the list lookup under the lock and move the unregister after the unlock. Nothing can race it at that point: remove() owns the device, and once the timer is off the list nobody can reach it anymore. Signed-off-by: Bradley Morgan Link: https://patch.msgid.link/20260813074343.26460-2-include@grrlz.net Signed-off-by: Rafael J. Wysocki --- drivers/clocksource/timer-ti-dm.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/clocksource/timer-ti-dm.c b/drivers/clocksource/timer-ti-dm.c index 6787acac9a43..922a8f14562c 100644 --- a/drivers/clocksource/timer-ti-dm.c +++ b/drivers/clocksource/timer-ti-dm.c @@ -1528,7 +1528,7 @@ static int omap_dm_timer_probe(struct platform_device *pdev) */ static void omap_dm_timer_remove(struct platform_device *pdev) { - struct dmtimer *timer; + struct dmtimer *timer, *found = NULL; unsigned long flags; int ret = -EINVAL; @@ -1536,14 +1536,17 @@ static void omap_dm_timer_remove(struct platform_device *pdev) list_for_each_entry(timer, &omap_timer_list, node) if (!strcmp(dev_name(&timer->pdev->dev), dev_name(&pdev->dev))) { - if (!(timer->capability & OMAP_TIMER_ALWON)) - cpu_pm_unregister_notifier(&timer->nb); list_del(&timer->node); + found = timer; ret = 0; break; } spin_unlock_irqrestore(&dm_timer_lock, flags); + /* Unregister outside the lock: cpu_pm_unregister_notifier() may sleep. */ + if (found && !(found->capability & OMAP_TIMER_ALWON)) + cpu_pm_unregister_notifier(&found->nb); + pm_runtime_disable(&pdev->dev); if (ret) From 61f7eee7d4664079fbce821576891c3ee5ccf597 Mon Sep 17 00:00:00 2001 From: Bradley Morgan Date: Thu, 13 Aug 2026 07:43:43 +0000 Subject: [PATCH 11/89] PM: CPU: Restore synchronize_rcu() to cpu_pm_unregister_notifier() cpu_pm_notify() walks the notifier chain lockless under only rcu_read_lock(). That only works if a removed notifier block is not freed until every concurrent walker is done with it. The chain used to be an atomic_notifier, whose unregister ends in synchronize_rcu(). Commit b2f6662ac08d ("PM: cpu: Make notifier chain use a raw_spinlock_t") switched it over to a raw_notifier, and in doing so replaced that with raw_notifier_chain_unregister(), which does not synchronize. The grace period quietly went away, so a driver that frees the memory holding its notifier block right after cpu_pm_unregister_notifier() returns can race with a concurrent walker: cpu1 (idle exit) cpu2 (driver remove) ---------------- -------------------- cpu_pm_notify(CPU_PM_EXIT) rcu_read_lock() nb = rcu_dereference_raw(*nl) cpu_pm_unregister_notifier(&od->nb) unlink, no grace period remove() frees od (devm) nb->notifier_call(nb, ...) /* use after free */ The rcu_read_lock() on cpu1 does not stop cpu2 from freeing the block, and nothing else does. The read side is the idle exit path, so this can hit on any system where a driver with a cpu_pm notifier gets unbound. Add the grace period back. It is only needed when a notifier was actually removed, so wait on success and return -ENOENT without waiting otherwise. The kerneldoc gets back the "may sleep" note that the same commit dropped. Fixes: b2f6662ac08d ("PM: cpu: Make notifier chain use a raw_spinlock_t") Cc: All applicable Signed-off-by: Bradley Morgan [ rjw: Subject tweak ] Link: https://patch.msgid.link/20260813074343.26460-3-include@grrlz.net Signed-off-by: Rafael J. Wysocki --- kernel/cpu_pm.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/kernel/cpu_pm.c b/kernel/cpu_pm.c index 7481fbb947d3..a2a598ad6e6d 100644 --- a/kernel/cpu_pm.c +++ b/kernel/cpu_pm.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -76,7 +77,8 @@ EXPORT_SYMBOL_GPL(cpu_pm_register_notifier); * * Remove a driver from the CPU PM notifier list. * - * This function has the same return conditions as raw_notifier_chain_unregister. + * This function may sleep, and has the same return conditions as + * raw_notifier_chain_unregister. */ int cpu_pm_unregister_notifier(struct notifier_block *nb) { @@ -86,6 +88,11 @@ int cpu_pm_unregister_notifier(struct notifier_block *nb) raw_spin_lock_irqsave(&cpu_pm_notifier.lock, flags); ret = raw_notifier_chain_unregister(&cpu_pm_notifier.chain, nb); raw_spin_unlock_irqrestore(&cpu_pm_notifier.lock, flags); + + /* Wait for the rcu_read_lock() walkers in cpu_pm_notify(). */ + if (!ret) + synchronize_rcu(); + return ret; } EXPORT_SYMBOL_GPL(cpu_pm_unregister_notifier); From d594187cafd5ad65afc5f00242f8fa20e1d8c0f1 Mon Sep 17 00:00:00 2001 From: Xueqin Luo Date: Thu, 27 Aug 2026 17:48:19 +0800 Subject: [PATCH 12/89] cpuidle: menu: Do not return a disabled idle state When idle state 0 is disabled and the PM QoS latency constraint is zero (latency_req == 0), the first branch of the || in the early-return condition of menu_select() is taken immediately, so state 0 is returned regardless of its disable flag. Address this by making the !disable check a prerequisite for the entire early-return condition, so that when state 0 is disabled the normal state selection loop is used instead, which already skips disabled states. Fixes: 53812cdc9100 ("cpuidle: menu: Move the latency_req == 0 special case check") Signed-off-by: Xueqin Luo Link: https://patch.msgid.link/20260827094820.3900062-1-luoxueqin@kylinos.cn Signed-off-by: Rafael J. Wysocki --- drivers/cpuidle/governors/menu.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/cpuidle/governors/menu.c b/drivers/cpuidle/governors/menu.c index 544a5d593007..eb529d73d4b7 100644 --- a/drivers/cpuidle/governors/menu.c +++ b/drivers/cpuidle/governors/menu.c @@ -284,10 +284,10 @@ static int menu_select(struct cpuidle_driver *drv, struct cpuidle_device *dev, data->bucket = BUCKETS - 1; } - if (latency_req == 0 || - ((data->next_timer_ns < drv->states[1].target_residency_ns || - latency_req < drv->states[1].exit_latency_ns) && - !dev->states_usage[0].disable)) { + if (!dev->states_usage[0].disable && + (latency_req == 0 || + data->next_timer_ns < drv->states[1].target_residency_ns || + latency_req < drv->states[1].exit_latency_ns)) { /* * In this case state[0] will be used no matter what, so return * it right away and keep the tick running if state[0] is a From 60d5fca628f94f8f23398ad62d80aa284dae6571 Mon Sep 17 00:00:00 2001 From: Xueqin Luo Date: Fri, 28 Aug 2026 11:24:57 +0800 Subject: [PATCH 13/89] cpuidle: teo: Do not return a disabled idle state If idle state 0 is disabled and none of the enabled idle states satisfies the PM QoS latency constraint, constraint_idx in teo_select() stays at its initial value of 0, because the loop that updates it starts at state 1, and so the candidate state index is capped at state 0, which is disabled, and that state is returned to the caller. The cpuidle core does not validate the returned state index against the per-state disable flags, so the CPU ends up entering the disabled state 0. To address this, ensure that constraint_idx is at least equal to the index of the first enabled idle state (idx0), so that the candidate state is never capped to a disabled one. Fixes: c410a9a142f1 ("cpuidle: teo: Change the main idle state selection logic") Signed-off-by: Xueqin Luo Reviewed-by: Christian Loehle Link: https://patch.msgid.link/20260828032457.2317326-1-luoxueqin@kylinos.cn Signed-off-by: Rafael J. Wysocki --- drivers/cpuidle/governors/teo.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/drivers/cpuidle/governors/teo.c b/drivers/cpuidle/governors/teo.c index ac43b9b013b3..906f4f89e365 100644 --- a/drivers/cpuidle/governors/teo.c +++ b/drivers/cpuidle/governors/teo.c @@ -426,6 +426,17 @@ static int teo_select(struct cpuidle_driver *drv, struct cpuidle_device *dev, } } + /* + * If the latency constraint does not allow any of the enabled idle + * states to be used, the candidate state index will be capped to 0 + * by the check below, but state 0 may be disabled. To prevent the + * selection of a disabled state in that case, ensure that + * constraint_idx is at least equal to the index of the first + * enabled idle state. + */ + if (constraint_idx < idx0) + constraint_idx = idx0; + /* * If there is a latency constraint, it may be necessary to select an * idle state shallower than the current candidate one. From ca33550bf0f4feedfe71e75d0dd12546770d11f4 Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 7 Sep 2026 12:17:24 +0530 Subject: [PATCH 14/89] cpuidle: tegra: fix repeated word 'that' in comment Drop the second 'that', reported by checkpatch.pl as a possible repeated word. Only touches a comment, no code changes. Signed-off-by: Hemanth Selam [ rjw: Subject adjustment ] Link: https://patch.msgid.link/20260907064725.10686-5-hemanth.selam@gmail.com Signed-off-by: Rafael J. Wysocki --- drivers/cpuidle/cpuidle-tegra.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/cpuidle/cpuidle-tegra.c b/drivers/cpuidle/cpuidle-tegra.c index aca907a62bb5..d2fc216c38fc 100644 --- a/drivers/cpuidle/cpuidle-tegra.c +++ b/drivers/cpuidle/cpuidle-tegra.c @@ -281,7 +281,7 @@ static int tegra114_enter_s2idle(struct cpuidle_device *dev, * LP2 | C7 (CPU core power gating) * LP2 | CC6 (CPU cluster power gating) * - * Note that that the older CPUIDLE driver versions didn't explicitly + * Note that the older CPUIDLE driver versions didn't explicitly * differentiate the LP2 states because these states either used the same * code path or because CC6 wasn't supported. */ From 3b8650f2e8810f0baa48608c8dfe4f44120de123 Mon Sep 17 00:00:00 2001 From: Li Jun Date: Thu, 27 Aug 2026 20:32:56 +0800 Subject: [PATCH 15/89] PM: hibernate: docs: update swsusp.txt references to swsusp.rst The swsusp.txt documentation file has been renamed to swsusp.rst. Update the references in the Documentation/power/userland-swsusp.rst comments to use the correct filename. Signed-off-by: Li Jun [ rjw: Subject adjustment ] Link: https://patch.msgid.link/20260827123256.2548946-1-lijun01@kylinos.cn Signed-off-by: Rafael J. Wysocki --- Documentation/power/userland-swsusp.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/power/userland-swsusp.rst b/Documentation/power/userland-swsusp.rst index 1cf62d80a9ca..b00b850148b2 100644 --- a/Documentation/power/userland-swsusp.rst +++ b/Documentation/power/userland-swsusp.rst @@ -4,9 +4,9 @@ Documentation for userland software suspend interface (C) 2006 Rafael J. Wysocki -First, the warnings at the beginning of swsusp.txt still apply. +First, the warnings at the beginning of swsusp.rst still apply. -Second, you should read the FAQ in swsusp.txt _now_ if you have not +Second, you should read the FAQ in swsusp.rst _now_ if you have not done it already. Now, to use the userland interface for software suspend you need special From be5755f023e7e02ec90bbd1d4ca63a0abe594733 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Mon, 31 Aug 2026 18:23:22 -0700 Subject: [PATCH 16/89] PM: runtime: Only queue an idle check for RPM-linked suppliers When a device RPM-suspends, it queues up an idle check for all of its suppliers, in case that device was the last consumer, and those suppliers are now able to suspend. Today, we do this for all suppliers, and not only for those suppliers that are marked for use by runtime PM. This isn't directly harmful, but it is a bit wasteful, and also may induce unexpected suspend attempts (e.g., if a device was purposely last touched with pm_runtime_put_noidle()). To avoid excess idle checks, only call pm_request_idle() on linked suppliers that opted into runtime PM. Noticed by inspection of trace logs. This is not expected to have a functional impact on systems that are using device links properly, and should only be considered an optimization. Fixes: 5244f5e2d801 ("PM: runtime: Defer suspending suppliers") Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260901013341.1417232-1-briannorris@chromium.org Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c index fab38bc98113..4d84d97f09c4 100644 --- a/drivers/base/power/runtime.c +++ b/drivers/base/power/runtime.c @@ -361,7 +361,8 @@ static void rpm_suspend_suppliers(struct device *dev) list_for_each_entry_rcu(link, &dev->links.suppliers, c_node, device_links_read_lock_held()) - pm_request_idle(link->supplier); + if (device_link_test(link, DL_FLAG_PM_RUNTIME)) + pm_request_idle(link->supplier); device_links_read_unlock(idx); } From e7572e381d049daabc236f5a12c67adcaa117791 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Mon, 31 Aug 2026 18:23:23 -0700 Subject: [PATCH 17/89] PM: runtime: Add kunit test for supplier idle/suspend Add a unit test that covers the bugfix/optimization in the previous patch, "PM: runtime: Only queue an idle check for RPM-linked suppliers". This test ensures: 1) device links with DL_FLAG_PM_RUNTIME can RPM-resume/suspend properly; and 2) device links without DL_FLAG_PM_RUNTIME are not accidentally resumed/suspended along with the consumer. Notably, without patch 1 ("PM: runtime: Only queue an idle check for RPM-linked suppliers"), the no-RPM supplier will fail 2 of its last 3 test expectations: $ tools/testing/kunit/kunit.py run --kconfig_add CONFIG_PM=y 'pm_runtime*' ... ## Fails because rpm_suspend_suppliers() suspended the wrong links [17:01:44] # pm_runtime_supplier_suspend_test: EXPECTATION FAILED at drivers/base/power/runtime-test.c:279 [17:01:44] Expected pm_runtime_active(norpm_supplier) to be true, but is false ## Fails because the no-RPM supplier is already suspended unexpectedly [17:01:44] # pm_runtime_supplier_suspend_test: EXPECTATION FAILED at drivers/base/power/runtime-test.c:282 [17:01:44] Expected 0 == pm_runtime_suspend(norpm_supplier), but [17:01:44] pm_runtime_suspend(norpm_supplier) == 1 (0x1) ... Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260901013341.1417232-2-briannorris@chromium.org Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime-test.c | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/drivers/base/power/runtime-test.c b/drivers/base/power/runtime-test.c index 1535ad2b0264..24865ce844fc 100644 --- a/drivers/base/power/runtime-test.c +++ b/drivers/base/power/runtime-test.c @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -229,6 +230,61 @@ static void pm_runtime_probe_active_test(struct kunit *test) KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(dev)); } +static void pm_runtime_supplier_suspend_test(struct kunit *test) +{ + struct device *dev = kunit_device_register(test, DEVICE_NAME); + struct device *supplier = kunit_device_register(test, DEVICE_NAME "_supplier"); + struct device *norpm_supplier = kunit_device_register(test, DEVICE_NAME "_norpm_supplier"); + + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, supplier); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, norpm_supplier); + + KUNIT_ASSERT_NOT_NULL(test, device_link_add(dev, supplier, DL_FLAG_PM_RUNTIME)); + KUNIT_ASSERT_NOT_NULL(test, device_link_add(dev, norpm_supplier, 0)); + + pm_runtime_enable(dev); + pm_runtime_enable(supplier); + pm_runtime_enable(norpm_supplier); + + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(dev)); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(supplier)); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(norpm_supplier)); + + /* + * Resume the device and RPM-linked supplier; non-RPM-linked supplier + * stays suspended. + */ + KUNIT_EXPECT_EQ(test, 0, pm_runtime_get_sync(dev)); + KUNIT_EXPECT_TRUE(test, pm_runtime_active(dev)); + KUNIT_EXPECT_TRUE(test, pm_runtime_active(supplier)); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(norpm_supplier)); + + /* Also resume the non-RPM-linked supplier. */ + KUNIT_EXPECT_EQ(test, 0, pm_runtime_get_sync(norpm_supplier)); + KUNIT_EXPECT_TRUE(test, pm_runtime_active(norpm_supplier)); + + pm_runtime_put_noidle(norpm_supplier); + KUNIT_EXPECT_TRUE(test, pm_runtime_active(norpm_supplier)); + + /* + * Suspend device. This should only suspend the device and its + * RPM-linked supplier, not the non-RPM-linked supplier. + */ + KUNIT_EXPECT_EQ(test, 0, pm_runtime_put_sync(dev)); + /* Note: supplier suspend is async, so we have to flush the queue. */ + flush_workqueue(pm_wq); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(dev)); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(supplier)); + KUNIT_EXPECT_TRUE(test, pm_runtime_active(norpm_supplier)); + + /* Now suspend non-RPM-linked supplier. */ + KUNIT_EXPECT_EQ(test, 0, pm_runtime_suspend(norpm_supplier)); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(dev)); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(supplier)); + KUNIT_EXPECT_TRUE(test, pm_runtime_suspended(norpm_supplier)); +} + static struct kunit_case pm_runtime_test_cases[] = { KUNIT_CASE(pm_runtime_depth_test), KUNIT_CASE(pm_runtime_already_suspended_test), @@ -236,6 +292,7 @@ static struct kunit_case pm_runtime_test_cases[] = { KUNIT_CASE(pm_runtime_disabled_test), KUNIT_CASE(pm_runtime_error_test), KUNIT_CASE(pm_runtime_probe_active_test), + KUNIT_CASE(pm_runtime_supplier_suspend_test), {} }; From dbdd18404e33ed44436bb9294fe8f2609b93af03 Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Fri, 4 Sep 2026 17:23:50 +0530 Subject: [PATCH 18/89] tools: power: pm-graph: fix typo "hierachy" in comment Correct "hierachy" to "hierarchy", reported by scripts/checkpatch.pl using the misspelling list in scripts/spelling.txt. Only touches comments, no code changes. Signed-off-by: Hemanth Selam [ rjw: Subject adjustment ] Link: https://patch.msgid.link/20260904115351.9356-1-hemanth.selam@gmail.com Signed-off-by: Rafael J. Wysocki --- tools/power/pm-graph/sleepgraph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/power/pm-graph/sleepgraph.py b/tools/power/pm-graph/sleepgraph.py index f6d172254829..c8bc3bed3f5a 100755 --- a/tools/power/pm-graph/sleepgraph.py +++ b/tools/power/pm-graph/sleepgraph.py @@ -1448,7 +1448,7 @@ class DevProps: # Class: DeviceNode # Description: -# A container used to create a device hierachy, with a single root node +# A container used to create a device hierarchy, with a single root node # and a tree of child nodes. Used by Data.deviceTopology() class DeviceNode: def __init__(self, nodename, nodedepth): From 0fa55c5539173bceaeb400c69b1974aaa0cdb8dc Mon Sep 17 00:00:00 2001 From: Shengming Hu Date: Wed, 16 Sep 2026 14:09:26 +0800 Subject: [PATCH 19/89] cpufreq: conservative: Restrict deferred downscaling to low load For a shared cpufreq policy, dbs_update() derives the load used for the frequency decision from the maximum load among its CPUs, but it may also record deferred idle periods accumulated by an idle CPU in the policy. Consequently, a single update can contain both a high decision load from one CPU and multiple deferred idle periods from another CPU. The conservative governor applies the deferred down steps before evaluating the decision load. If that load subsequently triggers an up step, the deferred down steps can outweigh the single up step and produce a net frequency reduction. This was observed on a policy shared by CPUs 2 and 3. A CPU-bound SCHED_EXT task kept CPU 2 fully utilized while CPU 3 remained idle. On this system, SCHED_EXT generated update-util callbacks less frequently than CFS, resulting in sparse DBS updates such as: load=100 idle_periods=7 interval=59 ms load=100 idle_periods=4 interval=39 ms load=100 idle_periods=2 interval=19 ms load=100 idle_periods=7 interval=59 ms With the default 5% frequency step and a 2.6 GHz policy maximum, seven deferred periods reduce the requested frequency by seven 130 MHz steps before the high decision load adds only one step. Repeated updates therefore keep the policy near 530 MHz even though CPU 2 is fully utilized. There are two load values relevant to this behavior: * sample_load is calculated from the busy and elapsed time of the current sample. * decision_load is returned by dbs_update() for the governor's frequency decision. After a long idle interval, it may reuse prev_load to improve the response to a waking task and can therefore differ from sample_load. The problem is that idle_periods is derived from accumulated idle time and is applied independently of whether the current sample load is in the conservative governor's downscaling region. A high sample load and multiple deferred idle periods can therefore coexist, particularly when updates are sparse. As suggested by Zhongqiu Han, calculate the sample load separately from the decision load. If no time has elapsed, use prev_load for the sample load as the existing load calculation does, since no load can be calculated for that interval. Record the maximum sample load for the policy and apply deferred down steps only when it is below down_threshold. This makes deferred downscaling follow the same threshold that the conservative governor normally uses to determine whether the policy load is low enough to reduce the frequency. The existing code applies deferred down steps whenever idle_periods is valid, regardless of the sample load. When the decision load is in the hold region, the locally adjusted frequency is not submitted unless another frequency-update branch is taken. Thus, when the sample and decision loads are the same and both are in the hold region, this change does not alter the requested frequency. The difference becomes visible when the decision load causes a frequency update. In particular, if it exceeds up_threshold, the governor applies a single up step after the deferred down steps and submits the resulting request. Multiple deferred down steps can then outweigh that up step and cause a frequency reduction even when the sample load is above down_threshold. With this change, deferred down steps are skipped whenever the maximum sample load is at or above down_threshold, keeping deferred downscaling consistent with the conservative governor's threshold semantics. The requested frequency may therefore be higher than with the existing behavior when deferred idle periods are present and the decision load causes a frequency update. This is intentional, because the lower request previously resulted from applying deferred downscaling while the sampled policy load was outside the governor's downscaling region. Accelerated deferred downscaling remains available when the policy sample load is below down_threshold, including after a workload has completed and the policy has become idle. Fixes: 00bfe05889e9 ("cpufreq: conservative: Decrease frequency faster for deferred updates") Cc: All applicable Reviewed-by: Luo Haiyang Reviewed-by: Run Zhang Suggested-by: Zhongqiu Han Signed-off-by: Shengming Hu Link: https://patch.msgid.link/20260916140926740-DCokXh6dAgtqDn1EWdCh@zte.com.cn Signed-off-by: Rafael J. Wysocki --- drivers/cpufreq/cpufreq_conservative.c | 8 ++++--- drivers/cpufreq/cpufreq_governor.c | 29 +++++++++++++++++++------- drivers/cpufreq/cpufreq_governor.h | 2 ++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/drivers/cpufreq/cpufreq_conservative.c b/drivers/cpufreq/cpufreq_conservative.c index 0b32ae28ec85..02bfd46543e9 100644 --- a/drivers/cpufreq/cpufreq_conservative.c +++ b/drivers/cpufreq/cpufreq_conservative.c @@ -85,10 +85,12 @@ static unsigned int cs_dbs_update(struct cpufreq_policy *policy) freq_step = get_freq_step(cs_tuners, policy); /* - * Decrease requested_freq one freq_step for each idle period that - * we didn't update the frequency. + * Apply deferred down steps only when the policy sample load is + * below down_threshold. Otherwise, multiple deferred down steps may + * cause a net frequency decrease outside the downscaling region. */ - if (policy_dbs->idle_periods < UINT_MAX) { + if (policy_dbs->max_sample_load < cs_tuners->down_threshold && + policy_dbs->idle_periods < UINT_MAX) { unsigned int freq_steps = policy_dbs->idle_periods * freq_step; if (requested_freq > policy->min + freq_steps) diff --git a/drivers/cpufreq/cpufreq_governor.c b/drivers/cpufreq/cpufreq_governor.c index 710d93ec89b5..f3bd3dbe7c41 100644 --- a/drivers/cpufreq/cpufreq_governor.c +++ b/drivers/cpufreq/cpufreq_governor.c @@ -124,7 +124,8 @@ unsigned int dbs_update(struct cpufreq_policy *policy) struct policy_dbs_info *policy_dbs = policy->governor_data; struct dbs_data *dbs_data = policy_dbs->dbs_data; unsigned int ignore_nice = dbs_data->ignore_nice_load; - unsigned int max_load = 0, idle_periods = UINT_MAX; + unsigned int max_load = 0, max_sample_load = 0; + unsigned int idle_periods = UINT_MAX; unsigned int sampling_rate, io_busy, j; u64 cur_nice; @@ -147,7 +148,7 @@ unsigned int dbs_update(struct cpufreq_policy *policy) struct cpu_dbs_info *j_cdbs = &per_cpu(cpu_dbs, j); u64 update_time, cur_idle_time; unsigned int idle_time, time_elapsed; - unsigned int load; + unsigned int load, sample_load; cur_idle_time = get_cpu_idle_time(j, &update_time, io_busy); @@ -186,6 +187,20 @@ unsigned int dbs_update(struct cpufreq_policy *policy) j_cdbs->prev_cpu_nice = cur_nice; + /* + * Compute the sample load separately from the prev_load value + * that may be reused after a long idle interval. The conservative + * governor uses it to decide whether to apply deferred down steps. + * If no time has elapsed, retain the existing behavior and use + * prev_load. + */ + if (unlikely(!time_elapsed)) + sample_load = j_cdbs->prev_load; + else if (time_elapsed > idle_time) + sample_load = 100 * (time_elapsed - idle_time) / time_elapsed; + else + sample_load = 0; + if (unlikely(!time_elapsed)) { /* * That can only happen when this function is called @@ -220,11 +235,7 @@ unsigned int dbs_update(struct cpufreq_policy *policy) load = j_cdbs->prev_load; j_cdbs->prev_load = 0; } else { - if (time_elapsed > idle_time) - load = 100 * (time_elapsed - idle_time) / time_elapsed; - else - load = 0; - + load = sample_load; j_cdbs->prev_load = load; } @@ -237,9 +248,13 @@ unsigned int dbs_update(struct cpufreq_policy *policy) if (load > max_load) max_load = load; + + if (sample_load > max_sample_load) + max_sample_load = sample_load; } policy_dbs->idle_periods = idle_periods; + policy_dbs->max_sample_load = max_sample_load; return max_load; } diff --git a/drivers/cpufreq/cpufreq_governor.h b/drivers/cpufreq/cpufreq_governor.h index 73b8ed7cfaae..806d8fb4dff1 100644 --- a/drivers/cpufreq/cpufreq_governor.h +++ b/drivers/cpufreq/cpufreq_governor.h @@ -95,6 +95,8 @@ struct policy_dbs_info { /* Multiplier for increasing sample delay temporarily. */ unsigned int rate_mult; unsigned int idle_periods; /* For conservative */ + /* Maximum load from the current policy sample. */ + unsigned int max_sample_load; /* Status indicators */ bool is_shared; /* This object is used by multiple CPUs */ bool work_in_progress; /* Work is being queued up or in progress */ From b5aa69375d2fb46ca9c3cd43ecd843ffbce38be7 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Fri, 4 Sep 2026 14:12:06 -0700 Subject: [PATCH 20/89] PM: runtime: kerneldoc fixes When included into a Documentation/.../*.rst file, `make htmldocs` complains: ./include/linux/pm_runtime.h:359: WARNING: Bullet list ends without a blank line; unexpected unindent. [docutils] [... more ...] We should fix up the list format here to look nicer in HTML form, and avoid warnings. Adjust to a few other kerneldoc-isms (formatting, "Return:") while we're at it too. The result now passes 'make htmldocs' without warning, once these files are included in Documentation/.../*.rst. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260904141215.1.Ib31d6be7e93f6c02321d03c78ea410e02d7380fc@changeid Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime.c | 24 ++- include/linux/pm_runtime.h | 291 ++++++++++++++++++++--------------- 2 files changed, 183 insertions(+), 132 deletions(-) diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c index 4d84d97f09c4..29e5ce69d04c 100644 --- a/drivers/base/power/runtime.c +++ b/drivers/base/power/runtime.c @@ -162,7 +162,7 @@ static void pm_runtime_cancel_pending(struct device *dev) dev->power.request = RPM_REQ_NONE; } -/* +/** * pm_runtime_autosuspend_expiration - Get a device's autosuspend-delay expiration time. * @dev: Device to handle. * @@ -200,7 +200,7 @@ static int dev_memalloc_noio(struct device *dev, void *data) return dev->power.memalloc_noio; } -/* +/** * pm_runtime_set_memalloc_noio - Set a device's memalloc_noio flag. * @dev: Device to handle. * @enable: True for setting the flag and False for clearing the flag. @@ -1044,6 +1044,11 @@ static enum hrtimer_restart pm_suspend_timer_fn(struct hrtimer *timer) * pm_schedule_suspend - Set up a timer to submit a suspend request in future. * @dev: Device to suspend. * @delay: Time to wait before submitting a suspend request, in milliseconds. + * + * Return: + * * %1: Success; @dev is already %RPM_SUSPENDED. + * * %0: Success. + * * Error code on failure. */ int pm_schedule_suspend(struct device *dev, unsigned int delay) { @@ -1106,7 +1111,7 @@ static int rpm_drop_usage_count(struct device *dev) * warning, increment it, and return an error). Then carry out an idle * notification, either synchronous or asynchronous. * - * This routine may be called in atomic context if the RPM_ASYNC flag is set, + * This routine may be called in atomic context if the %RPM_ASYNC flag is set, * or if pm_runtime_irq_safe() has been called. */ int __pm_runtime_idle(struct device *dev, int rpmflags) @@ -1144,7 +1149,7 @@ EXPORT_SYMBOL_GPL(__pm_runtime_idle); * warning, increment it, and return an error). Then carry out a suspend, * either synchronous or asynchronous. * - * This routine may be called in atomic context if the RPM_ASYNC flag is set, + * This routine may be called in atomic context if the %RPM_ASYNC flag is set, * or if pm_runtime_irq_safe() has been called. */ int __pm_runtime_suspend(struct device *dev, int rpmflags) @@ -1180,7 +1185,7 @@ EXPORT_SYMBOL_GPL(__pm_runtime_suspend); * If the RPM_GET_PUT flag is set, increment the device's usage count. Then * carry out a resume, either synchronous or asynchronous. * - * This routine may be called in atomic context if the RPM_ASYNC flag is set, + * This routine may be called in atomic context if the %RPM_ASYNC flag is set, * or if pm_runtime_irq_safe() has been called. */ int __pm_runtime_resume(struct device *dev, int rpmflags) @@ -1255,9 +1260,12 @@ static int pm_runtime_get_conditional(struct device *dev, bool ign_usage_count) * @dev: Target device. * * Increment the runtime PM usage counter of @dev if its runtime PM status is - * %RPM_ACTIVE, in which case it returns 1. If the device is in a different - * state, 0 is returned. -EINVAL is returned if runtime PM is disabled for the - * device, in which case also the usage_count will remain unmodified. + * already %RPM_ACTIVE + * + * Return: + * * %-EINVAL: Runtime PM is disabled for @dev. The usage counter is not incremented. + * * %1: Success; usage counter is incremented. + * * %0: @dev was not active. */ int pm_runtime_get_if_active(struct device *dev) { diff --git a/include/linux/pm_runtime.h b/include/linux/pm_runtime.h index 64921b10ac74..ab6a19a85880 100644 --- a/include/linux/pm_runtime.h +++ b/include/linux/pm_runtime.h @@ -137,13 +137,14 @@ static inline void pm_runtime_put_noidle(struct device *dev) * pm_runtime_suspended - Check whether or not a device is runtime-suspended. * @dev: Target device. * - * Return %true if runtime PM is enabled for @dev and its runtime PM status is - * %RPM_SUSPENDED, or %false otherwise. - * * Note that the return value of this function can only be trusted if it is * called under the runtime PM lock of @dev or under conditions in which * runtime PM cannot be either disabled or enabled for @dev and its runtime PM * status cannot change. + * + * Return: + * * %true: @dev has runtime PM enabled and its status is %RPM_SUSPENDED. + * * %false: Otherwise. */ static inline bool pm_runtime_suspended(struct device *dev) { @@ -155,13 +156,14 @@ static inline bool pm_runtime_suspended(struct device *dev) * pm_runtime_active - Check whether or not a device is runtime-active. * @dev: Target device. * - * Return %true if runtime PM is disabled for @dev or its runtime PM status is - * %RPM_ACTIVE, or %false otherwise. - * * Note that the return value of this function can only be trusted if it is * called under the runtime PM lock of @dev or under conditions in which * runtime PM cannot be either disabled or enabled for @dev and its runtime PM * status cannot change. + * + * Return: + * * %true: Runtime PM is disabled for @dev or its status is %RPM_ACTIVE. + * * %false: Otherwise. */ static inline bool pm_runtime_active(struct device *dev) { @@ -173,12 +175,13 @@ static inline bool pm_runtime_active(struct device *dev) * pm_runtime_status_suspended - Check if runtime PM status is "suspended". * @dev: Target device. * - * Return %true if the runtime PM status of @dev is %RPM_SUSPENDED, or %false - * otherwise, regardless of whether or not runtime PM has been enabled for @dev. - * * Note that the return value of this function can only be trusted if it is * called under the runtime PM lock of @dev or under conditions in which the * runtime PM status of @dev cannot change. + * + * Return: + * * %true: Runtime PM status of @dev is %RPM_SUSPENDED. + * * %false: Otherwise. */ static inline bool pm_runtime_status_suspended(struct device *dev) { @@ -189,11 +192,13 @@ static inline bool pm_runtime_status_suspended(struct device *dev) * pm_runtime_enabled - Check if runtime PM is enabled. * @dev: Target device. * - * Return %true if runtime PM is enabled for @dev or %false otherwise. - * * Note that the return value of this function can only be trusted if it is * called under the runtime PM lock of @dev or under conditions in which * runtime PM cannot be either disabled or enabled for @dev. + * + * Return: + * * %true: Runtime PM is enabled for @dev. + * * %false: Otherwise. */ static inline bool pm_runtime_enabled(struct device *dev) { @@ -205,6 +210,10 @@ static inline bool pm_runtime_enabled(struct device *dev) * @dev: Target device. * * Do not call this function outside system suspend/resume code paths. + * + * Return: + * * %true: Runtime PM enabling is blocked for @dev. + * * %false: Otherwise. */ static inline bool pm_runtime_blocked(struct device *dev) { @@ -215,8 +224,9 @@ static inline bool pm_runtime_blocked(struct device *dev) * pm_runtime_has_no_callbacks - Check if runtime PM callbacks may be present. * @dev: Target device. * - * Return %true if @dev is a special device without runtime PM callbacks or - * %false otherwise. + * Return: + * * %true: @dev is marked as having no runtime PM callbacks. + * * %false: Otherwise. */ static inline bool pm_runtime_has_no_callbacks(struct device *dev) { @@ -239,9 +249,11 @@ static inline void pm_runtime_mark_last_busy(struct device *dev) * pm_runtime_is_irq_safe - Check if runtime PM can work in interrupt context. * @dev: Target device. * - * Return %true if @dev has been marked as an "IRQ-safe" device (with respect - * to runtime PM), in which case its runtime PM callabcks can be expected to - * work correctly when invoked from interrupt handlers. + * Return: + * * %true: @dev has been marked as an "IRQ-safe" device, in which case its + * runtime PM callbacks can be expected to work correctly from interrupt + * handlers. + * * %false: Otherwise. */ static inline bool pm_runtime_is_irq_safe(struct device *dev) { @@ -348,17 +360,17 @@ static inline int pm_runtime_force_resume(struct device *dev) { return -ENXIO; } * autosuspend has been enabled for it). * * Return: - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter non-zero, Runtime PM status change - * ongoing or device not in %RPM_ACTIVE state. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -EINPROGRESS: Suspend already in progress. - * * -ENOSYS: CONFIG_PM not enabled. - * Other values and conditions for the above values are possible as returned by - * Runtime PM idle and suspend callbacks. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter non-zero, Runtime PM status change + * ongoing or device not in %RPM_ACTIVE state. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-EINPROGRESS: Suspend already in progress. + * * %-ENOSYS: %CONFIG_PM not enabled. + * * Other values and conditions for the above values are possible as returned + * by Runtime PM idle and suspend callbacks. */ static inline int pm_runtime_idle(struct device *dev) { @@ -370,17 +382,17 @@ static inline int pm_runtime_idle(struct device *dev) * @dev: Target device. * * Return: - * * 1: Success; device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter non-zero or Runtime PM status change - * ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -ENOSYS: CONFIG_PM not enabled. - * Other values and conditions for the above values are possible as returned by - * Runtime PM suspend callbacks. + * * %1: Success; device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter non-zero or Runtime PM status change + * ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-ENOSYS: %CONFIG_PM not enabled. + * * Other values and conditions for the above values are possible as returned + * by Runtime PM suspend callbacks. */ static inline int pm_runtime_suspend(struct device *dev) { @@ -397,17 +409,17 @@ static inline int pm_runtime_suspend(struct device *dev) * engaging its "idle check" callback. * * Return: - * * 1: Success; device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter non-zero or Runtime PM status change - * ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -ENOSYS: CONFIG_PM not enabled. - * Other values and conditions for the above values are possible as returned by - * Runtime PM suspend callbacks. + * * %1: Success; device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter non-zero or Runtime PM status change + * ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-ENOSYS: %CONFIG_PM not enabled. + * * Other values and conditions for the above values are possible as returned + * by Runtime PM suspend callbacks. */ static inline int pm_runtime_autosuspend(struct device *dev) { @@ -418,6 +430,11 @@ static inline int pm_runtime_autosuspend(struct device *dev) /** * pm_runtime_resume - Resume a device synchronously. * @dev: Target device. + * + * Return: + * * %1: Success; @dev is already %RPM_ACTIVE. + * * %0: Success. + * * Error code on failure. */ static inline int pm_runtime_resume(struct device *dev) { @@ -432,15 +449,15 @@ static inline int pm_runtime_resume(struct device *dev) * asynchronously. * * Return: - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter non-zero, Runtime PM status change - * ongoing or device not in %RPM_ACTIVE state. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -EINPROGRESS: Suspend already in progress. - * * -ENOSYS: CONFIG_PM not enabled. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter non-zero, Runtime PM status change + * ongoing or device not in %RPM_ACTIVE state. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-EINPROGRESS: Suspend already in progress. + * * %-ENOSYS: %CONFIG_PM not enabled. */ static inline int pm_request_idle(struct device *dev) { @@ -450,6 +467,11 @@ static inline int pm_request_idle(struct device *dev) /** * pm_request_resume - Queue up runtime-resume of a device. * @dev: Target device. + * + * Return: + * * %1: Success; @dev is already %RPM_ACTIVE. + * * %0: Success. + * * Error code on failure. */ static inline int pm_request_resume(struct device *dev) { @@ -465,16 +487,16 @@ static inline int pm_request_resume(struct device *dev) * equivalent pm_runtime_autosuspend() for @dev asynchronously. * * Return: - * * 1: Success; device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter non-zero or Runtime PM status change - * ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -EINPROGRESS: Suspend already in progress. - * * -ENOSYS: CONFIG_PM not enabled. + * * %1: Success; device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter non-zero or Runtime PM status change + * ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-EINPROGRESS: Suspend already in progress. + * * %-ENOSYS: %CONFIG_PM not enabled. */ static inline int pm_request_autosuspend(struct device *dev) { @@ -488,6 +510,11 @@ static inline int pm_request_autosuspend(struct device *dev) * * Bump up the runtime PM usage counter of @dev and queue up a work item to * carry out runtime-resume of it. + * + * Return: + * * %1: Success; @dev is already %RPM_ACTIVE. + * * %0: Success; runtime-resume was queued. + * * Error code on failure. */ static inline int pm_runtime_get(struct device *dev) { @@ -507,6 +534,11 @@ static inline int pm_runtime_get(struct device *dev) * Consider using pm_runtime_resume_and_get() instead of it, especially * if its return value is checked by the caller, as this is likely to result * in cleaner code. + * + * Return: + * * %1: Success; @dev is already %RPM_ACTIVE. + * * %0: Success. + * * Error code on failure. */ static inline int pm_runtime_get_sync(struct device *dev) { @@ -531,8 +563,11 @@ static inline int pm_runtime_get_active(struct device *dev, int rpmflags) * @dev: Target device. * * Resume @dev synchronously and if that is successful, increment its runtime - * PM usage counter. Return 0 if the runtime PM usage counter of @dev has been - * incremented or a negative error code otherwise. + * PM usage counter. + * + * Return: + * * %0: Success; @dev is active and its usage counter has been incremented. + * * Negative error code on failure; usage counter is unchanged. */ static inline int pm_runtime_resume_and_get(struct device *dev) { @@ -559,16 +594,16 @@ static inline void pm_runtime_put(struct device *dev) * equal to 0, queue up a work item for @dev like in pm_request_autosuspend(). * * Return: - * * 1: Success. Usage counter dropped to zero, but device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status - * change ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -EINPROGRESS: Suspend already in progress. - * * -ENOSYS: CONFIG_PM not enabled. + * * %1: Success. Usage counter dropped to zero, but device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status + * change ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-EINPROGRESS: Suspend already in progress. + * * %-ENOSYS: %CONFIG_PM not enabled. */ static inline int __pm_runtime_put_autosuspend(struct device *dev) { @@ -585,16 +620,16 @@ static inline int __pm_runtime_put_autosuspend(struct device *dev) * in pm_request_autosuspend(). * * Return: - * * 1: Success. Usage counter dropped to zero, but device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status - * change ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -EINPROGRESS: Suspend already in progress. - * * -ENOSYS: CONFIG_PM not enabled. + * * %1: Success. Usage counter dropped to zero, but device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status + * change ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-EINPROGRESS: Suspend already in progress. + * * %-ENOSYS: %CONFIG_PM not enabled. */ static inline int pm_runtime_put_autosuspend(struct device *dev) { @@ -662,17 +697,17 @@ DEFINE_GUARD_COND(pm_runtime_active_auto, _try_enabled, * if it returns an error code. * * Return: - * * 1: Success. Usage counter dropped to zero, but device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status - * change ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -ENOSYS: CONFIG_PM not enabled. - * Other values and conditions for the above values are possible as returned by - * Runtime PM suspend callbacks. + * * %1: Success. Usage counter dropped to zero, but device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status + * change ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-ENOSYS: %CONFIG_PM not enabled. + * * Other values and conditions for the above values are possible as returned + * by Runtime PM suspend callbacks. */ static inline int pm_runtime_put_sync(struct device *dev) { @@ -690,17 +725,17 @@ static inline int pm_runtime_put_sync(struct device *dev) * if it returns an error code. * * Return: - * * 1: Success. Usage counter dropped to zero, but device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status - * change ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -ENOSYS: CONFIG_PM not enabled. - * Other values and conditions for the above values are possible as returned by - * Runtime PM suspend callbacks. + * * %1: Success. Usage counter dropped to zero, but device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status + * change ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-ENOSYS: %CONFIG_PM not enabled. + * * Other values and conditions for the above values are possible as returned + * by Runtime PM suspend callbacks. */ static inline int pm_runtime_put_sync_suspend(struct device *dev) { @@ -721,18 +756,18 @@ static inline int pm_runtime_put_sync_suspend(struct device *dev) * if it returns an error code. * * Return: - * * 1: Success. Usage counter dropped to zero, but device was already suspended. - * * 0: Success. - * * -EINVAL: Runtime PM error. - * * -EACCES: Runtime PM disabled. - * * -EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status - * change ongoing. - * * -EBUSY: Runtime PM child_count non-zero. - * * -EPERM: Device PM QoS resume latency 0. - * * -EINPROGRESS: Suspend already in progress. - * * -ENOSYS: CONFIG_PM not enabled. - * Other values and conditions for the above values are possible as returned by - * Runtime PM suspend callbacks. + * * %1: Success. Usage counter dropped to zero, but device was already suspended. + * * %0: Success. + * * %-EINVAL: Runtime PM error. + * * %-EACCES: Runtime PM disabled. + * * %-EAGAIN: Runtime PM usage counter became non-zero or Runtime PM status + * change ongoing. + * * %-EBUSY: Runtime PM child_count non-zero. + * * %-EPERM: Device PM QoS resume latency 0. + * * %-EINPROGRESS: Suspend already in progress. + * * %-ENOSYS: %CONFIG_PM not enabled. + * * Other values and conditions for the above values are possible as returned + * by Runtime PM suspend callbacks. */ static inline int pm_runtime_put_sync_autosuspend(struct device *dev) { @@ -748,6 +783,10 @@ static inline int pm_runtime_put_sync_autosuspend(struct device *dev) * of it will be taken into account. * * It is not valid to call this function for devices with runtime PM enabled. + * + * Return: + * * %0: Success. + * * Error code on failure. */ static inline int pm_runtime_set_active(struct device *dev) { @@ -762,6 +801,10 @@ static inline int pm_runtime_set_active(struct device *dev) * dependencies of it will be taken into account. * * It is not valid to call this function for devices with runtime PM enabled. + * + * Return: + * * %0: Success. + * * Error code on failure. */ static inline int pm_runtime_set_suspended(struct device *dev) { From 4711357dd298594beac51de081d9ba4326a189e3 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Fri, 4 Sep 2026 14:12:07 -0700 Subject: [PATCH 21/89] PM: runtime: Improve set_{status,active,suspended} docs The set_active()/set_suspended() docs don't mention that they also clear the 'runtime_error' field. This is a very important note, since that's one key purpose for using them. Fix a typo in __pm_runtime_set_status() while we're at it. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260904141215.2.If02a538531ea004987c9a28a8d030ee9f71221cd@changeid Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime.c | 4 ++-- include/linux/pm_runtime.h | 23 +++++++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c index 29e5ce69d04c..3c874b056af5 100644 --- a/drivers/base/power/runtime.c +++ b/drivers/base/power/runtime.c @@ -1296,7 +1296,7 @@ int pm_runtime_get_if_in_use(struct device *dev) EXPORT_SYMBOL_GPL(pm_runtime_get_if_in_use); /** - * __pm_runtime_set_status - Set runtime PM status of a device. + * __pm_runtime_set_status - Set runtime PM status of a device and clear errors. * @dev: Device to handle. * @status: New runtime PM status of the device. * @@ -1315,7 +1315,7 @@ EXPORT_SYMBOL_GPL(pm_runtime_get_if_in_use); * If @dev has any suppliers (as reflected by device links to them), and @status * is RPM_ACTIVE, they will be activated upfront and if the activation of one * of them fails, the status of @dev will be changed to RPM_SUSPENDED (instead - * of the @status value) and the suppliers will be deacticated on exit. The + * of the @status value) and the suppliers will be deactivated on exit. The * error returned by the failing supplier activation will be returned in that * case. */ diff --git a/include/linux/pm_runtime.h b/include/linux/pm_runtime.h index ab6a19a85880..1ffd9d5c3010 100644 --- a/include/linux/pm_runtime.h +++ b/include/linux/pm_runtime.h @@ -776,13 +776,18 @@ static inline int pm_runtime_put_sync_autosuspend(struct device *dev) } /** - * pm_runtime_set_active - Set runtime PM status to "active". + * pm_runtime_set_active - Set runtime PM status to "active" and clear errors. * @dev: Target device. * - * Set the runtime PM status of @dev to %RPM_ACTIVE and ensure that dependencies - * of it will be taken into account. + * Set the runtime PM status of @dev to %RPM_ACTIVE and ensure that its + * dependencies will be taken into account. Also clear the device's error + * status (@dev->power.runtime_error). * - * It is not valid to call this function for devices with runtime PM enabled. + * It is only valid to call this function if runtime PM is disabled or if + * @dev->power.runtime_error is set. + * + * This will fail if suppliers cannot be resumed, or if the parent is not in + * the correct state. * * Return: * * %0: Success. @@ -794,13 +799,15 @@ static inline int pm_runtime_set_active(struct device *dev) } /** - * pm_runtime_set_suspended - Set runtime PM status to "suspended". + * pm_runtime_set_suspended - Set runtime PM status to "suspended" and clear errors. * @dev: Target device. * - * Set the runtime PM status of @dev to %RPM_SUSPENDED and ensure that - * dependencies of it will be taken into account. + * Set the runtime PM status of @dev to %RPM_SUSPENDED and ensure that its + * dependencies will be taken into account. Also clear the device's error + * status (@dev->power.runtime_error). * - * It is not valid to call this function for devices with runtime PM enabled. + * It is only valid to call this function if runtime PM is disabled or if + * @dev->power.runtime_error is set. * * Return: * * %0: Success. From 3baf1652fde8e54754df0b8bc9a6014c1752383d Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Fri, 4 Sep 2026 14:12:08 -0700 Subject: [PATCH 22/89] PM: runtime: kerneldoc wording improvements In preparation for removing duplicate documentation from Documentation/power/runtime_pm.rst, borrow some of the useful wording from runtime_pm.rst, and update other language for clarity, ease of reading, and completeness. Other guiding principles in this change: * Try to highlight "core", as in, "functions that are not for driver use but are exported because the real entrypoints are inline functions" * Rework pm_runtime_barrier() docs significantly. More below. * Include some clarifying cross-references and recommendations for pm_runtime_put_sync{,_suspend,_autosuspend}() * Attempt to deemphasize some of the implementation details (e.g., "asynchronous" instead of "queue") * Try for more clear user-facing language. For example, "set up autosuspend" isn't quite clear whether we're configuring autosuspend, or if we're initiating an attempt to autosuspend (i.e., setting a timer). pm_runtime_barrier(): currently, we speak a lot about implementation details and sequences of events, but obscure the key point that it treats "pending resume" and "pending suspend" very differently -- I try to improve that. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260904141215.3.I3bdec7dcda46e1bee6d4f91530518fbe9c9096fd@changeid Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime.c | 65 +++++++++++++----------- include/linux/pm_runtime.h | 95 +++++++++++++++++++----------------- 2 files changed, 87 insertions(+), 73 deletions(-) diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c index 3c874b056af5..bb008dfe85a1 100644 --- a/drivers/base/power/runtime.c +++ b/drivers/base/power/runtime.c @@ -1102,14 +1102,13 @@ static int rpm_drop_usage_count(struct device *dev) } /** - * __pm_runtime_idle - Entry point for runtime idle operations. + * __pm_runtime_idle - Core entry point for runtime idle operations. * @dev: Device to send idle notification for. * @rpmflags: Flag bits. * - * If the RPM_GET_PUT flag is set, decrement the device's usage count and - * return immediately if it is larger than zero (if it becomes negative, log a - * warning, increment it, and return an error). Then carry out an idle - * notification, either synchronous or asynchronous. + * Carry out an idle check for @dev, either synchronous or asynchronous. + * If %RPM_GET_PUT is set in @rpmflags, decrement the device's usage count + * first, proceeding with idle notification only if the counter drops to zero. * * This routine may be called in atomic context if the %RPM_ASYNC flag is set, * or if pm_runtime_irq_safe() has been called. @@ -1140,14 +1139,13 @@ int __pm_runtime_idle(struct device *dev, int rpmflags) EXPORT_SYMBOL_GPL(__pm_runtime_idle); /** - * __pm_runtime_suspend - Entry point for runtime put/suspend operations. + * __pm_runtime_suspend - Core entry point for runtime put/suspend operations. * @dev: Device to suspend. * @rpmflags: Flag bits. * - * If the RPM_GET_PUT flag is set, decrement the device's usage count and - * return immediately if it is larger than zero (if it becomes negative, log a - * warning, increment it, and return an error). Then carry out a suspend, - * either synchronous or asynchronous. + * Carry out a suspend operation for @dev, either synchronous or asynchronous. + * If %RPM_GET_PUT is set in @rpmflags, decrement the device's usage count + * first, proceeding with suspend only if the counter drops to zero. * * This routine may be called in atomic context if the %RPM_ASYNC flag is set, * or if pm_runtime_irq_safe() has been called. @@ -1178,12 +1176,13 @@ int __pm_runtime_suspend(struct device *dev, int rpmflags) EXPORT_SYMBOL_GPL(__pm_runtime_suspend); /** - * __pm_runtime_resume - Entry point for runtime resume operations. + * __pm_runtime_resume - Core entry point for runtime resume operations. * @dev: Device to resume. * @rpmflags: Flag bits. * - * If the RPM_GET_PUT flag is set, increment the device's usage count. Then - * carry out a resume, either synchronous or asynchronous. + * Carry out a runtime resume operation for @dev, either synchronous or + * asynchronous. If %RPM_GET_PUT is set in @rpmflags, increment the device's + * usage count first, then bring the device to %RPM_ACTIVE state. * * This routine may be called in atomic context if the %RPM_ASYNC flag is set, * or if pm_runtime_irq_safe() has been called. @@ -1277,17 +1276,15 @@ EXPORT_SYMBOL_GPL(pm_runtime_get_if_active); * pm_runtime_get_if_in_use - Conditionally bump up runtime PM usage counter. * @dev: Target device. * - * Increment the runtime PM usage counter of @dev if its runtime PM status is - * %RPM_ACTIVE and its runtime PM usage counter is greater than 0 or it is not - * ignoring children and its active child count is nonzero. 1 is returned in - * this case. + * Increment the runtime PM usage counter of @dev if it is "in use." A device + * is considered in use if its runtime PM status is %RPM_ACTIVE and its runtime + * PM usage counter is greater than 0, or if it is not ignoring children and + * its active child count is nonzero. * - * If @dev is in a different state or it is not in use (that is, its usage - * counter is 0, or it is ignoring children, or its active child count is 0), - * 0 is returned. - * - * -EINVAL is returned if runtime PM is disabled for the device, in which case - * also the usage counter of @dev is not updated. + * Return: + * * %-EINVAL: Runtime PM is disabled for @dev. The usage counter is not incremented. + * * %1: Success; usage counter is incremented. + * * %0: @dev was not in use; usage counter is not incremented. */ int pm_runtime_get_if_in_use(struct device *dev) { @@ -1471,11 +1468,13 @@ static void __pm_runtime_barrier(struct device *dev) * pm_runtime_barrier - Flush pending requests and wait for completions. * @dev: Device to handle. * - * Prevent the device from being suspended by incrementing its usage counter and - * if there's a pending resume request for the device, wake the device up. - * Next, make sure that all pending requests for the device have been flushed - * from pm_wq and wait for all runtime PM operations involving the device in - * progress to complete. + * If the device has a pending resume request, resume it synchronously. For all + * other request types, cancel any queued request, and wait for running + * operations to complete. + * + * Note that this is intentionally asymmetric, as it guarantees any queued + * asynchronous resume request will complete, but it may cancel asynchronous + * suspend requests. */ void pm_runtime_barrier(struct device *dev) { @@ -1559,8 +1558,16 @@ void __pm_runtime_disable(struct device *dev, bool check_resume) EXPORT_SYMBOL_GPL(__pm_runtime_disable); /** - * pm_runtime_enable - Enable runtime PM of a device. + * pm_runtime_enable - Enable runtime PM for a device. * @dev: Device to handle. + * + * Enable runtime PM transitions for @dev by decrementing its disable counter. + * Once the counter reaches zero, the PM core is permitted to execute runtime + * PM callbacks for @dev as power conditions change. + * + * Callers should ensure that the device's runtime PM status accurately reflects + * its physical hardware state (via pm_runtime_set_active() or + * pm_runtime_set_suspended()) before enabling runtime PM. */ void pm_runtime_enable(struct device *dev) { diff --git a/include/linux/pm_runtime.h b/include/linux/pm_runtime.h index 1ffd9d5c3010..322e3b17f987 100644 --- a/include/linux/pm_runtime.h +++ b/include/linux/pm_runtime.h @@ -352,11 +352,11 @@ static inline int pm_runtime_force_resume(struct device *dev) { return -ENXIO; } #endif /* CONFIG_PM_SLEEP */ /** - * pm_runtime_idle - Conditionally set up autosuspend of a device or suspend it. + * pm_runtime_idle - Conditionally initiate autosuspend of a device or suspend it. * @dev: Target device. * * Invoke the "idle check" callback of @dev and, depending on its return value, - * set up autosuspend of @dev or suspend it (depending on whether or not + * initiate autosuspend of @dev or suspend it (depending on whether or not * autosuspend has been enabled for it). * * Return: @@ -400,13 +400,13 @@ static inline int pm_runtime_suspend(struct device *dev) } /** - * pm_runtime_autosuspend - Update the last access time and set up autosuspend + * pm_runtime_autosuspend - Update the last access time and initiate autosuspend * of a device. * @dev: Target device. * - * First update the last access time, then set up autosuspend of @dev or suspend - * it (depending on whether or not autosuspend is enabled for it) without - * engaging its "idle check" callback. + * First update the last access time, then initiate autosuspend of @dev or + * suspend it (depending on whether or not autosuspend is enabled for it) + * without engaging its "idle check" callback. * * Return: * * %1: Success; device was already suspended. @@ -442,11 +442,11 @@ static inline int pm_runtime_resume(struct device *dev) } /** - * pm_request_idle - Queue up "idle check" execution for a device. + * pm_request_idle - Request an asynchronous idle check for a device. * @dev: Target device. * - * Queue up a work item to run an equivalent of pm_runtime_idle() for @dev - * asynchronously. + * Asynchronously request the PM core to evaluate whether @dev can be idled + * or suspended, invoking its ->runtime_idle() callback if provided. * * Return: * * %0: Success. @@ -465,9 +465,12 @@ static inline int pm_request_idle(struct device *dev) } /** - * pm_request_resume - Queue up runtime-resume of a device. + * pm_request_resume - Request an asynchronous runtime resume for a device. * @dev: Target device. * + * Asynchronously request the PM core to resume @dev to %RPM_ACTIVE state + * without modifying its usage counter. + * * Return: * * %1: Success; @dev is already %RPM_ACTIVE. * * %0: Success. @@ -479,12 +482,11 @@ static inline int pm_request_resume(struct device *dev) } /** - * pm_request_autosuspend - Update the last access time and queue up autosuspend - * of a device. + * pm_request_autosuspend - Update access time and request delayed suspension. * @dev: Target device. * - * Update the last access time of a device and queue up a work item to run an - * equivalent pm_runtime_autosuspend() for @dev asynchronously. + * Update the last access time of @dev and asynchronously request the PM core + * to suspend it after the autosuspend delay has elapsed. * * Return: * * %1: Success; device was already suspended. @@ -505,11 +507,11 @@ static inline int pm_request_autosuspend(struct device *dev) } /** - * pm_runtime_get - Bump up usage counter and queue up resume of a device. + * pm_runtime_get - Increment usage counter and request asynchronous resume. * @dev: Target device. * - * Bump up the runtime PM usage counter of @dev and queue up a work item to - * carry out runtime-resume of it. + * Increment the runtime PM usage counter of @dev and, if the device is + * currently suspended, asynchronously request the PM core to resume it. * * Return: * * %1: Success; @dev is already %RPM_ACTIVE. @@ -528,12 +530,10 @@ static inline int pm_runtime_get(struct device *dev) * Bump up the runtime PM usage counter of @dev and carry out runtime-resume of * it synchronously. * - * The possible return values of this function are the same as for - * pm_runtime_resume() and the runtime PM usage counter of @dev remains - * incremented in all cases, even if it returns an error code. - * Consider using pm_runtime_resume_and_get() instead of it, especially - * if its return value is checked by the caller, as this is likely to result - * in cleaner code. + * Note that the runtime PM usage counter of @dev remains incremented in all + * cases, even if it returns an error code. Consider using + * pm_runtime_resume_and_get() instead, especially if the return value is + * checked by the caller, as this is likely to result in cleaner code. * * Return: * * %1: Success; @dev is already %RPM_ACTIVE. @@ -575,11 +575,12 @@ static inline int pm_runtime_resume_and_get(struct device *dev) } /** - * pm_runtime_put - Drop device usage counter and queue up "idle check" if 0. + * pm_runtime_put - Drop device usage counter and request asynchronous idle check. * @dev: Target device. * - * Decrement the runtime PM usage counter of @dev and if it turns out to be - * equal to 0, queue up a work item for @dev like in pm_request_idle(). + * Decrement the runtime PM usage counter of @dev. If the counter reaches zero + * and the device has no active child dependencies, asynchronously request the + * PM core to idle or suspend the device. */ static inline void pm_runtime_put(struct device *dev) { @@ -611,13 +612,13 @@ static inline int __pm_runtime_put_autosuspend(struct device *dev) } /** - * pm_runtime_put_autosuspend - Update the last access time of a device, drop - * its usage counter and queue autosuspend if the usage counter becomes 0. + * pm_runtime_put_autosuspend - Update the last access time, drop usage counter + * and request autosuspend. * @dev: Target device. * - * Update the last access time of @dev, decrement runtime PM usage counter of - * @dev and if it turns out to be equal to 0, queue up a work item for @dev like - * in pm_request_autosuspend(). + * Update the last access time of @dev and decrement its runtime PM usage + * counter. If the counter drops to zero, asynchronously request the PM core to + * suspend the device once its autosuspend delay has elapsed. * * Return: * * %1: Success. Usage counter dropped to zero, but device was already suspended. @@ -688,10 +689,12 @@ DEFINE_GUARD_COND(pm_runtime_active_auto, _try_enabled, * pm_runtime_put_sync - Drop device usage counter and run "idle check" if 0. * @dev: Target device. * - * Decrement the runtime PM usage counter of @dev and if it turns out to be - * equal to 0, invoke the "idle check" callback of @dev and, depending on its - * return value, set up autosuspend of @dev or suspend it (depending on whether - * or not autosuspend has been enabled for it). + * Decrement the runtime PM usage counter of @dev. If the counter drops to zero, + * synchronously evaluate and trigger idle/suspend handling. + * + * Note that this does not update the last access time, but it does respect + * existing autosuspend timers. If @dev uses autosuspend, consider using + * pm_runtime_put_sync_autosuspend() or pm_runtime_put_sync_suspend() instead. * * The runtime PM usage counter of @dev remains decremented in all cases, even * if it returns an error code. @@ -718,8 +721,12 @@ static inline int pm_runtime_put_sync(struct device *dev) * pm_runtime_put_sync_suspend - Drop device usage counter and suspend if 0. * @dev: Target device. * - * Decrement the runtime PM usage counter of @dev and if it turns out to be - * equal to 0, carry out runtime-suspend of @dev synchronously. + * Decrement the runtime PM usage counter of @dev. If the counter drops to zero, + * suspend the device synchronously. + * + * This API differs from pm_runtime_put_sync() and + * pm_runtime_put_sync_autosuspend() in that it ignores any outstanding + * autosuspend delays. * * The runtime PM usage counter of @dev remains decremented in all cases, even * if it returns an error code. @@ -747,10 +754,11 @@ static inline int pm_runtime_put_sync_suspend(struct device *dev) * drop device usage counter and autosuspend if 0. * @dev: Target device. * - * Update the last access time of @dev, decrement the runtime PM usage counter - * of @dev and if it turns out to be equal to 0, set up autosuspend of @dev or - * suspend it synchronously (depending on whether or not autosuspend has been - * enabled for it). + * Update the last access time of @dev and decrement its runtime PM usage + * counter. If the counter drops to zero, synchronously suspend the device (or + * schedule autosuspend if the delay has not elapsed). + * + * Prefer this API over pm_runtime_put_sync() for devices that use autosuspend. * * The runtime PM usage counter of @dev remains decremented in all cases, even * if it returns an error code. @@ -827,9 +835,8 @@ static inline int pm_runtime_set_suspended(struct device *dev) * * If the counter is zero when this function runs and there is a pending runtime * resume request for @dev, it will be resumed. If the counter is still zero at - * that point, all of the pending runtime PM requests for @dev will be canceled - * and all runtime PM operations in progress involving it will be waited for to - * complete. + * that point, this function cancels all pending runtime PM requests for @dev + * and waits for its runtime PM operations to complete (if any). * * For each invocation of this function for @dev, there must be a matching * pm_runtime_enable() call, so that runtime PM is eventually enabled for it From 3d6174fe309012184226eb159e26b478efcd8291 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Fri, 4 Sep 2026 14:12:09 -0700 Subject: [PATCH 23/89] PM: runtime: Pull API docs from kerneldoc This avoids staleness and duplication, as the same APIs were previously documented twice. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260904141215.4.I2cb2c43192ef46bd9dfad6699080d8a1d56756d5@changeid Signed-off-by: Rafael J. Wysocki --- Documentation/power/runtime_pm.rst | 278 +---------------------------- 1 file changed, 5 insertions(+), 273 deletions(-) diff --git a/Documentation/power/runtime_pm.rst b/Documentation/power/runtime_pm.rst index a53ab09c37d5..380dad7590a9 100644 --- a/Documentation/power/runtime_pm.rst +++ b/Documentation/power/runtime_pm.rst @@ -307,219 +307,10 @@ All of the above fields are members of the 'power' member of 'struct device'. The following runtime PM helper functions are defined in drivers/base/power/runtime.c and include/linux/pm_runtime.h: - `void pm_runtime_init(struct device *dev);` - - initialize the device runtime PM fields in 'struct dev_pm_info' +.. kernel-doc:: drivers/base/power/runtime.c + :export: - `void pm_runtime_remove(struct device *dev);` - - make sure that the runtime PM of the device will be disabled after - removing the device from device hierarchy - - `int pm_runtime_idle(struct device *dev);` - - execute the subsystem-level idle callback for the device; returns an - error code on failure, where -EINPROGRESS means that ->runtime_idle() is - already being executed; if there is no callback or the callback returns 0 - then run pm_runtime_autosuspend(dev) and return its result - - `int pm_runtime_suspend(struct device *dev);` - - execute the subsystem-level suspend callback for the device; returns 0 on - success, 1 if the device's runtime PM status was already 'suspended', or - error code on failure, where -EAGAIN or -EBUSY means it is safe to attempt - to suspend the device again in future and -EACCES means that - 'power.disable_depth' is different from 0 - - `int pm_runtime_autosuspend(struct device *dev);` - - same as pm_runtime_suspend() except that a call to - pm_runtime_mark_last_busy() is made and an autosuspend is scheduled for - the appropriate time and 0 is returned - - `int pm_runtime_resume(struct device *dev);` - - execute the subsystem-level resume callback for the device; returns 0 on - success, 1 if the device's runtime PM status is already 'active' (also if - 'power.disable_depth' is nonzero, but the status was 'active' when it was - changing from 0 to 1) or error code on failure, where -EAGAIN means it may - be safe to attempt to resume the device again in future, but - 'power.runtime_error' should be checked additionally, and -EACCES means - that the callback could not be run, because 'power.disable_depth' was - different from 0 - - `int pm_runtime_resume_and_get(struct device *dev);` - - run pm_runtime_resume(dev) and if successful, increment the device's - usage counter; returns 0 on success (whether or not the device's - runtime PM status was already 'active') or the error code from - pm_runtime_resume() on failure. - - `int pm_request_idle(struct device *dev);` - - submit a request to execute the subsystem-level idle callback for the - device (the request is represented by a work item in pm_wq); returns 0 on - success or error code if the request has not been queued up - - `int pm_request_autosuspend(struct device *dev);` - - Call pm_runtime_mark_last_busy() and schedule the execution of the - subsystem-level suspend callback for the device when the autosuspend delay - expires - - `int pm_schedule_suspend(struct device *dev, unsigned int delay);` - - schedule the execution of the subsystem-level suspend callback for the - device in future, where 'delay' is the time to wait before queuing up a - suspend work item in pm_wq, in milliseconds (if 'delay' is zero, the work - item is queued up immediately); returns 0 on success, 1 if the device's PM - runtime status was already 'suspended', or error code if the request - hasn't been scheduled (or queued up if 'delay' is 0); if the execution of - ->runtime_suspend() is already scheduled and not yet expired, the new - value of 'delay' will be used as the time to wait - - `int pm_request_resume(struct device *dev);` - - submit a request to execute the subsystem-level resume callback for the - device (the request is represented by a work item in pm_wq); returns 0 on - success, 1 if the device's runtime PM status was already 'active', or - error code if the request hasn't been queued up - - `void pm_runtime_get_noresume(struct device *dev);` - - increment the device's usage counter - - `int pm_runtime_get(struct device *dev);` - - increment the device's usage counter, run pm_request_resume(dev) and - return its result - - `int pm_runtime_get_sync(struct device *dev);` - - increment the device's usage counter, run pm_runtime_resume(dev) and - return its result; - note that it does not drop the device's usage counter on errors, so - consider using pm_runtime_resume_and_get() instead of it, especially - if its return value is checked by the caller, as this is likely to - result in cleaner code. - - `int pm_runtime_get_if_in_use(struct device *dev);` - - return -EINVAL if 'power.disable_depth' is nonzero; otherwise, if the - runtime PM status is RPM_ACTIVE and the runtime PM usage counter is - nonzero, increment the counter and return 1; otherwise return 0 without - changing the counter - - `int pm_runtime_get_if_active(struct device *dev);` - - return -EINVAL if 'power.disable_depth' is nonzero; otherwise, if the - runtime PM status is RPM_ACTIVE, increment the counter and - return 1; otherwise return 0 without changing the counter - - `void pm_runtime_put_noidle(struct device *dev);` - - decrement the device's usage counter - - `int pm_runtime_put(struct device *dev);` - - decrement the device's usage counter; if the result is 0 then run - pm_request_idle(dev) and return its result - - `int pm_runtime_put_autosuspend(struct device *dev);` - - set the power.last_busy field to the current time and decrement the - device's usage counter; if the result is 0 then run - pm_request_autosuspend(dev) and return its result - - `int __pm_runtime_put_autosuspend(struct device *dev);` - - decrement the device's usage counter; if the result is 0 then run - pm_request_autosuspend(dev) and return its result - - `int pm_runtime_put_sync(struct device *dev);` - - decrement the device's usage counter; if the result is 0 then run - pm_runtime_idle(dev) and return its result - - `int pm_runtime_put_sync_suspend(struct device *dev);` - - decrement the device's usage counter; if the result is 0 then run - pm_runtime_suspend(dev) and return its result - - `int pm_runtime_put_sync_autosuspend(struct device *dev);` - - set the power.last_busy field to the current time and decrement the - device's usage counter; if the result is 0 then run - pm_runtime_autosuspend(dev) and return its result - - `void pm_runtime_enable(struct device *dev);` - - decrement the device's 'power.disable_depth' field; if that field is equal - to zero, the runtime PM helper functions can execute subsystem-level - callbacks described in Section 2 for the device - - `int pm_runtime_disable(struct device *dev);` - - increment the device's 'power.disable_depth' field (if the value of that - field was previously zero, this prevents subsystem-level runtime PM - callbacks from being run for the device), make sure that all of the - pending runtime PM operations on the device are either completed or - canceled; returns 1 if there was a resume request pending and it was - necessary to execute the subsystem-level resume callback for the device - to satisfy that request, otherwise 0 is returned - - `void pm_runtime_barrier(struct device *dev);` - - check if there's a resume request pending for the device and resume it - (synchronously) in that case, cancel any other pending runtime PM requests - regarding it and wait for all runtime PM operations on it in progress to - complete - - `void pm_suspend_ignore_children(struct device *dev, bool enable);` - - set/unset the power.ignore_children flag of the device - - `int pm_runtime_set_active(struct device *dev);` - - clear the device's 'power.runtime_error' flag, set the device's runtime - PM status to 'active' and update its parent's counter of 'active' - children as appropriate (it is only valid to use this function if - 'power.runtime_error' is set or 'power.disable_depth' is greater than - zero); it will fail and return error code if the device has a parent - which is not active and the 'power.ignore_children' flag of which is unset - - `void pm_runtime_set_suspended(struct device *dev);` - - clear the device's 'power.runtime_error' flag, set the device's runtime - PM status to 'suspended' and update its parent's counter of 'active' - children as appropriate (it is only valid to use this function if - 'power.runtime_error' is set or 'power.disable_depth' is greater than - zero) - - `bool pm_runtime_active(struct device *dev);` - - return true if the device's runtime PM status is 'active' or its - 'power.disable_depth' field is not equal to zero, or false otherwise - - `bool pm_runtime_suspended(struct device *dev);` - - return true if the device's runtime PM status is 'suspended' and its - 'power.disable_depth' field is equal to zero, or false otherwise - - `bool pm_runtime_status_suspended(struct device *dev);` - - return true if the device's runtime PM status is 'suspended' - - `void pm_runtime_no_callbacks(struct device *dev);` - - set the power.no_callbacks flag for the device and remove the runtime - PM attributes from /sys/devices/.../power (or prevent them from being - added when the device is registered) - - `void pm_runtime_irq_safe(struct device *dev);` - - set the power.irq_safe flag for the device, causing the runtime-PM - callbacks to be invoked with interrupts off - - `bool pm_runtime_is_irq_safe(struct device *dev);` - - return true if power.irq_safe flag was set for the device, causing - the runtime-PM callbacks to be invoked with interrupts off - - `void pm_runtime_mark_last_busy(struct device *dev);` - - set the power.last_busy field to the current time - - `void pm_runtime_use_autosuspend(struct device *dev);` - - set the power.use_autosuspend flag, enabling autosuspend delays; call - pm_runtime_get_sync if the flag was previously cleared and - power.autosuspend_delay is negative - - `void pm_runtime_dont_use_autosuspend(struct device *dev);` - - clear the power.use_autosuspend flag, disabling autosuspend delays; - decrement the device's usage counter if the flag was previously set and - power.autosuspend_delay is negative; call pm_runtime_idle - - `void pm_runtime_set_autosuspend_delay(struct device *dev, int delay);` - - set the power.autosuspend_delay value to 'delay' (expressed in - milliseconds); if 'delay' is negative then runtime suspends are - prevented; if power.use_autosuspend is set, pm_runtime_get_sync may be - called or the device's usage counter may be decremented and - pm_runtime_idle called depending on if power.autosuspend_delay is - changed to or from a negative value; if power.use_autosuspend is clear, - pm_runtime_idle is called - - `unsigned long pm_runtime_autosuspend_expiration(struct device *dev);` - - calculate the time when the current autosuspend delay period will expire, - based on power.last_busy and power.autosuspend_delay; if the delay time - is 1000 ms or larger then the expiration time is rounded up to the - nearest second; returns 0 if the delay period has already expired or - power.use_autosuspend isn't set, otherwise returns the expiration time - in jiffies +.. kernel-doc:: include/linux/pm_runtime.h It is safe to execute the following helper functions from interrupt context: @@ -728,67 +519,8 @@ Subsystems may wish to conserve code space by using the set of generic power management callbacks provided by the PM core, defined in driver/base/power/generic_ops.c: - `int pm_generic_runtime_suspend(struct device *dev);` - - invoke the ->runtime_suspend() callback provided by the driver of this - device and return its result, or return 0 if not defined - - `int pm_generic_runtime_resume(struct device *dev);` - - invoke the ->runtime_resume() callback provided by the driver of this - device and return its result, or return 0 if not defined - - `int pm_generic_suspend(struct device *dev);` - - if the device has not been suspended at run time, invoke the ->suspend() - callback provided by its driver and return its result, or return 0 if not - defined - - `int pm_generic_suspend_noirq(struct device *dev);` - - if pm_runtime_suspended(dev) returns "false", invoke the ->suspend_noirq() - callback provided by the device's driver and return its result, or return - 0 if not defined - - `int pm_generic_resume(struct device *dev);` - - invoke the ->resume() callback provided by the driver of this device and, - if successful, change the device's runtime PM status to 'active' - - `int pm_generic_resume_noirq(struct device *dev);` - - invoke the ->resume_noirq() callback provided by the driver of this device - - `int pm_generic_freeze(struct device *dev);` - - if the device has not been suspended at run time, invoke the ->freeze() - callback provided by its driver and return its result, or return 0 if not - defined - - `int pm_generic_freeze_noirq(struct device *dev);` - - if pm_runtime_suspended(dev) returns "false", invoke the ->freeze_noirq() - callback provided by the device's driver and return its result, or return - 0 if not defined - - `int pm_generic_thaw(struct device *dev);` - - if the device has not been suspended at run time, invoke the ->thaw() - callback provided by its driver and return its result, or return 0 if not - defined - - `int pm_generic_thaw_noirq(struct device *dev);` - - if pm_runtime_suspended(dev) returns "false", invoke the ->thaw_noirq() - callback provided by the device's driver and return its result, or return - 0 if not defined - - `int pm_generic_poweroff(struct device *dev);` - - if the device has not been suspended at run time, invoke the ->poweroff() - callback provided by its driver and return its result, or return 0 if not - defined - - `int pm_generic_poweroff_noirq(struct device *dev);` - - if pm_runtime_suspended(dev) returns "false", run the ->poweroff_noirq() - callback provided by the device's driver and return its result, or return - 0 if not defined - - `int pm_generic_restore(struct device *dev);` - - invoke the ->restore() callback provided by the driver of this device and, - if successful, change the device's runtime PM status to 'active' - - `int pm_generic_restore_noirq(struct device *dev);` - - invoke the ->restore_noirq() callback provided by the device's driver +.. kernel-doc:: drivers/base/power/generic_ops.c + :export: These functions are the defaults used by the PM core if a subsystem doesn't provide its own callbacks for ->runtime_idle(), ->runtime_suspend(), From 6881d8de92c70783fdc934024edeaa48cf6d8baa Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Fri, 4 Sep 2026 14:12:10 -0700 Subject: [PATCH 24/89] PM: core: Document struct dev_pm_info with kerneldoc Documentation/power/runtime_pm.rst includes several descriptions of dev_pm_info fields, but many of them are wrong these days, as the types or behaviors have changed. This is a prime reason for keeping docs closer to the code where possible. Adapt and rewrite some of these descriptions, and add them to include/linux/pm.h directly. Then pull these docs into the generated HTML. Tested with `make htmldocs`. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260904141215.5.Ib0272869f8b1178426106d911441c449ac299048@changeid Signed-off-by: Rafael J. Wysocki --- Documentation/power/runtime_pm.rst | 101 ++--------------------------- include/linux/pm.h | 93 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 96 deletions(-) diff --git a/Documentation/power/runtime_pm.rst b/Documentation/power/runtime_pm.rst index 380dad7590a9..39fdeeda7a1e 100644 --- a/Documentation/power/runtime_pm.rst +++ b/Documentation/power/runtime_pm.rst @@ -203,103 +203,12 @@ rules: 3. Runtime PM Device Fields =========================== -The following device runtime PM fields are present in 'struct dev_pm_info', as -defined in include/linux/pm.h: +Device PM fields are found in 'struct dev_pm_info', as defined in +include/linux/pm.h. Many of those fields track runtime PM configuration and +state. - `struct timer_list suspend_timer;` - - timer used for scheduling (delayed) suspend and autosuspend requests - - `unsigned long timer_expires;` - - timer expiration time, in jiffies (if this is different from zero, the - timer is running and will expire at that time, otherwise the timer is not - running) - - `struct work_struct work;` - - work structure used for queuing up requests (i.e. work items in pm_wq) - - `wait_queue_head_t wait_queue;` - - wait queue used if any of the helper functions needs to wait for another - one to complete - - `spinlock_t lock;` - - lock used for synchronization - - `atomic_t usage_count;` - - the usage counter of the device - - `atomic_t child_count;` - - the count of 'active' children of the device - - `unsigned int ignore_children;` - - if set, the value of child_count is ignored (but still updated) - - `unsigned int disable_depth;` - - used for disabling the helper functions (they work normally if this is - equal to zero); the initial value of it is 1 (i.e. runtime PM is - initially disabled for all devices) - - `int runtime_error;` - - if set, there was a fatal error (one of the callbacks returned error code - as described in Section 2), so the helper functions will not work until - this flag is cleared; this is the error code returned by the failing - callback - - `unsigned int idle_notification;` - - if set, ->runtime_idle() is being executed - - `unsigned int request_pending;` - - if set, there's a pending request (i.e. a work item queued up into pm_wq) - - `enum rpm_request request;` - - type of request that's pending (valid if request_pending is set) - - `unsigned int deferred_resume;` - - set if ->runtime_resume() is about to be run while ->runtime_suspend() is - being executed for that device and it is not practical to wait for the - suspend to complete; means "start a resume as soon as you've suspended" - - `enum rpm_status runtime_status;` - - the runtime PM status of the device; this field's initial value is - RPM_SUSPENDED, which means that each device is initially regarded by the - PM core as 'suspended', regardless of its real hardware status - - `enum rpm_status last_status;` - - the last runtime PM status of the device captured before disabling runtime - PM for it (invalid initially and when disable_depth is 0) - - `unsigned int runtime_auto;` - - if set, indicates that the user space has allowed the device driver to - power manage the device at run time via the /sys/devices/.../power/control - `interface;` it may only be modified with the help of the - pm_runtime_allow() and pm_runtime_forbid() helper functions - - `unsigned int no_callbacks;` - - indicates that the device does not use the runtime PM callbacks (see - Section 8); it may be modified only by the pm_runtime_no_callbacks() - helper function - - `unsigned int irq_safe;` - - indicates that the ->runtime_suspend() and ->runtime_resume() callbacks - will be invoked with the spinlock held and interrupts disabled - - `unsigned int use_autosuspend;` - - indicates that the device's driver supports delayed autosuspend (see - Section 9); it may be modified only by the - pm_runtime{_dont}_use_autosuspend() helper functions - - `unsigned int timer_autosuspends;` - - indicates that the PM core should attempt to carry out an autosuspend - when the timer expires rather than a normal suspend - - `int autosuspend_delay;` - - the delay time (in milliseconds) to be used for autosuspend - - `unsigned long last_busy;` - - the time (in jiffies) when the pm_runtime_mark_last_busy() helper - function was last called for this device; used in calculating inactivity - periods for autosuspend - -All of the above fields are members of the 'power' member of 'struct device'. +.. kernel-doc:: include/linux/pm.h + :identifiers: dev_pm_info 4. Runtime PM Device Helper Functions ===================================== diff --git a/include/linux/pm.h b/include/linux/pm.h index afcaaa37a812..ef3f1310e749 100644 --- a/include/linux/pm.h +++ b/include/linux/pm.h @@ -663,6 +663,99 @@ struct pm_subsys_data { #define DPM_FLAG_SMART_SUSPEND BIT(2) #define DPM_FLAG_MAY_SKIP_RESUME BIT(3) +/** + * struct dev_pm_info - Device power management information. + * + * @power_state: Legacy power state (mostly unused in modern kernels). + * @can_wakeup: Device is capable of generating wakeup signals. + * @async_suspend: Device can be suspended and resumed asynchronously. + * @in_dpm_list: Device is on the dpm_list. + * @is_prepared: Device's ->prepare() callback has run successfully. + * @is_suspended: Device is suspended during a system sleep transition. + * @is_noirq_suspended: Device's noirq suspend callback has run successfully. + * @is_late_suspended: Device's late suspend callback has run successfully. + * @no_pm: Device does not participate in power management transitions. + * @early_init: Device was initialized before standard PM initialization. + * @direct_complete: Device can skip suspend/resume callbacks and remain + * runtime-suspended during system sleep. + * @driver_flags: Driver flags (e.g. %DPM_FLAG_SMART_SUSPEND) set at probe time. + * @lock: Spinlock used for synchronizing PM state transitions and runtime PM + * operations. + * @entry: List head for device power management lists. + * @completion: Completion for synchronization during asynchronous system + * suspend/resume. + * @wakeup: Wakeup source object associated with the device. + * @work_in_progress: Asynchronous PM operation in progress. + * @wakeup_path: Device is in the wakeup path or can wake the system up. + * @syscore: Device participates in syscore power management operations. + * @no_pm_callbacks: Device has no PM callbacks; handled by parent or subsystem. + * @smart_suspend: Driver requested smart-suspend behavior. + * @must_resume: Device must be resumed during system resume. + * @may_skip_resume: Set by subsystems to indicate driver resume callbacks may + * be skipped. + * @out_band_wakeup: Out-of-band wakeup is supported. + * @strict_midlayer: Middle layer code does not want callbacks invoked via + * pm_runtime_force_suspend() / pm_runtime_force_resume(). + * @should_wakeup: Wakeup flag when system sleep is not enabled. + * @suspend_timer: High-resolution timer used for scheduling delayed runtime + * suspend and autosuspend requests. + * @timer_expires: Timer expiration time in nanoseconds monotonic time + * (runtime PM). + * @work: Work structure used for queuing up requests into pm_wq (runtime PM). + * @wait_queue: Wait queue used if any helper functions need to wait for another + * state change to complete (runtime PM). + * @wakeirq: Dedicated wakeup interrupt for the device. + * @usage_count: Device runtime PM usage counter. + * @child_count: Count of active children of the device (runtime PM). + * @disable_depth: Disable counter for runtime PM (runtime PM is enabled when + * this is 0; initial value is 1). + * @idle_notification: Set if ->runtime_idle() is being executed. + * @request_pending: Set if a work item is queued into pm_wq (runtime PM). + * @deferred_resume: Set if ->runtime_resume() should run as soon as + * ->runtime_suspend() completes. + * @needs_force_resume: Indicates the device was forced into suspend by + * pm_runtime_force_suspend() and must be resumed by + * pm_runtime_force_resume(). + * @runtime_auto: User space has allowed the driver to power manage the device + * at runtime via sysfs control attribute; also can be set by + * pm_runtime_allow() or pm_runtime_forbid(). + * @ignore_children: If set, the value of child_count is ignored for runtime + * suspend and idle decisions. + * @no_callbacks: Indicates the device does not use runtime PM callbacks. + * @irq_safe: Indicates runtime PM callbacks will be invoked with the spinlock + * held and interrupts disabled. + * @use_autosuspend: Indicates the device driver supports delayed runtime + * autosuspend. + * @timer_autosuspends: Indicates the runtime PM core should attempt an + * autosuspend rather than a normal suspend when the timer expires. + * @memalloc_noio: Indicates memory allocation during runtime PM transitions + * must avoid I/O (GFP_NOIO). + * @links_count: Number of device links that require runtime PM coordination. + * @request: Type of pending runtime PM request (valid if request_pending is + * set). + * @runtime_status: Runtime PM status of the device. + * @last_status: Last status captured before disabling runtime PM, or + * %RPM_BLOCKED / %RPM_INVALID. + * @runtime_error: Fatal error code returned by a failing callback, blocking + * helpers until cleared. + * @autosuspend_delay: Delay time in milliseconds to be used for runtime + * autosuspend. + * @last_busy: Timestamp in nanoseconds when pm_runtime_mark_last_busy() was + * last called. Used in calculating inactivity periods for autosuspend. + * @active_time: Accumulated time in nanoseconds spent in %RPM_ACTIVE state. + * @suspended_time: Accumulated time in nanoseconds spent in %RPM_SUSPENDED + * state. + * @accounting_timestamp: Timestamp in nanoseconds of the last runtime PM state + * accounting update. + * @subsys_data: Subsystem-specific power management data. + * @set_latency_tolerance: Callback for setting latency tolerance. + * @qos: Per-device PM Quality of Service (QoS) constraints. + * @detach_power_off: Indicates device should be detached from PM domain on + * power off. + * + * Device power management information stored in the "power" member of struct + * device. + */ struct dev_pm_info { pm_message_t power_state; bool can_wakeup:1; From fb345025f161d17fe7c8a6048c7e80e467fa8ddf Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:46 +0100 Subject: [PATCH 25/89] ACPI: CPPC: Validate the _CPC package header The _CPC NumEntries field includes every package element, including NumEntries and Revision. acpi_cppc_processor_probe() nevertheless reads those first two elements before checking that they exist and trusts NumEntries when walking the remaining elements. Reject packages with fewer than two elements and NumEntries values which exceed the package count before accessing Revision or iterating over register descriptors. Although the specification defines NumEntries as the number of package elements, tolerate additional trailing elements because ignoring them is safe and avoids rejecting padded firmware. Revision is specified as a BYTE, but the parser assigns its 64-bit AML Integer to an unsigned int before validating it. Reject values above U8_MAX before conversion so, for example, 0x100000004 cannot truncate to revision 4. Fixes: 337aadff8e45 ("ACPI: Introduce CPU performance controls using CPPC") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com Link: https://sashiko.dev/#/patchset/20260808082644.1251332-1-christian.loehle%40arm.com Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-2-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index fef54fcd00b7..3b8cdf88e31d 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -793,6 +793,11 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } out_obj = (union acpi_object *) output.pointer; + if (out_obj->package.count < 2) { + pr_debug("Unexpected _CPC package count (%u) for CPU:%d\n", + out_obj->package.count, pr->id); + goto out_buf_free; + } cpc_ptr = kzalloc_obj(struct cpc_desc); if (!cpc_ptr) { @@ -803,12 +808,15 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) /* First entry is NumEntries. */ cpc_obj = &out_obj->package.elements[0]; if (cpc_obj->type == ACPI_TYPE_INTEGER) { - num_ent = cpc_obj->integer.value; - if (num_ent <= 1) { - pr_debug("Unexpected _CPC NumEntries value (%d) for CPU:%d\n", - num_ent, pr->id); + if (cpc_obj->integer.value < 2 || + cpc_obj->integer.value > out_obj->package.count) { + pr_debug("Invalid _CPC NumEntries (%llu) for package count (%u) on CPU:%d\n", + cpc_obj->integer.value, out_obj->package.count, + pr->id); goto out_free; } + + num_ent = cpc_obj->integer.value; } else { pr_debug("Unexpected _CPC NumEntries entry type (%d) for CPU:%d\n", cpc_obj->type, pr->id); @@ -818,6 +826,12 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) /* Second entry should be revision. */ cpc_obj = &out_obj->package.elements[1]; if (cpc_obj->type == ACPI_TYPE_INTEGER) { + if (cpc_obj->integer.value > U8_MAX) { + pr_debug("Invalid _CPC Revision (%llu) for CPU:%d\n", + cpc_obj->integer.value, pr->id); + ret = -EINVAL; + goto out_free; + } cpc_rev = cpc_obj->integer.value; } else { pr_debug("Unexpected _CPC Revision entry type (%d) for CPU:%d\n", From 32299b29a887ed5615695f42a19cd8a843903146 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:47 +0100 Subject: [PATCH 26/89] ACPI: CPPC: Validate _CPC entry and control semantics Writable _CPC controls are Register descriptors encoded as Buffer objects. Integer entries represent fixed values or legacy placeholders for unsupported optional registers; Autonomous Selection Integer 1 is the special immutable form which enables operation without Desired Performance. The parser accepts arbitrary object types and cpc_write() assumes that its argument contains a GAS. Malformed firmware can therefore make it interpret an Integer union member as a register. Validate the portion of each encoding consumed by the driver: bound Integer capabilities stored in u32 fields to U32_MAX, and require Buffer entries to start with a complete Generic Register descriptor with the expected header. Do not restrict an Integer Counter Wraparound Time. Both AML Integers and the kernel interface are 64-bit, despite the DWORD annotation in the ACPI table. Continue tolerating Integer 0 placeholders for absent optional Buffer registers to avoid breaking legacy platforms. Report their invalid encoding as a firmware bug. Retain type checks in cpc_write() as defense in depth. Reject an attempt to disable immutable Autonomous Selection instead of silently applying only the EPP part of the request. Capability registers are read into u64 temporaries but exposed through u32 fields. Reject values above U32_MAX instead of allowing them to be truncated. In particular, a truncated Highest Performance value can become a zero divisor in the performance-to-frequency conversion. Enforce the required ordering from Highest through Nominal, Lowest Nonlinear, and Lowest Performance, and constrain a present Guaranteed Performance to the inclusive Lowest-to-Nominal range. Also reject reversed frequency anchors and unequal frequency anchors with identical performance anchors. Those invalid tuples otherwise make affine-conversion differences wrap or divide by zero. Check mandatory object presence separately from the Integer-zero convention for absent optional fields. Reject Minimum and Maximum Performance readbacks above U32_MAX before narrowing them. A truncated minimum could otherwise become an explicit zero update when cppc-cpufreq writes the initial controls back to the platform. Minimum Performance also defines zero as a real no-limit value, but the exported cppc_set_perf() interface historically used zero to omit a bound. Add a minimum-valid flag so callers can request zero without changing that legacy convention. Set it when reading a supported Minimum Performance register, preserving zero across cppc-cpufreq initialization and exit. Keep zero-as-omit for Maximum Performance, whose no-limit encoding is all ones, not zero. Initialize amd-pstate's control structure with the new flag clear: its shared-memory backend does not record bios_min_perf, so its zero-valued restore requests must remain omitted. Performance Limited is listed as a required Buffer, but the interface does not depend on it to control performance and the specification permits a platform with no limiting indication to always report zero. Preserve compatibility with firmware that represents that case using a NULL register descriptor instead of disabling CPPC entirely. Emit an error when a present _CPC package fails parsing or initialization so such firmware and resource failures no longer silently suppress cpufreq. Initialize malformed-package failures to -EINVAL and preserve specific allocation, mapping, and unsupported-access errors in that diagnostic. Fixes: 337aadff8e45 ("ACPI: Introduce CPU performance controls using CPPC") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com Suggested-by: Rafael J. Wysocki Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-3-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 183 +++++++++++++++++++++++++++++++---- drivers/cpufreq/amd-pstate.c | 10 +- include/acpi/cppc_acpi.h | 2 + 3 files changed, 169 insertions(+), 26 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 3b8cdf88e31d..965ac29ab8b1 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -129,6 +129,21 @@ static DEFINE_PER_CPU(struct cpc_desc *, cpc_desc_ptr); !!(cpc)->cpc_entry.int_value : \ !IS_NULL_REG(&(cpc)->cpc_entry.reg)) +static bool cpc_is_writable(const struct cpc_register_resource *cpc) +{ + return cpc->type == ACPI_TYPE_BUFFER && + !IS_NULL_REG(&cpc->cpc_entry.reg); +} + +static bool cpc_entry_present(const struct cpc_register_resource *cpc) +{ + if (cpc->type == ACPI_TYPE_INTEGER) + return true; + + return cpc->type == ACPI_TYPE_BUFFER && + !IS_NULL_REG(&cpc->cpc_entry.reg); +} + /* * Each bit indicates the optionality of the register in per-cpu * cpc_regs[] with the corresponding index. 0 means mandatory and 1 @@ -142,6 +157,36 @@ static DEFINE_PER_CPU(struct cpc_desc *, cpc_desc_ptr); */ #define IS_OPTIONAL_CPC_REG(reg_idx) (REG_OPTIONAL & (1U << (reg_idx))) +static bool cpc_integer_entry_valid(unsigned int reg_idx, u64 value, + bool *legacy_null) +{ + *legacy_null = false; + + switch (reg_idx) { + case HIGHEST_PERF: + case NOMINAL_PERF: + case LOW_NON_LINEAR_PERF: + case LOWEST_PERF: + case REFERENCE_PERF: + case LOWEST_FREQ: + case NOMINAL_FREQ: + return value <= U32_MAX; + case CTR_WRAP_TIME: + /* AML Integers and the kernel interface are both 64-bit. */ + return true; + case AUTO_SEL_ENABLE: + return value <= 1; + case DESIRED_PERF: + /* Validated against Autonomous Selection after parsing. */ + *legacy_null = value == 0; + return *legacy_null; + default: + /* Tolerate legacy Integer 0 placeholders for absent options. */ + *legacy_null = value == 0 && IS_OPTIONAL_CPC_REG(reg_idx); + return *legacy_null; + } +} + /* * Arbitrary Retries in case the remote processor is slow to respond * to PCC commands. Keeping it high enough to cover emulators where @@ -150,6 +195,8 @@ static DEFINE_PER_CPU(struct cpc_desc *, cpc_desc_ptr); #define NUM_RETRIES 500ULL #define OVER_16BTS_MASK ~0xFFFFULL +#define CPC_GENERIC_REGISTER_DESCRIPTOR 0x82 +#define CPC_GENERIC_REGISTER_LENGTH (sizeof(struct cpc_reg) - 3) #define define_one_cppc_ro(_name) \ static struct kobj_attribute _name = \ @@ -773,8 +820,10 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) acpi_handle handle = pr->handle; unsigned int num_ent, i, cpc_rev; int pcc_subspace_id = -1; + bool cpc_present = false; acpi_status status; int ret = -ENODATA; + int err; if (!osc_sb_cppc2_support_acked) { pr_debug("CPPC v2 _OSC not acked\n"); @@ -791,6 +840,8 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) ret = -ENODEV; goto out_buf_free; } + cpc_present = true; + ret = -EINVAL; out_obj = (union acpi_object *) output.pointer; if (out_obj->package.count < 2) { @@ -871,11 +922,38 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) cpc_obj = &out_obj->package.elements[i]; if (cpc_obj->type == ACPI_TYPE_INTEGER) { - cpc_ptr->cpc_regs[i-2].type = ACPI_TYPE_INTEGER; - cpc_ptr->cpc_regs[i-2].cpc_entry.int_value = cpc_obj->integer.value; + bool legacy_null; + + if (!cpc_integer_entry_valid(i - 2, + cpc_obj->integer.value, + &legacy_null)) { + pr_debug("Invalid Integer _CPC register %u for CPU:%d\n", + i - 2, pr->id); + ret = -EINVAL; + goto out_free; + } + if (legacy_null) + pr_warn_once(FW_BUG "_CPC register %u uses Integer 0 for an absent Buffer\n", + i - 2); + cpc_ptr->cpc_regs[i - 2].type = ACPI_TYPE_INTEGER; + cpc_ptr->cpc_regs[i - 2].cpc_entry.int_value = cpc_obj->integer.value; } else if (cpc_obj->type == ACPI_TYPE_BUFFER) { + if (cpc_obj->buffer.length < sizeof(*gas_t)) { + pr_debug("Invalid register descriptor for CPU:%d\n", + pr->id); + ret = -EINVAL; + goto out_free; + } + gas_t = (struct cpc_reg *) cpc_obj->buffer.pointer; + if (gas_t->descriptor != CPC_GENERIC_REGISTER_DESCRIPTOR || + gas_t->length != CPC_GENERIC_REGISTER_LENGTH) { + pr_debug("Invalid register resource for CPU:%d\n", + pr->id); + ret = -EINVAL; + goto out_free; + } /* * The PCC Subspace index is encoded inside @@ -886,8 +964,11 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (gas_t->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { if (pcc_subspace_id < 0) { pcc_subspace_id = gas_t->access_width; - if (pcc_data_alloc(pcc_subspace_id)) + err = pcc_data_alloc(pcc_subspace_id); + if (err) { + ret = err; goto out_free; + } } else if (pcc_subspace_id != gas_t->access_width) { pr_debug("Mismatched PCC ids in _CPC for CPU:%d\n", pr->id); @@ -900,14 +981,18 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (!osc_cpc_flexible_adr_space_confirmed) { pr_debug("Flexible address space capability not supported\n"); + ret = -EOPNOTSUPP; if (!cpc_supported_by_cpu()) goto out_free; + ret = -EINVAL; } access_width = GET_BIT_WIDTH(gas_t) / 8; addr = ioremap(gas_t->address, access_width); - if (!addr) + if (!addr) { + ret = -ENOMEM; goto out_free; + } cpc_ptr->cpc_regs[i-2].sys_mem_vaddr = addr; } } else if (gas_t->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { @@ -929,14 +1014,17 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } if (!osc_cpc_flexible_adr_space_confirmed) { pr_debug("Flexible address space capability not supported\n"); + ret = -EOPNOTSUPP; if (!cpc_supported_by_cpu()) goto out_free; + ret = -EINVAL; } } else { if (gas_t->space_id != ACPI_ADR_SPACE_FIXED_HARDWARE || !cpc_ffh_supported()) { /* Support only PCC, SystemMemory, SystemIO, and FFH type regs. */ pr_debug("Unsupported register type (%d) in _CPC\n", gas_t->space_id); + ret = -EOPNOTSUPP; goto out_free; } } @@ -961,15 +1049,35 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } per_cpu(cpu_pcc_subspace_idx, pr->id) = pcc_subspace_id; + /* + * Performance Limited is required by the specification, but tolerate a + * NULL descriptor used by firmware which cannot report limiting events. + * CPPC control does not depend on this status. + */ + for (i = 0; i < num_ent - 2; i++) { + if (i != DESIRED_PERF && i != PERF_LIMITED && + !IS_OPTIONAL_CPC_REG(i) && + !cpc_entry_present(&cpc_ptr->cpc_regs[i])) { + pr_debug("CPU:%d lacks mandatory _CPC register %u\n", + pr->id, i); + ret = -EINVAL; + goto out_free; + } + } + /* * In CPPC v1, DESIRED_PERF is mandatory. In CPPC v2, it is optional * only when AUTO_SEL_ENABLE is supported. */ - if (!CPC_SUPPORTED(&cpc_ptr->cpc_regs[DESIRED_PERF]) && + if (!cpc_is_writable(&cpc_ptr->cpc_regs[DESIRED_PERF]) && (!osc_sb_cppc2_support_acked || - !CPC_SUPPORTED(&cpc_ptr->cpc_regs[AUTO_SEL_ENABLE]))) - pr_warn("Desired perf. register is mandatory if CPPC v2 is not supported " - "or autonomous selection is disabled\n"); + cpc_ptr->cpc_regs[AUTO_SEL_ENABLE].type != ACPI_TYPE_INTEGER || + cpc_ptr->cpc_regs[AUTO_SEL_ENABLE].cpc_entry.int_value != 1)) { + pr_debug("CPU:%d lacks a writable Desired Performance register\n", + pr->id); + ret = -EINVAL; + goto out_free; + } /* * Initialize the remaining cpc_regs as unsupported. @@ -1037,6 +1145,8 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) kfree(cpc_ptr); out_buf_free: + if (cpc_present) + pr_err("CPU%d: failed to initialize _CPC: %d\n", pr->id, ret); kfree(output.pointer); return ret; } @@ -1217,11 +1327,18 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) u64 prev_val; void __iomem *vaddr = NULL; int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, cpu); - struct cpc_reg *reg = ®_res->cpc_entry.reg; + struct cpc_reg *reg; struct cpc_desc *cpc_desc; unsigned long flags; bool locked = false; + if (reg_res->type != ACPI_TYPE_BUFFER) + return -EOPNOTSUPP; + + reg = ®_res->cpc_entry.reg; + if (IS_NULL_REG(reg)) + return -EOPNOTSUPP; + size = GET_BIT_WIDTH(reg); if (IS_ENABLED(CONFIG_HAS_IOPORT) && @@ -1364,7 +1481,9 @@ static int cppc_get_reg_val(int cpu, enum cppc_regs reg_idx, u64 *val) reg = &cpc_desc->cpc_regs[reg_idx]; - if ((reg->type == ACPI_TYPE_INTEGER && IS_OPTIONAL_CPC_REG(reg_idx) && + /* Desired may be absent for immutable autonomous selection. */ + if ((reg->type == ACPI_TYPE_INTEGER && + (IS_OPTIONAL_CPC_REG(reg_idx) || reg_idx == DESIRED_PERF) && !reg->cpc_entry.int_value) || (reg->type != ACPI_TYPE_INTEGER && IS_NULL_REG(®->cpc_entry.reg))) { pr_debug("CPC register is not supported\n"); @@ -1415,7 +1534,7 @@ static int cppc_set_reg_val(int cpu, enum cppc_regs reg_idx, u64 val) reg = &cpc_desc->cpc_regs[reg_idx]; /* if a register is writeable, it must be a buffer and not null */ - if ((reg->type != ACPI_TYPE_BUFFER) || IS_NULL_REG(®->cpc_entry.reg)) { + if (!cpc_is_writable(reg)) { pr_debug("CPC register is not supported\n"); return -EOPNOTSUPP; } @@ -1505,7 +1624,7 @@ int cppc_get_perf_caps(int cpunum, struct cppc_perf_caps *perf_caps) struct cpc_register_resource *highest_reg, *lowest_reg, *lowest_non_linear_reg, *nominal_reg, *reference_reg, *guaranteed_reg, *low_freq_reg = NULL, *nom_freq_reg = NULL; - u64 high, low, guaranteed, nom, ref, min_nonlinear, + u64 high, low, guaranteed = 0, nom, ref, min_nonlinear, low_f = 0, nom_f = 0; int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, cpunum); struct cppc_pcc_data *pcc_ss_data = NULL; @@ -1588,7 +1707,12 @@ int cppc_get_perf_caps(int cpunum, struct cppc_perf_caps *perf_caps) goto out_err; perf_caps->lowest_nonlinear_perf = min_nonlinear; - if (!high || !low || !nom || !ref || !min_nonlinear) { + if (!high || !low || !nom || !ref || !min_nonlinear || + high > U32_MAX || low > U32_MAX || guaranteed > U32_MAX || + nom > U32_MAX || ref > U32_MAX || min_nonlinear > U32_MAX || + high < nom || nom < min_nonlinear || min_nonlinear < low || + (CPC_SUPPORTED(guaranteed_reg) && + (guaranteed < low || guaranteed > nom))) { ret = -EFAULT; goto out_err; } @@ -1605,6 +1729,14 @@ int cppc_get_perf_caps(int cpunum, struct cppc_perf_caps *perf_caps) if (ret) goto out_err; } + /* Require ordered anchors and a nonzero slope when frequencies differ. */ + if (low_f > U32_MAX || nom_f > U32_MAX || + (low_f && nom_f && + (nom_f < low_f || nom < low || + (nom_f != low_f && nom == low)))) { + ret = -EFAULT; + goto out_err; + } perf_caps->lowest_freq = low_f; perf_caps->nominal_freq = nom_f; @@ -1779,6 +1911,9 @@ int cppc_set_epp_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls, bool enable) auto_sel_reg = &cpc_desc->cpc_regs[AUTO_SEL_ENABLE]; epp_set_reg = &cpc_desc->cpc_regs[ENERGY_PERF]; + if (!enable && auto_sel_reg->type == ACPI_TYPE_INTEGER && + auto_sel_reg->cpc_entry.int_value == 1) + return -EOPNOTSUPP; epp_ffh_sysmem = CPC_SUPPORTED(epp_set_reg) && (CPC_IN_FFH(epp_set_reg) || CPC_IN_SYSTEM_MEMORY(epp_set_reg)); @@ -1791,13 +1926,13 @@ int cppc_set_epp_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls, bool enable) return -ENODEV; } - if (CPC_SUPPORTED(auto_sel_reg)) { + if (cpc_is_writable(auto_sel_reg)) { ret = cpc_write(cpu, auto_sel_reg, enable); if (ret) return ret; } - if (CPC_SUPPORTED(epp_set_reg)) { + if (cpc_is_writable(epp_set_reg)) { ret = cpc_write(cpu, epp_set_reg, perf_ctrls->energy_perf); if (ret) return ret; @@ -1996,6 +2131,7 @@ int cppc_get_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) max_perf_reg = &cpc_desc->cpc_regs[MAX_PERF]; energy_perf_reg = &cpc_desc->cpc_regs[ENERGY_PERF]; auto_sel_reg = &cpc_desc->cpc_regs[AUTO_SEL_ENABLE]; + perf_ctrls->min_perf_valid = false; /* Are any of the regs PCC ?*/ if (CPC_IN_PCC(min_perf_reg) || CPC_IN_PCC(max_perf_reg) || @@ -2020,6 +2156,10 @@ int cppc_get_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) ret = cpc_read(cpu, max_perf_reg, &max); if (ret) goto out_err; + if (max > U32_MAX) { + ret = -EFAULT; + goto out_err; + } } perf_ctrls->max_perf = max; @@ -2027,6 +2167,11 @@ int cppc_get_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) ret = cpc_read(cpu, min_perf_reg, &min); if (ret) goto out_err; + if (min > U32_MAX) { + ret = -EFAULT; + goto out_err; + } + perf_ctrls->min_perf_valid = true; } perf_ctrls->min_perf = min; @@ -2113,12 +2258,8 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) if (CPC_SUPPORTED(desired_reg)) cpc_write(cpu, desired_reg, perf_ctrls->desired_perf); - /* - * Only write if min_perf and max_perf not zero. Some drivers pass zero - * value to min and max perf, but they don't mean to set the zero value, - * they just don't want to write to those registers. - */ - if (perf_ctrls->min_perf && CPC_SUPPORTED(min_perf_reg)) + if (CPC_SUPPORTED(min_perf_reg) && + (perf_ctrls->min_perf || perf_ctrls->min_perf_valid)) cpc_write(cpu, min_perf_reg, perf_ctrls->min_perf); if (perf_ctrls->max_perf && CPC_SUPPORTED(max_perf_reg)) cpc_write(cpu, max_perf_reg, perf_ctrls->max_perf); diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 8bfd46d60843..6ee2d43b7b95 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -544,7 +544,11 @@ static int shmem_update_perf(struct cpufreq_policy *policy, u8 min_perf, u8 des_perf, u8 max_perf, u8 epp, bool fast_switch) { struct amd_cpudata *cpudata = policy->driver_data; - struct cppc_perf_ctrls perf_ctrls; + struct cppc_perf_ctrls perf_ctrls = { + .max_perf = max_perf, + .min_perf = min_perf, + .desired_perf = des_perf, + }; u64 value, prev; int ret; @@ -577,10 +581,6 @@ static int shmem_update_perf(struct cpufreq_policy *policy, u8 min_perf, if (value == prev) return 0; - perf_ctrls.max_perf = max_perf; - perf_ctrls.min_perf = min_perf; - perf_ctrls.desired_perf = des_perf; - ret = cppc_set_perf(cpudata->cpu, &perf_ctrls); if (ret) return ret; diff --git a/include/acpi/cppc_acpi.h b/include/acpi/cppc_acpi.h index 94a6277edab2..edf0f6ffb437 100644 --- a/include/acpi/cppc_acpi.h +++ b/include/acpi/cppc_acpi.h @@ -141,6 +141,8 @@ struct cppc_perf_ctrls { u32 desired_perf; u32 energy_perf; bool auto_sel; + /* Allow an explicit zero minimum; otherwise zero omits the update. */ + bool min_perf_valid; }; struct cppc_perf_fb_ctrs { From 3685d9829c7161a05d338cb3b3367c6a76d38ed8 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:48 +0100 Subject: [PATCH 27/89] ACPI: CPPC: Propagate performance-control write errors cppc_set_perf() can skip malformed controls, discard cpc_write() failures, and report success without programming the requested performance tuple. Return every control-write error to the caller and honor the explicit Minimum Performance validity flag, so an explicit zero minimum remains distinct from a legacy omitted bound. Validate every requested PCC field before changing either a direct control or the shared payload; after that check, PCC staging cannot fail partway through a tuple. Multiple Phase-I callers may set the pending flag to true while holding the shared side of pcc_lock. Mark that intentional same-value store with WRITE_ONCE(). A layout which mixes PCC with directly accessed controls needs stronger ordering. A fallible direct write cannot safely run alongside another CPU's staged PCC tuple: if it fails after changing a direct register, neither submitting nor discarding the shared batch can preserve the other request. Serialize the complete mixed transaction with the exclusive PCC lock. Drain an older pending batch before changing a direct control. Check the preceding PCC command for completion, then program the direct and PCC portions and submit the new command synchronously. Use the mixed-layout synchronization even when the current request omits its PCC-backed bounds, so a direct-only update cannot race a prior PCC command. Fixes: 337aadff8e45 ("ACPI: Introduce CPU performance controls using CPPC") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian.loehle%40arm.com Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-4-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 271 ++++++++++++++++++++++++++++++--------- 1 file changed, 207 insertions(+), 64 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 965ac29ab8b1..74feb05195a1 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -240,6 +240,19 @@ show_cppc_data(cppc_get_perf_ctrs, cppc_perf_fb_ctrs, wraparound_time); (reg)->space_id != ACPI_ADR_SPACE_PLATFORM_COMM) ? \ (8 << ((reg)->access_width - 1)) : (reg)->bit_width) +static bool cpc_pcc_write_supported(const struct cpc_register_resource *reg) +{ + switch (GET_BIT_WIDTH(®->cpc_entry.reg)) { + case 8: + case 16: + case 32: + case 64: + return true; + default: + return false; + } +} + /* Shift and apply the mask for CPC reads/writes */ #define MASK_VAL_READ(reg, val) (((val) >> (reg)->bit_offset) & \ GENMASK(((reg)->bit_width) - 1, 0)) @@ -380,13 +393,34 @@ static int check_pcc_chan(int pcc_ss_id, bool chk_err_bit) return ret; } +static void cppc_complete_pcc_write(struct cppc_pcc_data *pcc_ss_data, + int ret) +{ + int i; + + if (unlikely(ret)) { + for_each_possible_cpu(i) { + struct cpc_desc *desc = per_cpu(cpc_desc_ptr, i); + + if (!desc) + continue; + + if (desc->write_cmd_id == pcc_ss_data->pcc_write_cnt) + desc->write_cmd_status = ret; + } + } + + pcc_ss_data->pcc_write_cnt++; + wake_up_all(&pcc_ss_data->pcc_write_wait_q); +} + /* * This function transfers the ownership of the PCC to the platform * So it must be called while holding write_lock(pcc_lock) */ static int send_pcc_cmd(int pcc_ss_id, u16 cmd) { - int ret = -EIO, i; + int ret = -EIO; struct cppc_pcc_data *pcc_ss_data = pcc_data[pcc_ss_id]; struct acpi_pcct_shared_memory __iomem *generic_comm_base = pcc_ss_data->pcc_channel->shmem; @@ -478,21 +512,8 @@ static int send_pcc_cmd(int pcc_ss_id, u16 cmd) mbox_client_txdone(pcc_ss_data->pcc_channel->mchan, ret); end: - if (cmd == CMD_WRITE) { - if (unlikely(ret)) { - for_each_possible_cpu(i) { - struct cpc_desc *desc = per_cpu(cpc_desc_ptr, i); - - if (!desc) - continue; - - if (desc->write_cmd_id == pcc_ss_data->pcc_write_cnt) - desc->write_cmd_status = ret; - } - } - pcc_ss_data->pcc_write_cnt++; - wake_up_all(&pcc_ss_data->pcc_write_wait_q); - } + if (cmd == CMD_WRITE) + cppc_complete_pcc_write(pcc_ss_data, ret); return ret; } @@ -2211,7 +2232,9 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) struct cpc_register_resource *desired_reg, *min_perf_reg, *max_perf_reg; int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, cpu); struct cppc_pcc_data *pcc_ss_data = NULL; - bool regs_in_pcc; + bool desired_update, min_update, max_update; + bool desired_pcc, min_pcc, max_pcc, pcc_update; + bool pcc_layout, direct_layout, mixed_layout; int ret = 0; if (!cpc_desc) { @@ -2222,50 +2245,168 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) desired_reg = &cpc_desc->cpc_regs[DESIRED_PERF]; min_perf_reg = &cpc_desc->cpc_regs[MIN_PERF]; max_perf_reg = &cpc_desc->cpc_regs[MAX_PERF]; - regs_in_pcc = CPC_IN_PCC(desired_reg) || CPC_IN_PCC(min_perf_reg) || - CPC_IN_PCC(max_perf_reg); + desired_update = cpc_is_writable(desired_reg); + min_update = cpc_is_writable(min_perf_reg) && + (perf_ctrls->min_perf || perf_ctrls->min_perf_valid); + max_update = cpc_is_writable(max_perf_reg) && + perf_ctrls->max_perf; + desired_pcc = desired_update && CPC_IN_PCC(desired_reg); + min_pcc = min_update && CPC_IN_PCC(min_perf_reg); + max_pcc = max_update && CPC_IN_PCC(max_perf_reg); + pcc_update = desired_pcc || min_pcc || max_pcc; + pcc_layout = (cpc_is_writable(desired_reg) && CPC_IN_PCC(desired_reg)) || + (cpc_is_writable(min_perf_reg) && CPC_IN_PCC(min_perf_reg)) || + (cpc_is_writable(max_perf_reg) && CPC_IN_PCC(max_perf_reg)); + direct_layout = (cpc_is_writable(desired_reg) && + !CPC_IN_PCC(desired_reg)) || + (cpc_is_writable(min_perf_reg) && + !CPC_IN_PCC(min_perf_reg)) || + (cpc_is_writable(max_perf_reg) && + !CPC_IN_PCC(max_perf_reg)); + mixed_layout = pcc_layout && direct_layout; + + /* Do not modify any control if a requested PCC field cannot be staged. */ + if ((desired_pcc && !cpc_pcc_write_supported(desired_reg)) || + (min_pcc && !cpc_pcc_write_supported(min_perf_reg)) || + (max_pcc && !cpc_pcc_write_supported(max_perf_reg))) + return -EFAULT; + + if (mixed_layout || pcc_update) { + if (pcc_ss_id < 0) { + pr_debug("Invalid pcc_ss_id\n"); + return -ENODEV; + } + pcc_ss_data = pcc_data[pcc_ss_id]; + if (!pcc_ss_data) + return -ENODEV; + } + + /* + * A mixed layout cannot batch fallible direct writes safely: another + * CPU's staged PCC values may no longer match if a direct write fails. + * Serialize the complete mixed transaction and drain an older batch + * before changing a direct control. + */ + if (mixed_layout) { + down_write(&pcc_ss_data->pcc_lock); + if (pcc_ss_data->pending_pcc_write_cmd) { + ret = send_pcc_cmd(pcc_ss_id, CMD_WRITE); + if (ret) + goto out_mixed_unlock; + } + + if (pcc_ss_data->platform_owns_pcc) { + ret = check_pcc_chan(pcc_ss_id, false); + if (ret) + goto out_mixed_unlock; + } + + if (desired_update && !desired_pcc) { + ret = cpc_write(cpu, desired_reg, + perf_ctrls->desired_perf); + if (ret) + goto out_mixed_unlock; + } + if (min_update && !min_pcc) { + ret = cpc_write(cpu, min_perf_reg, + perf_ctrls->min_perf); + if (ret) + goto out_mixed_unlock; + } + if (max_update && !max_pcc) { + ret = cpc_write(cpu, max_perf_reg, + perf_ctrls->max_perf); + if (ret) + goto out_mixed_unlock; + } + + if (desired_pcc) { + ret = cpc_write(cpu, desired_reg, + perf_ctrls->desired_perf); + if (ret) + goto out_mixed_unlock; + } + if (min_pcc) { + ret = cpc_write(cpu, min_perf_reg, + perf_ctrls->min_perf); + if (ret) + goto out_mixed_unlock; + } + if (max_pcc) { + ret = cpc_write(cpu, max_perf_reg, + perf_ctrls->max_perf); + if (ret) + goto out_mixed_unlock; + } + + if (pcc_update) { + WRITE_ONCE(pcc_ss_data->pending_pcc_write_cmd, true); + cpc_desc->write_cmd_id = pcc_ss_data->pcc_write_cnt; + cpc_desc->write_cmd_status = 0; + ret = send_pcc_cmd(pcc_ss_id, CMD_WRITE); + } + +out_mixed_unlock: + up_write(&pcc_ss_data->pcc_lock); + return ret; + } + + /* A request without PCC updates has no payload to coordinate. */ + if (!pcc_update) { + if (desired_update) { + ret = cpc_write(cpu, desired_reg, + perf_ctrls->desired_perf); + if (ret) + return ret; + } + if (min_update) { + ret = cpc_write(cpu, min_perf_reg, + perf_ctrls->min_perf); + if (ret) + return ret; + } + if (max_update) + ret = cpc_write(cpu, max_perf_reg, + perf_ctrls->max_perf); + return ret; + } + + down_read(&pcc_ss_data->pcc_lock); /* BEGIN Phase-I */ + if (pcc_ss_data->platform_owns_pcc) { + ret = check_pcc_chan(pcc_ss_id, false); + if (ret) + goto out_pcc_read_unlock; + } /* * This is Phase-I where we want to write to CPC registers * -> We want all CPUs to be able to execute this phase in parallel * * Since read_lock can be acquired by multiple CPUs simultaneously we - * achieve that goal here + * achieve that goal here. */ - if (regs_in_pcc) { - if (pcc_ss_id < 0) { - pr_debug("Invalid pcc_ss_id\n"); - return -ENODEV; - } - pcc_ss_data = pcc_data[pcc_ss_id]; - down_read(&pcc_ss_data->pcc_lock); /* BEGIN Phase-I */ - if (pcc_ss_data->platform_owns_pcc) { - ret = check_pcc_chan(pcc_ss_id, false); - if (ret) { - up_read(&pcc_ss_data->pcc_lock); - return ret; - } - } - /* - * Update the pending_write to make sure a PCC CMD_READ will not - * arrive and steal the channel during the switch to write lock - */ - pcc_ss_data->pending_pcc_write_cmd = true; - cpc_desc->write_cmd_id = pcc_ss_data->pcc_write_cnt; - cpc_desc->write_cmd_status = 0; + if (desired_pcc) { + ret = cpc_write(cpu, desired_reg, perf_ctrls->desired_perf); + if (ret) + goto out_pcc_read_unlock; } - if (CPC_SUPPORTED(desired_reg)) - cpc_write(cpu, desired_reg, perf_ctrls->desired_perf); + if (min_pcc) { + ret = cpc_write(cpu, min_perf_reg, perf_ctrls->min_perf); + if (ret) + goto out_pcc_read_unlock; + } + if (max_pcc) { + ret = cpc_write(cpu, max_perf_reg, perf_ctrls->max_perf); + if (ret) + goto out_pcc_read_unlock; + } - if (CPC_SUPPORTED(min_perf_reg) && - (perf_ctrls->min_perf || perf_ctrls->min_perf_valid)) - cpc_write(cpu, min_perf_reg, perf_ctrls->min_perf); - if (perf_ctrls->max_perf && CPC_SUPPORTED(max_perf_reg)) - cpc_write(cpu, max_perf_reg, perf_ctrls->max_perf); - - if (regs_in_pcc) - up_read(&pcc_ss_data->pcc_lock); /* END Phase-I */ + /* Block a PCC read until the staged payload has been submitted. */ + WRITE_ONCE(pcc_ss_data->pending_pcc_write_cmd, true); + cpc_desc->write_cmd_id = pcc_ss_data->pcc_write_cnt; + cpc_desc->write_cmd_status = 0; + up_read(&pcc_ss_data->pcc_lock); /* END Phase-I */ /* * This is Phase-II where we transfer the ownership of PCC to Platform * @@ -2312,20 +2453,22 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) * case during a CMD_READ and if there are pending writes it delivers * the write command before servicing the read command */ - if (regs_in_pcc) { - if (down_write_trylock(&pcc_ss_data->pcc_lock)) {/* BEGIN Phase-II */ - /* Update only if there are pending write commands */ - if (pcc_ss_data->pending_pcc_write_cmd) - send_pcc_cmd(pcc_ss_id, CMD_WRITE); - up_write(&pcc_ss_data->pcc_lock); /* END Phase-II */ - } else - /* Wait until pcc_write_cnt is updated by send_pcc_cmd */ - wait_event(pcc_ss_data->pcc_write_wait_q, - cpc_desc->write_cmd_id != pcc_ss_data->pcc_write_cnt); - - /* send_pcc_cmd updates the status in case of failure */ - ret = cpc_desc->write_cmd_status; + if (down_write_trylock(&pcc_ss_data->pcc_lock)) {/* BEGIN Phase-II */ + /* Update only if there are pending write commands */ + if (pcc_ss_data->pending_pcc_write_cmd) + send_pcc_cmd(pcc_ss_id, CMD_WRITE); + up_write(&pcc_ss_data->pcc_lock); /* END Phase-II */ + } else { + /* Wait until pcc_write_cnt is updated by send_pcc_cmd */ + wait_event(pcc_ss_data->pcc_write_wait_q, + cpc_desc->write_cmd_id != pcc_ss_data->pcc_write_cnt); } + + /* send_pcc_cmd updates the status in case of failure */ + return cpc_desc->write_cmd_status; + +out_pcc_read_unlock: + up_read(&pcc_ss_data->pcc_lock); return ret; } EXPORT_SYMBOL_GPL(cppc_set_perf); From 50affbde6410825b7bd6c0f4d12dab40ec987021 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:49 +0100 Subject: [PATCH 28/89] ACPI: CPPC: Serialize PCC single-register payload updates The PCC doorbell protocol requires OSPM to confirm ownership of the shared subspace before placing a command and its payload there. cppc_set_reg_val_in_pcc() instead modifies the payload before taking pcc_lock. A concurrent command can consequently overwrite or consume the staged value, and OSPM can write the shared region while the platform still owns it. Reject a register width which cpc_write() cannot stage before taking the exclusive lock. Otherwise that operation can steal Phase II from an older valid performance batch and then abort it for an unrelated error. Take the PCC write lock first, wait for the previous command to complete, and keep the lock held while staging the value and submitting CMD_WRITE. This follows the ownership sequence in ACPI 6.5 Section 14.5 and the existing contract documented by send_pcc_cmd(). If ownership acquisition or staging fails, abort any older performance batch before dropping the exclusive lock. This advances its generation and wakes cppc_set_perf() callers which otherwise wait indefinitely for a command this path did not submit. The generation counter is local to each PCC subspace, while error completion scans the descriptors of every CPU. Match both the subspace and generation so failure in one subspace cannot poison a request using the same generation in another. Fixes: e05c75072c2e ("ACPI: CPPC: Add cppc_set_reg_val()") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian.loehle%40arm.com Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-5-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 44 ++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 74feb05195a1..42f3714f82ab 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -393,8 +393,8 @@ static int check_pcc_chan(int pcc_ss_id, bool chk_err_bit) return ret; } -static void cppc_complete_pcc_write(struct cppc_pcc_data *pcc_ss_data, - int ret) +static void cppc_complete_pcc_write(int pcc_ss_id, + struct cppc_pcc_data *pcc_ss_data, int ret) { int i; @@ -402,7 +402,8 @@ static void cppc_complete_pcc_write(struct cppc_pcc_data *pcc_ss_data, for_each_possible_cpu(i) { struct cpc_desc *desc = per_cpu(cpc_desc_ptr, i); - if (!desc) + if (!desc || + per_cpu(cpu_pcc_subspace_idx, i) != pcc_ss_id) continue; if (desc->write_cmd_id == pcc_ss_data->pcc_write_cnt) @@ -414,6 +415,18 @@ static void cppc_complete_pcc_write(struct cppc_pcc_data *pcc_ss_data, wake_up_all(&pcc_ss_data->pcc_write_wait_q); } +/* The caller must hold pcc_lock for write. */ +static void cppc_abort_pending_pcc_write(int pcc_ss_id, + struct cppc_pcc_data *pcc_ss_data, + int ret) +{ + if (!pcc_ss_data->pending_pcc_write_cmd) + return; + + pcc_ss_data->pending_pcc_write_cmd = false; + cppc_complete_pcc_write(pcc_ss_id, pcc_ss_data, ret); +} + /* * This function transfers the ownership of the PCC to the platform * So it must be called while holding write_lock(pcc_lock) @@ -513,7 +526,7 @@ static int send_pcc_cmd(int pcc_ss_id, u16 cmd) end: if (cmd == CMD_WRITE) - cppc_complete_pcc_write(pcc_ss_data, ret); + cppc_complete_pcc_write(pcc_ss_id, pcc_ss_data, ret); return ret; } @@ -1520,23 +1533,36 @@ static int cppc_get_reg_val(int cpu, enum cppc_regs reg_idx, u64 *val) static int cppc_set_reg_val_in_pcc(int cpu, struct cpc_register_resource *reg, u64 val) { int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, cpu); - struct cppc_pcc_data *pcc_ss_data = NULL; + struct cppc_pcc_data *pcc_ss_data; int ret; if (pcc_ss_id < 0) { pr_debug("Invalid pcc_ss_id\n"); return -ENODEV; } + if (!cpc_pcc_write_supported(reg)) + return -EFAULT; + + pcc_ss_data = pcc_data[pcc_ss_id]; + if (!pcc_ss_data) + return -ENODEV; + + down_write(&pcc_ss_data->pcc_lock); + + ret = check_pcc_chan(pcc_ss_id, false); + if (ret) + goto out; ret = cpc_write(cpu, reg, val); if (ret) - return ret; + goto out; - pcc_ss_data = pcc_data[pcc_ss_id]; - - down_write(&pcc_ss_data->pcc_lock); /* after writing CPC, transfer the ownership of PCC to platform */ ret = send_pcc_cmd(pcc_ss_id, CMD_WRITE); + +out: + if (ret) + cppc_abort_pending_pcc_write(pcc_ss_id, pcc_ss_data, ret); up_write(&pcc_ss_data->pcc_lock); return ret; From 76e6914a843ed2bc2bea152c30cc7dd445d02276 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:50 +0100 Subject: [PATCH 29/89] ACPI: CPPC: Serialize PCC EPP payload updates cppc_set_epp_perf() stages Autonomous Selection and Energy Performance Preference in the PCC shared region before taking pcc_lock. The platform may still own the subspace, or a concurrent command may consume or overwrite only part of the new payload. Take the PCC write lock and wait for OSPM ownership before staging either control. Keep the lock held until the complete payload has been submitted with CMD_WRITE, so firmware cannot observe a mixed transaction. For a mixed PCC/non-PCC description, complete every fallible non-PCC write before staging PCC data. Cross-address-space updates cannot be atomic, but a non-PCC failure can no longer leave an unsent value in shared memory for a later PCC command to consume. Reject an unsupported PCC width before any non-PCC control is changed. Classify every writable control as either PCC or non-PCC. For SystemIO, require port-I/O support and an access within the port range. Require natural alignment on non-x86 platforms, whose port-I/O accessors use MMIO; x86 port instructions support unaligned addresses. Also require a field covering the complete access unit. cpc_write() performs a raw whole-port write, so any other geometry could update the wrong bits or corrupt an adjacent field. If ownership acquisition or PCC staging fails, abort any older pending performance batch before releasing the exclusive lock so its Phase-II waiters receive the error instead of sleeping indefinitely. This follows the PCC ownership sequence in ACPI 6.5 Section 14.5. Fixes: 7bc1fcd39901 ("ACPI: CPPC: Add AMD pstate energy performance preference cppc control") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian.loehle%40arm.com Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-6-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 104 +++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 42f3714f82ab..2198376a6a9c 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -260,6 +260,22 @@ static bool cpc_pcc_write_supported(const struct cpc_register_resource *reg) ((((val) & GENMASK(((reg)->bit_width) - 1, 0)) << (reg)->bit_offset) | \ ((prev_val) & ~(GENMASK(((reg)->bit_width) - 1, 0) << (reg)->bit_offset))) \ +static bool +cpc_system_io_write_supported(const struct cpc_register_resource *reg) +{ + const struct cpc_reg *gas = ®->cpc_entry.reg; + unsigned int access_width = GET_BIT_WIDTH(gas); + u64 access_size; + + if (!IS_ENABLED(CONFIG_HAS_IOPORT) || !CPC_IN_SYSTEM_IO(reg) || + (access_width != 8 && access_width != 16 && access_width != 32)) + return false; + + access_size = access_width / 8; + return !gas->bit_offset && gas->bit_width == access_width && + gas->address <= U16_MAX - (access_size - 1) && + (IS_ENABLED(CONFIG_X86) || IS_ALIGNED(gas->address, access_size)); +} static u64 cpc_sysmem_access_size(const struct cpc_register_resource *reg) { const struct cpc_reg *gas = ®->cpc_entry.reg; @@ -1947,8 +1963,10 @@ int cppc_set_epp_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls, bool enable) struct cpc_register_resource *auto_sel_reg; struct cpc_desc *cpc_desc = per_cpu(cpc_desc_ptr, cpu); struct cppc_pcc_data *pcc_ss_data = NULL; - bool autosel_ffh_sysmem; - bool epp_ffh_sysmem; + bool auto_sel_pcc; + bool auto_sel_non_pcc; + bool epp_pcc; + bool epp_non_pcc; int ret; if (!cpc_desc) { @@ -1962,52 +1980,74 @@ int cppc_set_epp_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls, bool enable) auto_sel_reg->cpc_entry.int_value == 1) return -EOPNOTSUPP; - epp_ffh_sysmem = CPC_SUPPORTED(epp_set_reg) && - (CPC_IN_FFH(epp_set_reg) || CPC_IN_SYSTEM_MEMORY(epp_set_reg)); - autosel_ffh_sysmem = CPC_SUPPORTED(auto_sel_reg) && - (CPC_IN_FFH(auto_sel_reg) || CPC_IN_SYSTEM_MEMORY(auto_sel_reg)); + auto_sel_pcc = cpc_is_writable(auto_sel_reg) && + CPC_IN_PCC(auto_sel_reg); + epp_pcc = cpc_is_writable(epp_set_reg) && CPC_IN_PCC(epp_set_reg); + if ((auto_sel_pcc && !cpc_pcc_write_supported(auto_sel_reg)) || + (epp_pcc && !cpc_pcc_write_supported(epp_set_reg))) + return -EFAULT; + if ((cpc_is_writable(auto_sel_reg) && + CPC_IN_SYSTEM_IO(auto_sel_reg) && + !cpc_system_io_write_supported(auto_sel_reg)) || + (cpc_is_writable(epp_set_reg) && CPC_IN_SYSTEM_IO(epp_set_reg) && + !cpc_system_io_write_supported(epp_set_reg))) + return -EOPNOTSUPP; - if (CPC_IN_PCC(epp_set_reg) || CPC_IN_PCC(auto_sel_reg)) { + auto_sel_non_pcc = cpc_is_writable(auto_sel_reg) && !auto_sel_pcc; + epp_non_pcc = cpc_is_writable(epp_set_reg) && !epp_pcc; + + /* Complete fallible non-PCC writes before staging PCC data. */ + if (auto_sel_non_pcc) { + ret = cpc_write(cpu, auto_sel_reg, enable); + if (ret) + return ret; + } + if (epp_non_pcc) { + ret = cpc_write(cpu, epp_set_reg, perf_ctrls->energy_perf); + if (ret) + return ret; + } + + if (epp_pcc || auto_sel_pcc) { if (pcc_ss_id < 0) { pr_debug("Invalid pcc_ss_id for CPU:%d\n", cpu); return -ENODEV; } - if (cpc_is_writable(auto_sel_reg)) { - ret = cpc_write(cpu, auto_sel_reg, enable); - if (ret) - return ret; - } - - if (cpc_is_writable(epp_set_reg)) { - ret = cpc_write(cpu, epp_set_reg, perf_ctrls->energy_perf); - if (ret) - return ret; - } - pcc_ss_data = pcc_data[pcc_ss_id]; + if (!pcc_ss_data) + return -ENODEV; down_write(&pcc_ss_data->pcc_lock); - /* after writing CPC, transfer the ownership of PCC to platform */ - ret = send_pcc_cmd(pcc_ss_id, CMD_WRITE); - up_write(&pcc_ss_data->pcc_lock); - } else if (osc_cpc_flexible_adr_space_confirmed && - (epp_ffh_sysmem || autosel_ffh_sysmem)) { - if (autosel_ffh_sysmem) { + + ret = check_pcc_chan(pcc_ss_id, false); + if (ret) + goto out_unlock; + + if (auto_sel_pcc) { ret = cpc_write(cpu, auto_sel_reg, enable); if (ret) - return ret; + goto out_unlock; } - if (epp_ffh_sysmem) { - ret = cpc_write(cpu, epp_set_reg, - perf_ctrls->energy_perf); + if (epp_pcc) { + ret = cpc_write(cpu, epp_set_reg, perf_ctrls->energy_perf); if (ret) - return ret; + goto out_unlock; } + + /* after writing CPC, transfer the ownership of PCC to platform */ + ret = send_pcc_cmd(pcc_ss_id, CMD_WRITE); + +out_unlock: + if (ret) + cppc_abort_pending_pcc_write(pcc_ss_id, pcc_ss_data, ret); + up_write(&pcc_ss_data->pcc_lock); + } else if (epp_non_pcc || auto_sel_non_pcc) { + ret = 0; } else { - ret = -ENOTSUPP; - pr_debug("_CPC in PCC/FFH/SystemMemory are not supported\n"); + ret = -EOPNOTSUPP; + pr_debug("No writable EPP controls for CPU:%d\n", cpu); } return ret; From 27954e29d7de66e65ef1be4ce5a50505066c9bb6 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:51 +0100 Subject: [PATCH 30/89] ACPI: CPPC: Release CPC descriptors through kobject The CPC descriptor embeds the kobject used for its sysfs directory, but it has no release callback and processor exit frees the descriptor immediately after kobject_put(). It also unmaps register resources and releases PCC state before sysfs removal has drained active attribute callbacks. Provide a release callback which unmaps and frees the descriptor, and use the same cleanup helper before kobject initialization. Once initialized, let kobject_put() own descriptor cleanup so its error path cannot free the object twice. On processor exit, first unpublish the per-CPU descriptor and remove its sysfs directory, then release PCC state and drop the kobject reference. This keeps all resources valid while sysfs callbacks are active and also works with delayed kobject release. The frequency-invariance teardown can run after processor removal has unpublished a descriptor. Remember which PCC work items were initialized so policy exit drains them without looking up that descriptor. Also make the counter-transport query tolerate a missing descriptor. Fixes: 158c998ea44b ("ACPI / CPPC: add sysfs support to compute delivered performance") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-7-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 52 +++++++++++++++++++++------------- drivers/cpufreq/cppc_cpufreq.c | 15 +++++----- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 2198376a6a9c..0f0cce383007 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -373,7 +373,27 @@ static struct attribute *cppc_attrs[] = { }; ATTRIBUTE_GROUPS(cppc); +static void cppc_free_desc(struct cpc_desc *cpc_ptr) +{ + unsigned int i; + + for (i = 2; i < cpc_ptr->num_entries; i++) { + void __iomem *addr = cpc_ptr->cpc_regs[i - 2].sys_mem_vaddr; + + if (addr) + iounmap(addr); + } + + kfree(cpc_ptr); +} + +static void cppc_kobj_release(struct kobject *kobj) +{ + cppc_free_desc(to_cpc_desc(kobj)); +} + static const struct kobj_type cppc_ktype = { + .release = cppc_kobj_release, .sysfs_ops = &kobj_sysfs_ops, .default_groups = cppc_groups, }; @@ -1178,21 +1198,14 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (ret) { per_cpu(cpc_desc_ptr, pr->id) = NULL; kobject_put(&cpc_ptr->kobj); - goto out_free; + goto out_buf_free; } kfree(output.pointer); return 0; out_free: - /* Free all the mapped sys mem areas for this CPU */ - for (i = 2; i < cpc_ptr->num_entries; i++) { - void __iomem *addr = cpc_ptr->cpc_regs[i-2].sys_mem_vaddr; - - if (addr) - iounmap(addr); - } - kfree(cpc_ptr); + cppc_free_desc(cpc_ptr); out_buf_free: if (cpc_present) @@ -1211,10 +1224,14 @@ EXPORT_SYMBOL_GPL(acpi_cppc_processor_probe); void acpi_cppc_processor_exit(struct acpi_processor *pr) { struct cpc_desc *cpc_ptr; - unsigned int i; - void __iomem *addr; int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, pr->id); + cpc_ptr = per_cpu(cpc_desc_ptr, pr->id); + if (cpc_ptr) { + per_cpu(cpc_desc_ptr, pr->id) = NULL; + kobject_del(&cpc_ptr->kobj); + } + if (pcc_ss_id >= 0 && pcc_data[pcc_ss_id]) { if (pcc_data[pcc_ss_id]->pcc_channel_acquired) { pcc_data[pcc_ss_id]->refcount--; @@ -1225,20 +1242,12 @@ void acpi_cppc_processor_exit(struct acpi_processor *pr) } } } + per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; - cpc_ptr = per_cpu(cpc_desc_ptr, pr->id); if (!cpc_ptr) return; - /* Free all the mapped sys mem areas for this CPU */ - for (i = 2; i < cpc_ptr->num_entries; i++) { - addr = cpc_ptr->cpc_regs[i-2].sys_mem_vaddr; - if (addr) - iounmap(addr); - } - kobject_put(&cpc_ptr->kobj); - kfree(cpc_ptr); } EXPORT_SYMBOL_GPL(acpi_cppc_processor_exit); @@ -1822,6 +1831,9 @@ bool cppc_perf_ctrs_in_pcc_cpu(unsigned int cpu) { struct cpc_desc *cpc_desc = per_cpu(cpc_desc_ptr, cpu); + if (!cpc_desc) + return false; + return CPC_IN_PCC(&cpc_desc->cpc_regs[DELIVERED_CTR]) || CPC_IN_PCC(&cpc_desc->cpc_regs[REFERENCE_CTR]) || CPC_IN_PCC(&cpc_desc->cpc_regs[CTR_WRAP_TIME]); diff --git a/drivers/cpufreq/cppc_cpufreq.c b/drivers/cpufreq/cppc_cpufreq.c index 80893844353c..af46f913e907 100644 --- a/drivers/cpufreq/cppc_cpufreq.c +++ b/drivers/cpufreq/cppc_cpufreq.c @@ -41,6 +41,7 @@ MODULE_PARM_DESC(fie_disabled, "Disable Frequency Invariance Engine (FIE)"); /* Frequency invariance support */ struct cppc_freq_invariance { int cpu; + bool pcc_work_initialized; struct irq_work irq_work; struct kthread_work work; struct cppc_perf_fb_ctrs prev_perf_fb_ctrs; @@ -163,6 +164,7 @@ static void cppc_cpufreq_cpu_fie_init(struct cpufreq_policy *policy) if (cppc_perf_ctrs_in_pcc_cpu(cpu)) { kthread_init_work(&cppc_fi->work, cppc_scale_freq_workfn); init_irq_work(&cppc_fi->irq_work, cppc_irq_work); + cppc_fi->pcc_work_initialized = true; sftd = &cppc_sftd_pcc; } @@ -184,12 +186,8 @@ static void cppc_cpufreq_cpu_fie_init(struct cpufreq_policy *policy) } /* - * We free all the resources on policy's removal and not on CPU removal as the - * irq-work are per-cpu and the hotplug core takes care of flushing the pending - * irq-works (hint: smpcfd_dying_cpu()) on CPU hotplug. Even if the kthread-work - * fires on another CPU after the concerned CPU is removed, it won't harm. - * - * We just need to make sure to remove them all on policy->exit(). + * Drain work initialized by this policy even if processor removal has + * already unpublished the CPU's CPC descriptor. */ static void cppc_cpufreq_cpu_fie_exit(struct cpufreq_policy *policy) { @@ -203,11 +201,12 @@ static void cppc_cpufreq_cpu_fie_exit(struct cpufreq_policy *policy) topology_clear_scale_freq_source(SCALE_FREQ_SOURCE_CPPC, policy->related_cpus); for_each_cpu(cpu, policy->related_cpus) { - if (!cppc_perf_ctrs_in_pcc_cpu(cpu)) - continue; cppc_fi = &per_cpu(cppc_freq_inv, cpu); + if (!cppc_fi->pcc_work_initialized) + continue; irq_work_sync(&cppc_fi->irq_work); kthread_cancel_work_sync(&cppc_fi->work); + cppc_fi->pcc_work_initialized = false; } } From 968e29db97bbe45aecb5dac51ffbedbf62eb67de Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:52 +0100 Subject: [PATCH 31/89] ACPI: CPPC: Release PCC data after probe failures pcc_data_alloc() takes a per-processor reference while parsing the first PCC register. Every later probe failure currently frees the CPC descriptor without dropping that reference. Depending on where probe fails, this leaks an unacquired PCC object, an acquired mailbox channel, or an extra reference to a shared channel. Factor the teardown into pcc_data_put() so it handles both acquired and unacquired state, and invoke it on every failure after allocation. Allocation, reference updates, channel setup, and teardown of a shared subspace are also unsynchronized. Concurrent processor probe and removal can lose a reference update, free a live object, or request and initialize the same mailbox channel twice. Protect that lifecycle with a mutex and publish pcc_channel_acquired only after its lock and wait queue have been initialized. Do not store the temporary pcc_data_alloc() result in the eventual probe return value. A successful allocation must not make a later parsing failure run cleanup and then return success. A later processor-start step can fail after CPPC probe has published its descriptor. Reuse that descriptor when processor start is retried instead of resetting its PCC index and reparsing _CPC. This prevents the live descriptor from becoming detached from its PCC reference. The per-CPU PCC subspace index is zero-initialized. If probe returns before assigning it, a later processor exit can consequently drop the reference for subspace 0 even though this CPU never acquired one. Initialize the index to -1 before any probe return and only release it from exit after a CPC descriptor has been published. Fixes: 85b1407bf6d2 ("ACPI / CPPC: Make CPPC ACPI driver aware of PCC subspace IDs") Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-8-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 157 +++++++++++++++++++++++++-------------- 1 file changed, 103 insertions(+), 54 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 0f0cce383007..2ad45725f3e0 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -36,6 +36,7 @@ #include #include #include +#include #include #include #include @@ -81,6 +82,7 @@ struct cppc_pcc_data { /* Array to represent the PCC channel per subspace ID */ static struct cppc_pcc_data *pcc_data[MAX_PCC_SUBSPACES]; +static DEFINE_MUTEX(pcc_data_lock); /* The cpu_pcc_subspace_idx contains per CPU subspace ID */ static DEFINE_PER_CPU(int, cpu_pcc_subspace_idx); @@ -760,35 +762,51 @@ EXPORT_SYMBOL_GPL(acpi_get_psd_map); static int register_pcc_channel(int pcc_ss_idx) { + struct cppc_pcc_data *data; struct pcc_mbox_chan *pcc_chan; u64 usecs_lat; + int ret = 0; - if (pcc_ss_idx >= 0) { - pcc_chan = pcc_mbox_request_channel(&cppc_mbox_cl, pcc_ss_idx); + if (pcc_ss_idx < 0 || pcc_ss_idx >= MAX_PCC_SUBSPACES) + return -EINVAL; - if (IS_ERR(pcc_chan)) { - pr_err("Failed to find PCC channel for subspace %d\n", - pcc_ss_idx); - return -ENODEV; - } + mutex_lock(&pcc_data_lock); + data = pcc_data[pcc_ss_idx]; + if (!data) { + ret = -ENODEV; + goto out_unlock; + } + if (data->pcc_channel_acquired) + goto out_unlock; - pcc_data[pcc_ss_idx]->pcc_channel = pcc_chan; - /* - * cppc_ss->latency is just a Nominal value. In reality - * the remote processor could be much slower to reply. - * So add an arbitrary amount of wait on top of Nominal. - */ - usecs_lat = NUM_RETRIES * pcc_chan->latency; - pcc_data[pcc_ss_idx]->deadline_us = usecs_lat; - pcc_data[pcc_ss_idx]->pcc_mrtt = pcc_chan->min_turnaround_time; - pcc_data[pcc_ss_idx]->pcc_mpar = pcc_chan->max_access_rate; - pcc_data[pcc_ss_idx]->pcc_nominal = pcc_chan->latency; - - /* Set flag so that we don't come here for each CPU. */ - pcc_data[pcc_ss_idx]->pcc_channel_acquired = true; + pcc_chan = pcc_mbox_request_channel(&cppc_mbox_cl, pcc_ss_idx); + if (IS_ERR(pcc_chan)) { + pr_err("Failed to find PCC channel for subspace %d\n", + pcc_ss_idx); + ret = -ENODEV; + goto out_unlock; } - return 0; + data->pcc_channel = pcc_chan; + /* + * cppc_ss->latency is just a Nominal value. In reality + * the remote processor could be much slower to reply. + * So add an arbitrary amount of wait on top of Nominal. + */ + usecs_lat = NUM_RETRIES * pcc_chan->latency; + data->deadline_us = usecs_lat; + data->pcc_mrtt = pcc_chan->min_turnaround_time; + data->pcc_mpar = pcc_chan->max_access_rate; + data->pcc_nominal = pcc_chan->latency; + init_rwsem(&data->pcc_lock); + init_waitqueue_head(&data->pcc_write_wait_q); + + /* Reuse this channel when another CPU references the same subspace. */ + data->pcc_channel_acquired = true; + +out_unlock: + mutex_unlock(&pcc_data_lock); + return ret; } /** @@ -830,19 +848,49 @@ bool __weak cpc_supported_by_cpu(void) */ static int pcc_data_alloc(int pcc_ss_id) { + struct cppc_pcc_data *data; + int ret = 0; + if (pcc_ss_id < 0 || pcc_ss_id >= MAX_PCC_SUBSPACES) return -EINVAL; - if (pcc_data[pcc_ss_id]) { - pcc_data[pcc_ss_id]->refcount++; - } else { - pcc_data[pcc_ss_id] = kzalloc_obj(struct cppc_pcc_data); - if (!pcc_data[pcc_ss_id]) - return -ENOMEM; - pcc_data[pcc_ss_id]->refcount++; + mutex_lock(&pcc_data_lock); + data = pcc_data[pcc_ss_id]; + if (!data) { + data = kzalloc_obj(struct cppc_pcc_data); + if (!data) { + ret = -ENOMEM; + goto out_unlock; + } + pcc_data[pcc_ss_id] = data; } + data->refcount++; - return 0; +out_unlock: + mutex_unlock(&pcc_data_lock); + return ret; +} + +static void pcc_data_put(int pcc_ss_id) +{ + struct cppc_pcc_data *data; + + if (pcc_ss_id < 0 || pcc_ss_id >= MAX_PCC_SUBSPACES) + return; + + mutex_lock(&pcc_data_lock); + data = pcc_data[pcc_ss_id]; + if (!data || --data->refcount) + goto out_unlock; + + pcc_data[pcc_ss_id] = NULL; + if (data->pcc_channel_acquired) + pcc_mbox_free_channel(data->pcc_channel); + + kfree(data); + +out_unlock: + mutex_unlock(&pcc_data_lock); } /* @@ -890,11 +938,17 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) acpi_handle handle = pr->handle; unsigned int num_ent, i, cpc_rev; int pcc_subspace_id = -1; + bool pcc_data_ref = false; bool cpc_present = false; acpi_status status; int ret = -ENODATA; int err; + if (per_cpu(cpc_desc_ptr, pr->id)) + return 0; + + per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; + if (!osc_sb_cppc2_support_acked) { pr_debug("CPPC v2 _OSC not acked\n"); if (!cpc_supported_by_cpu()) { @@ -1039,6 +1093,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) ret = err; goto out_free; } + pcc_data_ref = true; } else if (pcc_subspace_id != gas_t->access_width) { pr_debug("Mismatched PCC ids in _CPC for CPU:%d\n", pr->id); @@ -1171,13 +1226,10 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; /* Register PCC channel once for all PCC subspace ID. */ - if (pcc_subspace_id >= 0 && !pcc_data[pcc_subspace_id]->pcc_channel_acquired) { + if (pcc_subspace_id >= 0) { ret = register_pcc_channel(pcc_subspace_id); if (ret) goto out_free; - - init_rwsem(&pcc_data[pcc_subspace_id]->pcc_lock); - init_waitqueue_head(&pcc_data[pcc_subspace_id]->pcc_write_wait_q); } /* Everything looks okay */ @@ -1198,7 +1250,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (ret) { per_cpu(cpc_desc_ptr, pr->id) = NULL; kobject_put(&cpc_ptr->kobj); - goto out_buf_free; + goto out_pcc_put; } kfree(output.pointer); @@ -1207,6 +1259,11 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) out_free: cppc_free_desc(cpc_ptr); +out_pcc_put: + if (pcc_data_ref) + pcc_data_put(pcc_subspace_id); + per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; + out_buf_free: if (cpc_present) pr_err("CPU%d: failed to initialize _CPC: %d\n", pr->id, ret); @@ -1224,28 +1281,20 @@ EXPORT_SYMBOL_GPL(acpi_cppc_processor_probe); void acpi_cppc_processor_exit(struct acpi_processor *pr) { struct cpc_desc *cpc_ptr; - int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, pr->id); + int pcc_ss_id; cpc_ptr = per_cpu(cpc_desc_ptr, pr->id); - if (cpc_ptr) { - per_cpu(cpc_desc_ptr, pr->id) = NULL; - kobject_del(&cpc_ptr->kobj); - } - - if (pcc_ss_id >= 0 && pcc_data[pcc_ss_id]) { - if (pcc_data[pcc_ss_id]->pcc_channel_acquired) { - pcc_data[pcc_ss_id]->refcount--; - if (!pcc_data[pcc_ss_id]->refcount) { - pcc_mbox_free_channel(pcc_data[pcc_ss_id]->pcc_channel); - kfree(pcc_data[pcc_ss_id]); - pcc_data[pcc_ss_id] = NULL; - } - } - } - per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; - - if (!cpc_ptr) + if (!cpc_ptr) { + per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; return; + } + + pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, pr->id); + per_cpu(cpc_desc_ptr, pr->id) = NULL; + kobject_del(&cpc_ptr->kobj); + + pcc_data_put(pcc_ss_id); + per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; kobject_put(&cpc_ptr->kobj); } From 2221dc547b38eeef4893abb0f55352ebd920ddcb Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:53 +0100 Subject: [PATCH 32/89] ACPI: CPPC: Reject unsafe cross-CPU SystemMemory RMW A SystemMemory CPPC field narrower than its access unit needs a read-modify-write operation. The per-CPC-descriptor lock cannot serialize access units described by different processors, so concurrent writers can lose updates. _PSD domains do not define physical register ownership. Track SystemMemory access units in a probe-time interval tree and reject cross-descriptor partial writers sharing an access unit. Allow exact aliases only for aligned native-width accesses to the same logical field. Reject overlapping writable logical fields and full-width writes which would overwrite another field. Disjoint read-only fields may share a larger read access unit with a writer. Within one descriptor, mark both overlapping accesses for the RMW lock, including a full-width byte beside a wider partial field. Count only controls written by the CPPC library as competing writers, so the unused OSPM Nominal Performance control does not trigger a writer conflict. Mark successful relaxed MMIO writes pending before releasing the raw spinlock. On architectures which need an MMIO write barrier during lock handoff, raw_spin_unlock_irqrestore() then orders the write before the next writer enters the RMW critical section. Validate the GAS geometry and supported access widths. Require natural alignment where the architecture's MMIO accessors need it. Exact writable aliases require alignment on x86 too, because an unaligned instruction need not be one indivisible device transaction. Keep validation results separate from the probe return value, so a successful geometry check cannot make a later mapping failure return success. Remove the interval records during teardown and probe rollback. Fixes: 60949b7b8054 ("ACPI: CPPC: Fix MASK_VAL() usage") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian-loehle%40arm.com Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-9-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 415 +++++++++++++++++++++++++++++++++++---- include/acpi/cppc_acpi.h | 5 +- 2 files changed, 382 insertions(+), 38 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 2ad45725f3e0..131dd3be1ec3 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -34,9 +34,12 @@ #define pr_fmt(fmt) "ACPI CPPC: " fmt #include +#include #include #include +#include #include +#include #include #include #include @@ -95,6 +98,40 @@ static DEFINE_PER_CPU(int, cpu_pcc_subspace_idx); */ static DEFINE_PER_CPU(struct cpc_desc *, cpc_desc_ptr); +struct cpc_sysmem_node { + struct rb_node rb; + u64 subtree_last; + u64 start; + u64 last; + struct cpc_desc *desc; + unsigned int reg_idx; + struct list_head aliases; + struct list_head alias_node; + struct cpc_sysmem_node *alias_of; + bool registered; +}; + +#define CPC_SYSMEM_START(node) ((node)->start) +#define CPC_SYSMEM_LAST(node) ((node)->last) + +INTERVAL_TREE_DEFINE(struct cpc_sysmem_node, rb, u64, subtree_last, + CPC_SYSMEM_START, CPC_SYSMEM_LAST, static inline, + cpc_sysmem_itree) + +static struct rb_root_cached cpc_sysmem_tree = RB_ROOT_CACHED; +static DEFINE_MUTEX(cpc_sysmem_lock); + +static struct cpc_sysmem_node *cpc_sysmem_first(u64 start, u64 last) +{ + return cpc_sysmem_itree_iter_first(&cpc_sysmem_tree, start, last); +} + +static struct cpc_sysmem_node *cpc_sysmem_next(struct cpc_sysmem_node *node, + u64 start, u64 last) +{ + return cpc_sysmem_itree_iter_next(node, start, last); +} + /* pcc mapped address + header size + offset within PCC subspace */ #define GET_PCC_VADDR(offs, pcc_ss_id) (pcc_data[pcc_ss_id]->pcc_channel->shmem + \ 0x8 + (offs)) @@ -278,15 +315,21 @@ cpc_system_io_write_supported(const struct cpc_register_resource *reg) gas->address <= U16_MAX - (access_size - 1) && (IS_ENABLED(CONFIG_X86) || IS_ALIGNED(gas->address, access_size)); } -static u64 cpc_sysmem_access_size(const struct cpc_register_resource *reg) -{ - const struct cpc_reg *gas = ®->cpc_entry.reg; - unsigned int width; - if (gas->access_width > 4) +static unsigned int cpc_reg_access_width(const struct cpc_reg *reg) +{ + if (reg->access_width > 4) return 0; - width = GET_BIT_WIDTH(gas); + if (reg->access_width) + return 8U << (reg->access_width - 1); + + return reg->bit_width; +} + +static u64 cpc_sysmem_access_size(const struct cpc_register_resource *reg) +{ + unsigned int width = cpc_reg_access_width(®->cpc_entry.reg); if (width != 8 && width != 16 && width != 32 && width != 64) return 0; @@ -294,6 +337,12 @@ static u64 cpc_sysmem_access_size(const struct cpc_register_resource *reg) return width / 8; } +static bool cpc_reg_access_aligned(const struct cpc_reg *reg, u64 access_size) +{ + /* x86 MMIO and port-I/O accessors support unaligned addresses. */ + return IS_ENABLED(CONFIG_X86) || IS_ALIGNED(reg->address, access_size); +} + static bool cpc_sysmem_access_units_overlap(const struct cpc_register_resource *a, const struct cpc_register_resource *b) { @@ -312,36 +361,314 @@ static bool cpc_sysmem_access_units_overlap(const struct cpc_register_resource * return a_gas->address - b_gas->address < b_size; } +static bool cpc_reg_is_writable(unsigned int reg_idx) +{ + /* Only controls written by this driver can be competing writers. */ + switch (reg_idx) { + case DESIRED_PERF: + case MIN_PERF: + case MAX_PERF: + case PERF_LIMITED: + case ENABLE: + case AUTO_SEL_ENABLE: + case AUTO_ACT_WINDOW: + case ENERGY_PERF: + return true; + default: + return false; + } +} + +static bool cpc_sysmem_reg_needs_rmw(const struct cpc_register_resource *reg) +{ + const struct cpc_reg *gas = ®->cpc_entry.reg; + u64 access_size = cpc_sysmem_access_size(reg); + + return gas->bit_offset || gas->bit_width != access_size * 8; +} + +static int cpc_validate_sysmem_reg(const struct cpc_desc *cpc_desc, + const struct cpc_reg *gas, + unsigned int reg_idx) +{ + unsigned int access_width = cpc_reg_access_width(gas); + u64 access_size; + + if (access_width != 8 && access_width != 16 && + access_width != 32 && access_width != 64) + goto invalid; + + if (!gas->bit_width || gas->bit_width > access_width || + gas->bit_offset >= access_width || + gas->bit_width > access_width - gas->bit_offset) + goto invalid; + + access_size = access_width / 8; + if (!gas->address || gas->address > U64_MAX - (access_size - 1)) + goto invalid; + if (!cpc_reg_access_aligned(gas, access_size)) + goto invalid; + + return 0; + +invalid: + pr_debug("CPU:%d invalid SystemMemory GAS for _CPC register %u\n", + cpc_desc->cpu_id, reg_idx); + return -EINVAL; +} + static void cpc_mark_rmw_lock_users(struct cpc_desc *cpc_desc) { - int i, j; + int i; for (i = 0; i < cpc_desc->num_entries - 2; i++) { - struct cpc_register_resource *a = &cpc_desc->cpc_regs[i]; - struct cpc_reg *gas; - u64 access_size; + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; - if (!CPC_SUPPORTED(a) || !CPC_IN_SYSTEM_MEMORY(a)) + if (CPC_SUPPORTED(reg) && CPC_IN_SYSTEM_MEMORY(reg)) + reg->cpc_entry.use_rmw_lock = + cpc_sysmem_reg_needs_rmw(reg); + } +} + +struct cpc_bit_position { + u64 byte; + u8 bit; +}; + +static bool cpc_bit_position_before(const struct cpc_bit_position *a, + const struct cpc_bit_position *b) +{ + return a->byte < b->byte || (a->byte == b->byte && a->bit < b->bit); +} + +static bool cpc_sysmem_fields_overlap(const struct cpc_register_resource *a, + const struct cpc_register_resource *b) +{ + const struct cpc_reg *a_gas = &a->cpc_entry.reg; + const struct cpc_reg *b_gas = &b->cpc_entry.reg; + unsigned int a_last_bit = a_gas->bit_offset + a_gas->bit_width - 1; + unsigned int b_last_bit = b_gas->bit_offset + b_gas->bit_width - 1; + struct cpc_bit_position a_start = { + .byte = a_gas->address + a_gas->bit_offset / 8, + .bit = a_gas->bit_offset % 8, + }; + struct cpc_bit_position a_end = { + .byte = a_gas->address + a_last_bit / 8, + .bit = a_last_bit % 8, + }; + struct cpc_bit_position b_start = { + .byte = b_gas->address + b_gas->bit_offset / 8, + .bit = b_gas->bit_offset % 8, + }; + struct cpc_bit_position b_end = { + .byte = b_gas->address + b_last_bit / 8, + .bit = b_last_bit % 8, + }; + + return !cpc_bit_position_before(&a_end, &b_start) && + !cpc_bit_position_before(&b_end, &a_start); +} + +static bool cpc_same_sysmem_register(unsigned int a_idx, + const struct cpc_register_resource *a, + unsigned int b_idx, + const struct cpc_register_resource *b) +{ + const struct cpc_reg *a_gas = &a->cpc_entry.reg; + const struct cpc_reg *b_gas = &b->cpc_entry.reg; + + return a_idx == b_idx && + a_gas->address == b_gas->address && + a_gas->bit_width == b_gas->bit_width && + a_gas->bit_offset == b_gas->bit_offset && + cpc_reg_access_width(a_gas) == cpc_reg_access_width(b_gas); +} + +static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, + unsigned int a_idx, + const struct cpc_desc *b_desc, + unsigned int b_idx) +{ + const struct cpc_register_resource *a = &a_desc->cpc_regs[a_idx]; + const struct cpc_register_resource *b = &b_desc->cpc_regs[b_idx]; + bool a_writable, b_writable; + bool fields_overlap; + + if (!CPC_SUPPORTED(a) || !CPC_IN_SYSTEM_MEMORY(a) || + !CPC_SUPPORTED(b) || !CPC_IN_SYSTEM_MEMORY(b) || + !cpc_sysmem_access_units_overlap(a, b)) + return 0; + + a_writable = cpc_reg_is_writable(a_idx); + b_writable = cpc_reg_is_writable(b_idx); + if (!a_writable && !b_writable) + return 0; + + if (cpc_same_sysmem_register(a_idx, a, b_idx, b)) { + u64 access_size = cpc_sysmem_access_size(a); + + /* + * Exact partial aliases update the same field and retain + * last-writer-wins semantics when the complete access is one native + * transaction. A 64-bit MMIO write may be split on 32-bit kernels, + * and an unaligned x86 access is not guaranteed to be one device + * transaction. + */ + if (!a_writable || + (IS_ALIGNED(a->cpc_entry.reg.address, access_size) && + (access_size < sizeof(u64) || + IS_ENABLED(CONFIG_64BIT)))) + return 0; + goto conflict; + } + + fields_overlap = cpc_sysmem_fields_overlap(a, b); + /* A full-width writer must not overwrite another logical field. */ + if (fields_overlap && + ((a_writable && b_writable) || + (a_writable && !cpc_sysmem_reg_needs_rmw(a)) || + (b_writable && !cpc_sysmem_reg_needs_rmw(b)))) + goto conflict; + + /* Different descriptors do not share their partial-write locks. */ + if (a_desc != b_desc && a_writable && b_writable) + goto conflict; + + return 0; + +conflict: + pr_err("CPU%d: SystemMemory _CPC register %u conflicts with CPU%d register %u\n", + a_desc->cpu_id, a_idx, b_desc->cpu_id, b_idx); + return -EINVAL; +} + +static void cpc_unregister_sysmem_desc_locked(struct cpc_desc *cpc_desc) +{ + unsigned int i; + + if (!cpc_desc->sysmem_nodes) + return; + + for (i = 0; i < cpc_desc->num_entries - 2; i++) { + struct cpc_sysmem_node *node = &cpc_desc->sysmem_nodes[i]; + struct cpc_sysmem_node *alias, *child; + + if (node->alias_of) { + list_del(&node->alias_node); + continue; + } + if (!node->registered) continue; - gas = &a->cpc_entry.reg; - access_size = cpc_sysmem_access_size(a); - if (gas->bit_offset || !access_size || - gas->bit_width != access_size * 8) - a->cpc_entry.use_rmw_lock = true; + cpc_sysmem_itree_remove(node, &cpc_sysmem_tree); + node->registered = false; + if (list_empty(&node->aliases)) + continue; - for (j = i + 1; j < cpc_desc->num_entries - 2; j++) { - struct cpc_register_resource *b = &cpc_desc->cpc_regs[j]; + /* Keep one representative for aliases owned by live descriptors. */ + alias = list_first_entry(&node->aliases, + struct cpc_sysmem_node, alias_node); + list_del_init(&alias->alias_node); + alias->alias_of = NULL; + alias->registered = true; + list_splice_init(&node->aliases, &alias->aliases); + list_for_each_entry(child, &alias->aliases, alias_node) + child->alias_of = alias; + cpc_sysmem_itree_insert(alias, &cpc_sysmem_tree); + } - if (!CPC_SUPPORTED(b) || !CPC_IN_SYSTEM_MEMORY(b)) - continue; - if (!cpc_sysmem_access_units_overlap(a, b)) - continue; + kfree(cpc_desc->sysmem_nodes); + cpc_desc->sysmem_nodes = NULL; +} - a->cpc_entry.use_rmw_lock = true; - b->cpc_entry.use_rmw_lock = true; +static int cpc_register_sysmem_desc(struct cpc_desc *cpc_desc) +{ + unsigned int nr_regs = cpc_desc->num_entries - 2; + unsigned int i; + int ret = 0; + bool found = false; + + for (i = 0; i < nr_regs; i++) { + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; + + if (CPC_SUPPORTED(reg) && CPC_IN_SYSTEM_MEMORY(reg)) { + found = true; + break; } } + if (!found) + return 0; + + cpc_desc->sysmem_nodes = kcalloc(nr_regs, + sizeof(*cpc_desc->sysmem_nodes), + GFP_KERNEL); + if (!cpc_desc->sysmem_nodes) + return -ENOMEM; + + mutex_lock(&cpc_sysmem_lock); + + for (i = 0; i < nr_regs; i++) { + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; + struct cpc_sysmem_node *alias = NULL, *match, *node; + u64 size; + + if (!CPC_SUPPORTED(reg) || !CPC_IN_SYSTEM_MEMORY(reg)) + continue; + + node = &cpc_desc->sysmem_nodes[i]; + size = cpc_sysmem_access_size(reg); + node->start = reg->cpc_entry.reg.address; + node->last = node->start + size - 1; + node->desc = cpc_desc; + node->reg_idx = i; + INIT_LIST_HEAD(&node->aliases); + INIT_LIST_HEAD(&node->alias_node); + + match = cpc_sysmem_first(node->start, node->last); + while (match) { + struct cpc_register_resource *match_reg; + + ret = cpc_validate_sysmem_pair(cpc_desc, i, match->desc, + match->reg_idx); + if (ret) + goto out_unregister; + match_reg = &match->desc->cpc_regs[match->reg_idx]; + if (cpc_desc == match->desc) { + reg->cpc_entry.use_rmw_lock = true; + match_reg->cpc_entry.use_rmw_lock = true; + } + if (cpc_same_sysmem_register(i, reg, match->reg_idx, match_reg)) + alias = match; + + match = cpc_sysmem_next(match, node->start, node->last); + } + if (alias) { + node->alias_of = alias; + list_add_tail(&node->alias_node, &alias->aliases); + continue; + } + + cpc_sysmem_itree_insert(node, &cpc_sysmem_tree); + node->registered = true; + } + + mutex_unlock(&cpc_sysmem_lock); + return 0; + +out_unregister: + cpc_unregister_sysmem_desc_locked(cpc_desc); + mutex_unlock(&cpc_sysmem_lock); + return ret; +} + +static void cpc_unregister_sysmem_desc(struct cpc_desc *cpc_desc) +{ + if (!cpc_desc->sysmem_nodes) + return; + + mutex_lock(&cpc_sysmem_lock); + cpc_unregister_sysmem_desc_locked(cpc_desc); + mutex_unlock(&cpc_sysmem_lock); } static ssize_t show_feedback_ctrs(struct kobject *kobj, @@ -379,6 +706,8 @@ static void cppc_free_desc(struct cpc_desc *cpc_ptr) { unsigned int i; + cpc_unregister_sysmem_desc(cpc_ptr); + for (i = 2; i < cpc_ptr->num_entries; i++) { void __iomem *addr = cpc_ptr->cpc_regs[i - 2].sys_mem_vaddr; @@ -979,6 +1308,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) ret = -ENOMEM; goto out_buf_free; } + cpc_ptr->cpu_id = pr->id; /* First entry is NumEntries. */ cpc_obj = &out_obj->package.elements[0]; @@ -1100,10 +1430,16 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; } } else if (gas_t->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { - if (gas_t->address) { + if (!IS_NULL_REG(gas_t)) { void __iomem *addr; size_t access_width; + err = cpc_validate_sysmem_reg(cpc_ptr, gas_t, i - 2); + if (err) { + ret = err; + goto out_free; + } + if (!osc_cpc_flexible_adr_space_confirmed) { pr_debug("Flexible address space capability not supported\n"); ret = -EOPNOTSUPP; @@ -1112,13 +1448,14 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) ret = -EINVAL; } - access_width = GET_BIT_WIDTH(gas_t) / 8; + access_width = cpc_reg_access_width(gas_t); + access_width /= 8; addr = ioremap(gas_t->address, access_width); if (!addr) { ret = -ENOMEM; goto out_free; } - cpc_ptr->cpc_regs[i-2].sys_mem_vaddr = addr; + cpc_ptr->cpc_regs[i - 2].sys_mem_vaddr = addr; } } else if (gas_t->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { if (gas_t->access_width < 1 || gas_t->access_width > 3) { @@ -1154,8 +1491,9 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } } - cpc_ptr->cpc_regs[i-2].type = ACPI_TYPE_BUFFER; - memcpy(&cpc_ptr->cpc_regs[i-2].cpc_entry.reg, gas_t, sizeof(*gas_t)); + cpc_ptr->cpc_regs[i - 2].type = ACPI_TYPE_BUFFER; + memcpy(&cpc_ptr->cpc_regs[i - 2].cpc_entry.reg, gas_t, + sizeof(*gas_t)); } else if (cpc_obj->type == ACPI_TYPE_PACKAGE && (i - 2) == RESOURCE_PRIORITY) { /* * ACPI 6.6, s8.4.6.1.2.7 defines Resource Priority as a @@ -1215,8 +1553,6 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } - /* Store CPU Logical ID */ - cpc_ptr->cpu_id = pr->id; cpc_mark_rmw_lock_users(cpc_ptr); raw_spin_lock_init(&cpc_ptr->rmw_lock); @@ -1225,6 +1561,10 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (ret) goto out_free; + ret = cpc_register_sysmem_desc(cpc_ptr); + if (ret) + goto out_free; + /* Register PCC channel once for all PCC subspace ID. */ if (pcc_subspace_id >= 0) { ret = register_pcc_channel(pcc_subspace_id); @@ -1249,6 +1589,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) "acpi_cppc"); if (ret) { per_cpu(cpc_desc_ptr, pr->id) = NULL; + cpc_unregister_sysmem_desc(cpc_ptr); kobject_put(&cpc_ptr->kobj); goto out_pcc_put; } @@ -1292,6 +1633,7 @@ void acpi_cppc_processor_exit(struct acpi_processor *pr) pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, pr->id); per_cpu(cpc_desc_ptr, pr->id) = NULL; kobject_del(&cpc_ptr->kobj); + cpc_unregister_sysmem_desc(cpc_ptr); pcc_data_put(pcc_ss_id); per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; @@ -1479,11 +1821,7 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) val, size); if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { - /* - * The _CPC layout is immutable after probe. The precomputed flag - * retains serialization for partial fields or overlapping access - * units; standalone full-width registers avoid the lock. - */ + /* Partial fields and local overlaps use the descriptor lock. */ locked = reg_res->cpc_entry.use_rmw_lock; if (locked) { cpc_desc = per_cpu(cpc_desc_ptr, cpu); @@ -1543,8 +1881,11 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) break; } - if (locked) + if (locked) { + if (!ret_val) + mmiowb_set_pending(); raw_spin_unlock_irqrestore(&cpc_desc->rmw_lock, flags); + } return ret_val; } diff --git a/include/acpi/cppc_acpi.h b/include/acpi/cppc_acpi.h index edf0f6ffb437..737219761c52 100644 --- a/include/acpi/cppc_acpi.h +++ b/include/acpi/cppc_acpi.h @@ -77,6 +77,8 @@ struct cpc_register_resource { } cpc_entry; }; +struct cpc_sysmem_node; + /* Container to hold the CPC details for each CPU */ struct cpc_desc { int num_entries; @@ -84,10 +86,11 @@ struct cpc_desc { int cpu_id; int write_cmd_status; int write_cmd_id; - /* Lock used for RMW operations in cpc_write() */ + /* Serialize partial SystemMemory writes within this descriptor. */ raw_spinlock_t rmw_lock; struct cpc_register_resource cpc_regs[MAX_CPC_REG_ENT]; struct acpi_psd_package domain_info; + struct cpc_sysmem_node *sysmem_nodes; struct kobject kobj; }; From 09d911c9966333c45409440ec2d777cf80a3305f Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:54 +0100 Subject: [PATCH 33/89] ACPI: CPPC: Reject direct reads of write-only controls Between _CPC revision 3 and revision 4, Desired Performance changed from Read/Write to Write. Revision 4 also added the write-only OSPM Nominal Performance control. ACPI 6.6 section 4.6.3 says reads of write-only bit positions produce undefined results. The public Desired Performance helper already rejects revision-4 readback, but the common register accessor still permits either write-only control to be read. Move the check into the common accessor and reject both controls there, so callers cannot consume undefined values. A partial SystemMemory field still makes cpc_write() read its complete access unit to preserve bits outside the field. MASK_VAL_WRITE() replaces every bit of the field being written, so that field's undefined readback is not propagated. However, another writer sharing the access unit would preserve and replay the write-only field when performing its own RMW. Reject such pairs; a partial write-only field may share its access unit only with disjoint read-only fields. Also reject readable fields whose logical bits overlap a write-only field, even if the write-only descriptor is retained as an inaccessible range. Reading the containing access unit is safe when the undefined bits are masked out, but an overlapping read field would expose them to callers. Apply this check before the no-writer shortcut, and reserve the complete known access range when a write-only field has no usable Bit Width. Keep an invalid but locatable write-only SystemMemory descriptor represented during overlap validation, but mark it unreadable and unwritable. Otherwise a neighbouring RMW field could evade validation and replay undefined readback into the hidden control. Conservatively claim the larger of the declared access unit and logical field span. Check if the writer access unit covers the write-only field so harmless asymmetric geometries are not rejected. Preserve a decoded access-unit claim even when the malformed Bit Width is zero, and use that claimed range when testing whether another writer's access overlaps the hidden field. Count only accessible descriptors as writers in the generic conflict rules. The logical-field overlap check protects a retained field from a full-width write, and the dedicated write-only-field check also protects it from another writer's RMW. Mark an inaccessible OSPM Nominal Performance control unsupported because it is optional. Do the same for inaccessible Desired Performance while parsing, then let the post-parse control check accept it only for immutable autonomous selection. This preserves the autonomous-only exception without accepting an unusable Desired control in non-autonomous mode. Do not advertise fast switching or zero transition latency unless Desired Performance remains writable. An inaccessible descriptor retained only for overlap validation still carries its original address-space identity, but cannot service a performance request. Fixes: 71e1815113f7 ("ACPI: CPPC: Add support for CPPC v4") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian.loehle%40arm.com Link: https://sashiko.dev/#/patchset/20260808082644.1251332-1-christian.loehle%40arm.com Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-10-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 162 ++++++++++++++++++++++++++++++++------- include/acpi/cppc_acpi.h | 2 + 2 files changed, 137 insertions(+), 27 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 131dd3be1ec3..e93fb78afa13 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -171,7 +171,14 @@ static struct cpc_sysmem_node *cpc_sysmem_next(struct cpc_sysmem_node *node, static bool cpc_is_writable(const struct cpc_register_resource *cpc) { return cpc->type == ACPI_TYPE_BUFFER && - !IS_NULL_REG(&cpc->cpc_entry.reg); + !IS_NULL_REG(&cpc->cpc_entry.reg) && + !cpc->cpc_entry.write_unsupported; +} + +static bool cpc_is_readable(const struct cpc_register_resource *cpc) +{ + return cpc->type != ACPI_TYPE_BUFFER || + !cpc->cpc_entry.read_unsupported; } static bool cpc_entry_present(const struct cpc_register_resource *cpc) @@ -337,6 +344,22 @@ static u64 cpc_sysmem_access_size(const struct cpc_register_resource *reg) return width / 8; } +static u64 cpc_sysmem_field_size(const struct cpc_reg *gas) +{ + return DIV_ROUND_UP((u64)gas->bit_offset + gas->bit_width, 8); +} + +static u64 cpc_sysmem_claim_size(const struct cpc_register_resource *reg) +{ + const struct cpc_reg *gas = ®->cpc_entry.reg; + u64 access_size = cpc_sysmem_access_size(reg); + + if (!gas->bit_width) + return access_size; + + return max(access_size, cpc_sysmem_field_size(gas)); +} + static bool cpc_reg_access_aligned(const struct cpc_reg *reg, u64 access_size) { /* x86 MMIO and port-I/O accessors support unaligned addresses. */ @@ -348,8 +371,8 @@ static bool cpc_sysmem_access_units_overlap(const struct cpc_register_resource * { const struct cpc_reg *a_gas = &a->cpc_entry.reg; const struct cpc_reg *b_gas = &b->cpc_entry.reg; - u64 a_size = cpc_sysmem_access_size(a); - u64 b_size = cpc_sysmem_access_size(b); + u64 a_size = cpc_sysmem_claim_size(a); + u64 b_size = cpc_sysmem_claim_size(b); /* Keep the conservative locking path for malformed access widths. */ if (!a_size || !b_size) @@ -379,6 +402,21 @@ static bool cpc_reg_is_writable(unsigned int reg_idx) } } +static bool cpc_reg_is_write_only(const struct cpc_desc *cpc_desc, + unsigned int reg_idx) +{ + return cpc_desc->version >= CPPC_V4_REV && + (reg_idx == DESIRED_PERF || reg_idx == OSPM_NOMINAL_PERF); +} + +static void cpc_disable_reg(struct cpc_desc *cpc_desc, unsigned int reg_idx) +{ + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[reg_idx]; + + reg->type = ACPI_TYPE_INTEGER; + reg->cpc_entry.int_value = 0; +} + static bool cpc_sysmem_reg_needs_rmw(const struct cpc_register_resource *reg) { const struct cpc_reg *gas = ®->cpc_entry.reg; @@ -387,7 +425,7 @@ static bool cpc_sysmem_reg_needs_rmw(const struct cpc_register_resource *reg) return gas->bit_offset || gas->bit_width != access_size * 8; } -static int cpc_validate_sysmem_reg(const struct cpc_desc *cpc_desc, +static int cpc_validate_sysmem_reg(struct cpc_desc *cpc_desc, const struct cpc_reg *gas, unsigned int reg_idx) { @@ -412,6 +450,23 @@ static int cpc_validate_sysmem_reg(const struct cpc_desc *cpc_desc, return 0; invalid: + access_size = 0; + if (access_width == 8 || access_width == 16 || + access_width == 32 || access_width == 64) + access_size = access_width / 8; + if (gas->bit_width) + access_size = max(access_size, cpc_sysmem_field_size(gas)); + if (cpc_reg_is_write_only(cpc_desc, reg_idx) && gas->address && + access_size && gas->address <= U64_MAX - (access_size - 1)) { + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[reg_idx]; + + pr_warn("CPU%d: _CPC v%d register %u is inaccessible; keeping its range reserved\n", + cpc_desc->cpu_id, cpc_desc->version, reg_idx); + reg->cpc_entry.read_unsupported = true; + reg->cpc_entry.write_unsupported = true; + return 0; + } + pr_debug("CPU:%d invalid SystemMemory GAS for _CPC register %u\n", cpc_desc->cpu_id, reg_idx); return -EINVAL; @@ -469,6 +524,27 @@ static bool cpc_sysmem_fields_overlap(const struct cpc_register_resource *a, !cpc_bit_position_before(&b_end, &a_start); } +static bool cpc_sysmem_access_overlaps_field(const struct cpc_register_resource *access, + const struct cpc_register_resource *field) +{ + const struct cpc_reg *access_gas = &access->cpc_entry.reg; + const struct cpc_reg *field_gas = &field->cpc_entry.reg; + u64 access_last; + u64 field_start; + u64 field_last; + + if (!field_gas->bit_width) + return cpc_sysmem_access_units_overlap(access, field); + + access_last = access_gas->address + + cpc_sysmem_access_size(access) - 1; + field_start = field_gas->address + field_gas->bit_offset / 8; + field_last = field_gas->address + + (field_gas->bit_offset + field_gas->bit_width - 1) / 8; + + return access_gas->address <= field_last && field_start <= access_last; +} + static bool cpc_same_sysmem_register(unsigned int a_idx, const struct cpc_register_resource *a, unsigned int b_idx, @@ -491,6 +567,7 @@ static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, { const struct cpc_register_resource *a = &a_desc->cpc_regs[a_idx]; const struct cpc_register_resource *b = &b_desc->cpc_regs[b_idx]; + bool a_write_only, b_write_only; bool a_writable, b_writable; bool fields_overlap; @@ -499,8 +576,19 @@ static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, !cpc_sysmem_access_units_overlap(a, b)) return 0; - a_writable = cpc_reg_is_writable(a_idx); - b_writable = cpc_reg_is_writable(b_idx); + a_write_only = cpc_reg_is_write_only(a_desc, a_idx); + b_write_only = cpc_reg_is_write_only(b_desc, b_idx); + fields_overlap = !a->cpc_entry.reg.bit_width || + !b->cpc_entry.reg.bit_width || + cpc_sysmem_fields_overlap(a, b); + /* A readable field must not expose another field's undefined bits. */ + if (a_write_only != b_write_only && + cpc_is_readable(a_write_only ? b : a) && + fields_overlap) + goto conflict; + + a_writable = cpc_reg_is_writable(a_idx) && cpc_is_writable(a); + b_writable = cpc_reg_is_writable(b_idx) && cpc_is_writable(b); if (!a_writable && !b_writable) return 0; @@ -522,7 +610,6 @@ static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, goto conflict; } - fields_overlap = cpc_sysmem_fields_overlap(a, b); /* A full-width writer must not overwrite another logical field. */ if (fields_overlap && ((a_writable && b_writable) || @@ -534,6 +621,18 @@ static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, if (a_desc != b_desc && a_writable && b_writable) goto conflict; + /* + * RMW of either writer preserves the other field. If that field is + * write-only, its readback is undefined and cannot safely be replayed. + */ + if ((a_write_only && b_writable && + cpc_sysmem_reg_needs_rmw(b) && + cpc_sysmem_access_overlaps_field(b, a)) || + (b_write_only && a_writable && + cpc_sysmem_reg_needs_rmw(a) && + cpc_sysmem_access_overlaps_field(a, b))) + goto conflict; + return 0; conflict: @@ -616,7 +715,7 @@ static int cpc_register_sysmem_desc(struct cpc_desc *cpc_desc) continue; node = &cpc_desc->sysmem_nodes[i]; - size = cpc_sysmem_access_size(reg); + size = cpc_sysmem_claim_size(reg); node->start = reg->cpc_entry.reg.address; node->last = node->start + size - 1; node->desc = cpc_desc; @@ -1003,7 +1102,7 @@ bool cppc_allow_fast_switch(const struct cpumask *cpus) min_reg = &cpc_ptr->cpc_regs[MIN_PERF]; max_reg = &cpc_ptr->cpc_regs[MAX_PERF]; - if (!CPC_SUPPORTED(desired_reg) || + if (!cpc_is_writable(desired_reg) || (!CPC_IN_SYSTEM_MEMORY(desired_reg) && !CPC_IN_SYSTEM_IO(desired_reg)) || (CPC_SUPPORTED(min_reg) && @@ -1409,6 +1508,10 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; } + cpc_ptr->cpc_regs[i - 2].type = ACPI_TYPE_BUFFER; + memcpy(&cpc_ptr->cpc_regs[i - 2].cpc_entry.reg, gas_t, + sizeof(*gas_t)); + /* * The PCC Subspace index is encoded inside * the CPC table entries. The same PCC index @@ -1435,10 +1538,24 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) size_t access_width; err = cpc_validate_sysmem_reg(cpc_ptr, gas_t, i - 2); + if (err && (i - 2 == DESIRED_PERF || + i - 2 == OSPM_NOMINAL_PERF)) { + const char *name = i - 2 == DESIRED_PERF ? + "Desired Performance" : + "OSPM Nominal Performance"; + + pr_warn("CPU%d: disabling inaccessible %s register\n", + pr->id, name); + cpc_disable_reg(cpc_ptr, i - 2); + continue; + } if (err) { ret = err; goto out_free; } + if (!cpc_is_readable(&cpc_ptr->cpc_regs[i - 2]) && + !cpc_is_writable(&cpc_ptr->cpc_regs[i - 2])) + continue; if (!osc_cpc_flexible_adr_space_confirmed) { pr_debug("Flexible address space capability not supported\n"); @@ -1490,10 +1607,6 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; } } - - cpc_ptr->cpc_regs[i - 2].type = ACPI_TYPE_BUFFER; - memcpy(&cpc_ptr->cpc_regs[i - 2].cpc_entry.reg, gas_t, - sizeof(*gas_t)); } else if (cpc_obj->type == ACPI_TYPE_PACKAGE && (i - 2) == RESOURCE_PRIORITY) { /* * ACPI 6.6, s8.4.6.1.2.7 defines Resource Priority as a @@ -1833,6 +1946,10 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) } if (reg->bit_offset || reg->bit_width != size) { + /* + * MASK_VAL_WRITE() discards the field's old bits, so undefined + * readback from a write-only field is not propagated. + */ switch (size) { case 8: prev_val = readb_relaxed(vaddr); @@ -1927,6 +2044,8 @@ static int cppc_get_reg_val(int cpu, enum cppc_regs reg_idx, u64 *val) pr_debug("No CPC descriptor for CPU:%d\n", cpu); return -ENODEV; } + if (cpc_reg_is_write_only(cpc_desc, reg_idx)) + return -EOPNOTSUPP; reg = &cpc_desc->cpc_regs[reg_idx]; @@ -2007,11 +2126,6 @@ static int cppc_set_reg_val(int cpu, enum cppc_regs reg_idx, u64 val) return cpc_write(cpu, reg, val); } -static bool cppc_desired_perf_readable(const struct cpc_desc *cpc_desc) -{ - return cpc_desc->version < CPPC_V4_REV; -} - /** * cppc_get_desired_perf - Get the desired performance register value. * @cpunum: CPU from which to get desired performance. @@ -2022,15 +2136,6 @@ static bool cppc_desired_perf_readable(const struct cpc_desc *cpc_desc) */ int cppc_get_desired_perf(int cpunum, u64 *desired_perf) { - struct cpc_desc *cpc_desc = per_cpu(cpc_desc_ptr, cpunum); - - if (!cpc_desc) - return -ENODEV; - - /* _CPC revision 4 no longer specifies Desired Performance as readable. */ - if (!cppc_desired_perf_readable(cpc_desc)) - return -EOPNOTSUPP; - return cppc_get_reg_val(cpunum, DESIRED_PERF, desired_perf); } EXPORT_SYMBOL_GPL(cppc_get_desired_perf); @@ -3033,6 +3138,9 @@ int cppc_get_transition_latency(int cpu_num) return -ENODATA; desired_reg = &cpc_desc->cpc_regs[DESIRED_PERF]; + if (!cpc_is_writable(desired_reg)) + return -ENODATA; + if (CPC_IN_SYSTEM_MEMORY(desired_reg) || CPC_IN_SYSTEM_IO(desired_reg)) return 0; diff --git a/include/acpi/cppc_acpi.h b/include/acpi/cppc_acpi.h index 737219761c52..53186da6a654 100644 --- a/include/acpi/cppc_acpi.h +++ b/include/acpi/cppc_acpi.h @@ -72,6 +72,8 @@ struct cpc_register_resource { struct { struct cpc_reg reg; bool use_rmw_lock; + bool read_unsupported; + bool write_unsupported; }; u64 int_value; } cpc_entry; From fd1bb9a6cb1f6641840a99653a175b913c0358f4 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:55 +0100 Subject: [PATCH 34/89] ACPI: CPPC: Validate and access PCC register layouts PCC GAS Access Size identifies the subspace while Bit Width identifies the field size. Support zero-offset byte-multiple fields up to 64 bits with byte-oriented I/O and explicit little-endian encoding. This accepts the unaligned 32-bit Delivered Performance Counter in the ACPI implementation example and read-only widths such as 24 bits. A per-subspace raw spinlock serializes aliased payload copies made under the shared side of pcc_lock. Defer unsupported-field decisions until the package is known. A provided but inaccessible CPPC Enable or Autonomous Selection Enable control fails probe. OSPM must set the former and cannot discard the latter without knowing the hardware autonomous-mode state. An inaccessible Desired control can be ignored only for immutable autonomous operation. If both Minimum and Maximum controls are inaccessible, disable the pair coherently; a one-sided bound fails probe. Other inaccessible optional controls are marked absent. Validate PCC ranges after the eight-byte header and reject overlapping logical ranges when either is writable. Bit-level PCC RMW remains unsupported. Probe now rejects or disables unsupported writable PCC widths before publishing the descriptor. Remove the earlier setter preflights which repeat that validation. Fixes: 85b1407bf6d2 ("ACPI / CPPC: Make CPPC ACPI driver aware of PCC subspace IDs") Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-11-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 454 ++++++++++++++++++++++++++++----------- 1 file changed, 328 insertions(+), 126 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index e93fb78afa13..fe1d109afbd5 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -74,6 +74,8 @@ struct cppc_pcc_data { * Take write_lock for all purposes which gives exclusive access */ struct rw_semaphore pcc_lock; + /* Serialize byte-oriented accesses to aliased PCC payload fields. */ + raw_spinlock_t payload_lock; /* Wait queue for CPUs whose requests were batched */ wait_queue_head_t pcc_write_wait_q; @@ -132,9 +134,11 @@ static struct cpc_sysmem_node *cpc_sysmem_next(struct cpc_sysmem_node *node, return cpc_sysmem_itree_iter_next(node, start, last); } +#define CPC_PCC_HEADER_SIZE 0x8 + /* pcc mapped address + header size + offset within PCC subspace */ #define GET_PCC_VADDR(offs, pcc_ss_id) (pcc_data[pcc_ss_id]->pcc_channel->shmem + \ - 0x8 + (offs)) + CPC_PCC_HEADER_SIZE + (offs)) /* Check if a CPC register is in PCC */ #define CPC_IN_PCC(cpc) ((cpc)->type == ACPI_TYPE_BUFFER && \ @@ -286,19 +290,6 @@ show_cppc_data(cppc_get_perf_ctrs, cppc_perf_fb_ctrs, wraparound_time); (reg)->space_id != ACPI_ADR_SPACE_PLATFORM_COMM) ? \ (8 << ((reg)->access_width - 1)) : (reg)->bit_width) -static bool cpc_pcc_write_supported(const struct cpc_register_resource *reg) -{ - switch (GET_BIT_WIDTH(®->cpc_entry.reg)) { - case 8: - case 16: - case 32: - case 64: - return true; - default: - return false; - } -} - /* Shift and apply the mask for CPC reads/writes */ #define MASK_VAL_READ(reg, val) (((val) >> (reg)->bit_offset) & \ GENMASK(((reg)->bit_width) - 1, 0)) @@ -472,6 +463,196 @@ static int cpc_validate_sysmem_reg(struct cpc_desc *cpc_desc, return -EINVAL; } +static bool cpc_immutable_autonomous(const struct cpc_desc *cpc_desc) +{ + const struct cpc_register_resource *reg; + + reg = &cpc_desc->cpc_regs[AUTO_SEL_ENABLE]; + return osc_sb_cppc2_support_acked && reg->type == ACPI_TYPE_INTEGER && + reg->cpc_entry.int_value == 1; +} + +static int cpc_resolve_unsupported(struct cpc_desc *cpc_desc, + u32 unsupported) +{ + unsigned int i; + u32 bounds = BIT(MIN_PERF) | BIT(MAX_PERF); + bool min_unusable, max_unusable; + + if (unsupported & bounds) { + min_unusable = (unsupported & BIT(MIN_PERF)) || + !cpc_is_writable(&cpc_desc->cpc_regs[MIN_PERF]); + max_unusable = (unsupported & BIT(MAX_PERF)) || + !cpc_is_writable(&cpc_desc->cpc_regs[MAX_PERF]); + if (min_unusable && max_unusable) { + pr_warn("CPU%d: ignoring inaccessible Minimum and Maximum Performance registers\n", + cpc_desc->cpu_id); + cpc_disable_reg(cpc_desc, MIN_PERF); + cpc_disable_reg(cpc_desc, MAX_PERF); + unsupported &= ~bounds; + } + } + + for (i = 0; i < cpc_desc->num_entries - 2; i++) { + if (!(unsupported & BIT(i))) + continue; + + if (i == DESIRED_PERF && cpc_immutable_autonomous(cpc_desc)) { + pr_warn("CPU%d: ignoring inaccessible Desired Performance register in autonomous mode\n", + cpc_desc->cpu_id); + cpc_disable_reg(cpc_desc, i); + continue; + } + + /* + * A present Enable or Autonomous Selection control must remain + * usable. Disabling the latter could leave autonomous selection + * enabled while OSPM believes that it has disabled it. + */ + if (i == ENABLE || + (i == AUTO_SEL_ENABLE && cpc_entry_present(&cpc_desc->cpc_regs[i])) || + i == MIN_PERF || i == MAX_PERF || + !IS_OPTIONAL_CPC_REG(i)) { + pr_err("CPU%d: cannot access _CPC register %u\n", + cpc_desc->cpu_id, i); + return -EINVAL; + } + + pr_warn("CPU%d: ignoring inaccessible optional _CPC register %u\n", + cpc_desc->cpu_id, i); + cpc_disable_reg(cpc_desc, i); + } + + return 0; +} + +static int cpc_validate_required_controls(struct cpc_desc *cpc_desc) +{ + unsigned int i; + + /* + * Performance Limited is required by the specification, but tolerate a + * NULL descriptor used by firmware which cannot report limiting events. + * CPPC control does not depend on this status. + */ + for (i = 0; i < cpc_desc->num_entries - 2; i++) { + if (i != DESIRED_PERF && i != PERF_LIMITED && + !IS_OPTIONAL_CPC_REG(i) && + !cpc_entry_present(&cpc_desc->cpc_regs[i])) { + pr_debug("CPU:%d lacks mandatory _CPC register %u\n", + cpc_desc->cpu_id, i); + return -EINVAL; + } + } + + /* Desired may be absent only for immutable autonomous operation. */ + if (!cpc_is_writable(&cpc_desc->cpc_regs[DESIRED_PERF]) && + !cpc_immutable_autonomous(cpc_desc)) { + pr_debug("CPU:%d lacks a writable Desired Performance register\n", + cpc_desc->cpu_id); + return -EINVAL; + } + + return 0; +} + +static int cpc_validate_bound_controls(struct cpc_desc *cpc_desc) +{ + bool have_min, have_max; + + have_min = cpc_is_writable(&cpc_desc->cpc_regs[MIN_PERF]); + have_max = cpc_is_writable(&cpc_desc->cpc_regs[MAX_PERF]); + if (have_min != have_max) { + pr_err("CPU%d: _CPC must provide both Minimum and Maximum Performance or neither\n", + cpc_desc->cpu_id); + return -EINVAL; + } + + return 0; +} + +static void cpc_validate_pcc_bounds(struct cpc_desc *cpc_desc, + struct cppc_pcc_data *data, + u32 *unsupported) +{ + u64 shmem_size = data->pcc_channel->shmem_size; + unsigned int i; + + for (i = 0; i < cpc_desc->num_entries - 2; i++) { + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; + struct cpc_reg *gas; + u64 access_size; + + if ((*unsupported & BIT(i)) || !CPC_SUPPORTED(reg) || + !CPC_IN_PCC(reg)) + continue; + + gas = ®->cpc_entry.reg; + access_size = gas->bit_width / 8; + if (shmem_size >= CPC_PCC_HEADER_SIZE && + gas->address <= shmem_size - CPC_PCC_HEADER_SIZE && + access_size <= shmem_size - CPC_PCC_HEADER_SIZE - gas->address) + continue; + + pr_debug("CPU%d: _CPC register %u exceeds the PCC shared region\n", + cpc_desc->cpu_id, i); + *unsupported |= BIT(i); + } +} + +static u64 cpc_non_mmio_access_size(const struct cpc_register_resource *reg) +{ + const struct cpc_reg *gas = ®->cpc_entry.reg; + + if (gas->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) + return gas->bit_width / 8; + + return cpc_reg_access_width(gas) / 8; +} + +static int cpc_validate_non_mmio_overlaps(struct cpc_desc *cpc_desc, + u8 space_id, const char *name) +{ + unsigned int i, j; + + for (i = 0; i < cpc_desc->num_entries - 2; i++) { + struct cpc_register_resource *a = &cpc_desc->cpc_regs[i]; + const struct cpc_reg *a_gas; + u64 a_size; + + if (!CPC_SUPPORTED(a) || a->type != ACPI_TYPE_BUFFER || + a->cpc_entry.reg.space_id != space_id) + continue; + + a_gas = &a->cpc_entry.reg; + a_size = cpc_non_mmio_access_size(a); + for (j = i + 1; j < cpc_desc->num_entries - 2; j++) { + struct cpc_register_resource *b = &cpc_desc->cpc_regs[j]; + const struct cpc_reg *b_gas; + u64 b_size; + + if (!CPC_SUPPORTED(b) || b->type != ACPI_TYPE_BUFFER || + b->cpc_entry.reg.space_id != space_id) + continue; + + b_gas = &b->cpc_entry.reg; + b_size = cpc_non_mmio_access_size(b); + if (!cpc_reg_is_writable(i) && !cpc_reg_is_writable(j)) + continue; + if (a_gas->address < b_gas->address ? + b_gas->address - a_gas->address >= a_size : + a_gas->address - b_gas->address >= b_size) + continue; + + pr_err("CPU%d: overlapping writable %s _CPC registers %u and %u\n", + cpc_desc->cpu_id, name, i, j); + return -EINVAL; + } + } + + return 0; +} + static void cpc_mark_rmw_lock_users(struct cpc_desc *cpc_desc) { int i; @@ -847,6 +1028,8 @@ static int check_pcc_chan(int pcc_ss_id, bool chk_err_bit) pcc_ss_data->deadline_us); if (likely(!ret)) { + /* Order completion status before reading the returned payload. */ + rmb(); pcc_ss_data->platform_owns_pcc = false; if (chk_err_bit && (status & PCC_ERROR_MASK)) ret = -EIO; @@ -1290,6 +1473,7 @@ static int pcc_data_alloc(int pcc_ss_id) ret = -ENOMEM; goto out_unlock; } + raw_spin_lock_init(&data->payload_lock); pcc_data[pcc_ss_id] = data; } data->refcount++; @@ -1365,11 +1549,12 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) struct device *cpu_dev; acpi_handle handle = pr->handle; unsigned int num_ent, i, cpc_rev; + u32 unsupported_regs = 0; int pcc_subspace_id = -1; bool pcc_data_ref = false; bool cpc_present = false; acpi_status status; - int ret = -ENODATA; + int ret = -EINVAL; int err; if (per_cpu(cpc_desc_ptr, pr->id)) @@ -1519,6 +1704,24 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) * so extract it only once. */ if (gas_t->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { + /* These registers have no specified 32-bit upper bound. */ + bool wide_write = i - 2 == PERF_LIMITED || + i - 2 == ENABLE || + i - 2 == AUTO_SEL_ENABLE; + bool write_width_supported = gas_t->bit_width == 8 || + gas_t->bit_width == 16 || + gas_t->bit_width == 32 || + gas_t->bit_width == 64; + + if (!gas_t->bit_width || gas_t->bit_width > 64 || + gas_t->bit_offset || gas_t->bit_width % 8 || + (cpc_reg_is_writable(i - 2) && + (!write_width_supported || + (!wide_write && gas_t->bit_width > 32)))) { + unsupported_regs |= BIT(i - 2); + continue; + } + if (pcc_subspace_id < 0) { pcc_subspace_id = gas_t->access_width; err = pcc_data_alloc(pcc_subspace_id); @@ -1530,6 +1733,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } else if (pcc_subspace_id != gas_t->access_width) { pr_debug("Mismatched PCC ids in _CPC for CPU:%d\n", pr->id); + ret = -EINVAL; goto out_free; } } else if (gas_t->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { @@ -1537,21 +1741,11 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) void __iomem *addr; size_t access_width; - err = cpc_validate_sysmem_reg(cpc_ptr, gas_t, i - 2); - if (err && (i - 2 == DESIRED_PERF || - i - 2 == OSPM_NOMINAL_PERF)) { - const char *name = i - 2 == DESIRED_PERF ? - "Desired Performance" : - "OSPM Nominal Performance"; - - pr_warn("CPU%d: disabling inaccessible %s register\n", - pr->id, name); - cpc_disable_reg(cpc_ptr, i - 2); - continue; - } + err = cpc_validate_sysmem_reg(cpc_ptr, gas_t, + i - 2); if (err) { - ret = err; - goto out_free; + unsupported_regs |= BIT(i - 2); + continue; } if (!cpc_is_readable(&cpc_ptr->cpc_regs[i - 2]) && !cpc_is_writable(&cpc_ptr->cpc_regs[i - 2])) @@ -1625,35 +1819,14 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } per_cpu(cpu_pcc_subspace_idx, pr->id) = pcc_subspace_id; - /* - * Performance Limited is required by the specification, but tolerate a - * NULL descriptor used by firmware which cannot report limiting events. - * CPPC control does not depend on this status. - */ - for (i = 0; i < num_ent - 2; i++) { - if (i != DESIRED_PERF && i != PERF_LIMITED && - !IS_OPTIONAL_CPC_REG(i) && - !cpc_entry_present(&cpc_ptr->cpc_regs[i])) { - pr_debug("CPU:%d lacks mandatory _CPC register %u\n", - pr->id, i); - ret = -EINVAL; - goto out_free; - } - } - - /* - * In CPPC v1, DESIRED_PERF is mandatory. In CPPC v2, it is optional - * only when AUTO_SEL_ENABLE is supported. - */ - if (!cpc_is_writable(&cpc_ptr->cpc_regs[DESIRED_PERF]) && - (!osc_sb_cppc2_support_acked || - cpc_ptr->cpc_regs[AUTO_SEL_ENABLE].type != ACPI_TYPE_INTEGER || - cpc_ptr->cpc_regs[AUTO_SEL_ENABLE].cpc_entry.int_value != 1)) { - pr_debug("CPU:%d lacks a writable Desired Performance register\n", - pr->id); - ret = -EINVAL; + ret = cpc_resolve_unsupported(cpc_ptr, unsupported_regs); + if (ret) + goto out_free; + unsupported_regs = 0; + + ret = cpc_validate_required_controls(cpc_ptr); + if (ret) goto out_free; - } /* * Initialize the remaining cpc_regs as unsupported. @@ -1684,6 +1857,24 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (ret) goto out_free; } + if (pcc_subspace_id >= 0) { + cpc_validate_pcc_bounds(cpc_ptr, pcc_data[pcc_subspace_id], + &unsupported_regs); + + ret = cpc_resolve_unsupported(cpc_ptr, unsupported_regs); + if (ret) + goto out_free; + } + + ret = cpc_validate_non_mmio_overlaps(cpc_ptr, + ACPI_ADR_SPACE_PLATFORM_COMM, + "PCC"); + if (ret) + goto out_free; + + ret = cpc_validate_bound_controls(cpc_ptr); + if (ret) + goto out_free; /* Everything looks okay */ pr_debug("Parsed CPC struct for CPU: %d\n", pr->id); @@ -1810,6 +2001,9 @@ int __weak cpc_write_ffh(int cpunum, struct cpc_reg *reg, u64 val) static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) { void __iomem *vaddr = NULL; + unsigned long flags; + u8 buf[sizeof(*val)]; + unsigned int i; int size; int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, cpu); struct cpc_reg *reg = ®_res->cpc_entry.reg; @@ -1837,15 +2031,30 @@ static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) *val = val_u32; return 0; - } else if (reg->space_id == ACPI_ADR_SPACE_PLATFORM_COMM && pcc_ss_id >= 0) { + } else if (reg->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { + if (pcc_ss_id < 0 || !pcc_data[pcc_ss_id]) + return -ENODEV; + /* * For registers in PCC space, the register size is determined * by the bit width field; the access size is used to indicate * the PCC subspace id. */ vaddr = GET_PCC_VADDR(reg->address, pcc_ss_id); - } - else if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) + size = reg->bit_width / 8; + if (!size || size > sizeof(buf) || reg->bit_width % 8) + return -EFAULT; + + raw_spin_lock_irqsave(&pcc_data[pcc_ss_id]->payload_lock, flags); + memcpy_fromio(buf, vaddr, size); + raw_spin_unlock_irqrestore(&pcc_data[pcc_ss_id]->payload_lock, + flags); + + *val = 0; + for (i = 0; i < size; i++) + *val |= (u64)buf[i] << (i * 8); + return 0; + } else if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) vaddr = reg_res->sys_mem_vaddr; else if (reg->space_id == ACPI_ADR_SPACE_FIXED_HARDWARE) return cpc_read_ffh(cpu, reg, val); @@ -1867,18 +2076,12 @@ static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) *val = readq_relaxed(vaddr); break; default: - if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { - pr_debug("Error: Cannot read %u bit width from system memory: 0x%llx\n", - size, reg->address); - } else if (reg->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { - pr_debug("Error: Cannot read %u bit width from PCC for ss: %d\n", - size, pcc_ss_id); - } + pr_debug("Error: Cannot read %u bit width from system memory: 0x%llx\n", + size, reg->address); return -EFAULT; } - if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) - *val = MASK_VAL_READ(reg, *val); + *val = MASK_VAL_READ(reg, *val); return 0; } @@ -1893,6 +2096,8 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) struct cpc_reg *reg; struct cpc_desc *cpc_desc; unsigned long flags; + u8 buf[sizeof(val)]; + unsigned int i; bool locked = false; if (reg_res->type != ACPI_TYPE_BUFFER) @@ -1917,15 +2122,31 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) } return 0; - } else if (reg->space_id == ACPI_ADR_SPACE_PLATFORM_COMM && pcc_ss_id >= 0) { + } else if (reg->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { + if (pcc_ss_id < 0 || !pcc_data[pcc_ss_id]) + return -ENODEV; + /* * For registers in PCC space, the register size is determined * by the bit width field; the access size is used to indicate * the PCC subspace id. */ vaddr = GET_PCC_VADDR(reg->address, pcc_ss_id); - } - else if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) + size = reg->bit_width / 8; + if (!size || size > sizeof(buf) || reg->bit_width % 8) + return -EFAULT; + + for (i = 0; i < size; i++) + buf[i] = val >> (i * 8); + + raw_spin_lock_irqsave(&pcc_data[pcc_ss_id]->payload_lock, flags); + memcpy_toio(vaddr, buf, size); + /* Publish every payload byte before another CPU can ring the doorbell. */ + wmb(); + raw_spin_unlock_irqrestore(&pcc_data[pcc_ss_id]->payload_lock, + flags); + return 0; + } else if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) vaddr = reg_res->sys_mem_vaddr; else if (reg->space_id == ACPI_ADR_SPACE_FIXED_HARDWARE) return cpc_write_ffh(cpu, reg, val); @@ -1933,44 +2154,42 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) return acpi_os_write_memory((acpi_physical_address)reg->address, val, size); - if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { - /* Partial fields and local overlaps use the descriptor lock. */ - locked = reg_res->cpc_entry.use_rmw_lock; - if (locked) { - cpc_desc = per_cpu(cpc_desc_ptr, cpu); - if (!cpc_desc) { - pr_debug("No CPC descriptor for CPU:%d\n", cpu); - return -ENODEV; - } - raw_spin_lock_irqsave(&cpc_desc->rmw_lock, flags); + /* Partial fields and local overlaps use the descriptor lock. */ + locked = reg_res->cpc_entry.use_rmw_lock; + if (locked) { + cpc_desc = per_cpu(cpc_desc_ptr, cpu); + if (!cpc_desc) { + pr_debug("No CPC descriptor for CPU:%d\n", cpu); + return -ENODEV; } + raw_spin_lock_irqsave(&cpc_desc->rmw_lock, flags); + } - if (reg->bit_offset || reg->bit_width != size) { - /* - * MASK_VAL_WRITE() discards the field's old bits, so undefined - * readback from a write-only field is not propagated. - */ - switch (size) { - case 8: - prev_val = readb_relaxed(vaddr); - break; - case 16: - prev_val = readw_relaxed(vaddr); - break; - case 32: - prev_val = readl_relaxed(vaddr); - break; - case 64: - prev_val = readq_relaxed(vaddr); - break; - default: - if (locked) - raw_spin_unlock_irqrestore(&cpc_desc->rmw_lock, - flags); - return -EFAULT; - } - val = MASK_VAL_WRITE(reg, prev_val, val); + if (reg->bit_offset || reg->bit_width != size) { + /* + * MASK_VAL_WRITE() discards the field's old bits, so undefined + * readback from a write-only field is not propagated. + */ + switch (size) { + case 8: + prev_val = readb_relaxed(vaddr); + break; + case 16: + prev_val = readw_relaxed(vaddr); + break; + case 32: + prev_val = readl_relaxed(vaddr); + break; + case 64: + prev_val = readq_relaxed(vaddr); + break; + default: + if (locked) + raw_spin_unlock_irqrestore(&cpc_desc->rmw_lock, + flags); + return -EFAULT; } + val = MASK_VAL_WRITE(reg, prev_val, val); } switch (size) { @@ -1987,13 +2206,8 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) writeq_relaxed(val, vaddr); break; default: - if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { - pr_debug("Error: Cannot write %u bit width to system memory: 0x%llx\n", - size, reg->address); - } else if (reg->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { - pr_debug("Error: Cannot write %u bit width to PCC for ss: %d\n", - size, pcc_ss_id); - } + pr_debug("Error: Cannot write %u bit width to system memory: 0x%llx\n", + size, reg->address); ret_val = -EFAULT; break; } @@ -2074,9 +2288,6 @@ static int cppc_set_reg_val_in_pcc(int cpu, struct cpc_register_resource *reg, u pr_debug("Invalid pcc_ss_id\n"); return -ENODEV; } - if (!cpc_pcc_write_supported(reg)) - return -EFAULT; - pcc_ss_data = pcc_data[pcc_ss_id]; if (!pcc_ss_data) return -ENODEV; @@ -2490,9 +2701,6 @@ int cppc_set_epp_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls, bool enable) auto_sel_pcc = cpc_is_writable(auto_sel_reg) && CPC_IN_PCC(auto_sel_reg); epp_pcc = cpc_is_writable(epp_set_reg) && CPC_IN_PCC(epp_set_reg); - if ((auto_sel_pcc && !cpc_pcc_write_supported(auto_sel_reg)) || - (epp_pcc && !cpc_pcc_write_supported(epp_set_reg))) - return -EFAULT; if ((cpc_is_writable(auto_sel_reg) && CPC_IN_SYSTEM_IO(auto_sel_reg) && !cpc_system_io_write_supported(auto_sel_reg)) || @@ -2838,12 +3046,6 @@ int cppc_set_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls) !CPC_IN_PCC(max_perf_reg)); mixed_layout = pcc_layout && direct_layout; - /* Do not modify any control if a requested PCC field cannot be staged. */ - if ((desired_pcc && !cpc_pcc_write_supported(desired_reg)) || - (min_pcc && !cpc_pcc_write_supported(min_perf_reg)) || - (max_pcc && !cpc_pcc_write_supported(max_perf_reg))) - return -EFAULT; - if (mixed_layout || pcc_update) { if (pcc_ss_id < 0) { pr_debug("Invalid pcc_ss_id\n"); From ea5ed1f82e2273bf8c16eace6d701e2ea4e820d2 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:56 +0100 Subject: [PATCH 35/89] ACPI: CPPC: Validate SystemIO register layouts cpc_read() and cpc_write() access SystemIO registers using the complete GAS access width. cpc_read() does not extract a partial field, while the writer does not preserve bits outside one. A partial register can therefore return the wrong value or clobber adjacent fields on write. Retain valid read-only fields in 8-, 16-, and 32-bit access units. Extract them after reading the complete port. Continue to require writable controls to cover the complete access unit at Bit Offset zero. A partial write would require a serialized RMW. Accept Access Size zero when Bit Width supplies one of the supported sizes. Require natural alignment on architectures which implement port I/O through MMIO and may fault on unaligned Device-memory accesses. Preserve port layouts on x86; its native port-I/O instructions support them. When CONFIG_HAS_IOPORT is disabled, report unavailable port-I/O support and mark SystemIO layouts inaccessible during probe. Also return -EOPNOTSUPP explicitly in cpc_read() and cpc_write() so a SystemIO entry can never fall through and treat its port number as a physical-memory address. Resolve inaccessible entries using the control-specific policy established for PCC: optional fields can be disabled, while mandatory or semantically required controls fail probe. System I/O may use the same numeric port for different read and write registers, but a direction-dependent alias is safe only when the control is write-only. A readable control such as Minimum Performance would otherwise return the read-side capability's value, which cppc-cpufreq can then write back as its initial performance limit. Reject such aliases and overlapping writers; retain read-only overlaps and aliases between a read-only entry and a revision-4 write-only control. Remove the EPP setter's duplicate SystemIO geometry preflight now that probe validates every published writable control. Fixes: a2c8f92bea5f ("ACPI: CPPC: Implement support for SystemIO registers") Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-12-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 110 +++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 44 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index fe1d109afbd5..10cf96236dfb 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -244,7 +244,6 @@ static bool cpc_integer_entry_valid(unsigned int reg_idx, u64 value, */ #define NUM_RETRIES 500ULL -#define OVER_16BTS_MASK ~0xFFFFULL #define CPC_GENERIC_REGISTER_DESCRIPTOR 0x82 #define CPC_GENERIC_REGISTER_LENGTH (sizeof(struct cpc_reg) - 3) @@ -297,23 +296,6 @@ show_cppc_data(cppc_get_perf_ctrs, cppc_perf_fb_ctrs, wraparound_time); ((((val) & GENMASK(((reg)->bit_width) - 1, 0)) << (reg)->bit_offset) | \ ((prev_val) & ~(GENMASK(((reg)->bit_width) - 1, 0) << (reg)->bit_offset))) \ -static bool -cpc_system_io_write_supported(const struct cpc_register_resource *reg) -{ - const struct cpc_reg *gas = ®->cpc_entry.reg; - unsigned int access_width = GET_BIT_WIDTH(gas); - u64 access_size; - - if (!IS_ENABLED(CONFIG_HAS_IOPORT) || !CPC_IN_SYSTEM_IO(reg) || - (access_width != 8 && access_width != 16 && access_width != 32)) - return false; - - access_size = access_width / 8; - return !gas->bit_offset && gas->bit_width == access_width && - gas->address <= U16_MAX - (access_size - 1) && - (IS_ENABLED(CONFIG_X86) || IS_ALIGNED(gas->address, access_size)); -} - static unsigned int cpc_reg_access_width(const struct cpc_reg *reg) { if (reg->access_width > 4) @@ -610,6 +592,17 @@ static u64 cpc_non_mmio_access_size(const struct cpc_register_resource *reg) return cpc_reg_access_width(gas) / 8; } +static bool cpc_non_mmio_overlap_conflicts(u8 space_id, bool a_writable, + bool b_writable, bool a_write_only, + bool b_write_only) +{ + /* Only a write-only control can use a separate read-side port alias. */ + if (space_id == ACPI_ADR_SPACE_SYSTEM_IO && a_writable != b_writable) + return a_writable ? !a_write_only : !b_write_only; + + return a_writable || b_writable; +} + static int cpc_validate_non_mmio_overlaps(struct cpc_desc *cpc_desc, u8 space_id, const char *name) { @@ -629,6 +622,7 @@ static int cpc_validate_non_mmio_overlaps(struct cpc_desc *cpc_desc, for (j = i + 1; j < cpc_desc->num_entries - 2; j++) { struct cpc_register_resource *b = &cpc_desc->cpc_regs[j]; const struct cpc_reg *b_gas; + bool a_writable, b_writable; u64 b_size; if (!CPC_SUPPORTED(b) || b->type != ACPI_TYPE_BUFFER || @@ -637,7 +631,12 @@ static int cpc_validate_non_mmio_overlaps(struct cpc_desc *cpc_desc, b_gas = &b->cpc_entry.reg; b_size = cpc_non_mmio_access_size(b); - if (!cpc_reg_is_writable(i) && !cpc_reg_is_writable(j)) + a_writable = cpc_reg_is_writable(i); + b_writable = cpc_reg_is_writable(j); + if (!cpc_non_mmio_overlap_conflicts(space_id, + a_writable, b_writable, + cpc_reg_is_write_only(cpc_desc, i), + cpc_reg_is_write_only(cpc_desc, j))) continue; if (a_gas->address < b_gas->address ? b_gas->address - a_gas->address >= a_size : @@ -1769,21 +1768,41 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) cpc_ptr->cpc_regs[i - 2].sys_mem_vaddr = addr; } } else if (gas_t->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { - if (gas_t->access_width < 1 || gas_t->access_width > 3) { - /* - * 1 = 8-bit, 2 = 16-bit, and 3 = 32-bit. - * SystemIO doesn't implement 64-bit - * registers. - */ - pr_debug("Invalid access width %d for SystemIO register in _CPC\n", - gas_t->access_width); - goto out_free; + u64 access_size; + const char *reason = "uses unsupported SystemIO geometry"; + unsigned int access_width; + bool unsupported; + + access_width = cpc_reg_access_width(gas_t); + unsupported = !IS_ENABLED(CONFIG_HAS_IOPORT); + if (unsupported) + reason = "requires unavailable SystemIO support"; + else + unsupported = access_width != 8 && + access_width != 16 && + access_width != 32; + if (!unsupported) { + access_size = access_width / 8; + unsupported = !gas_t->bit_width || + gas_t->bit_width > access_width || + gas_t->bit_offset >= access_width || + gas_t->bit_width > access_width - + gas_t->bit_offset; } - if (gas_t->address & OVER_16BTS_MASK) { - /* SystemIO registers use 16-bit integer addresses */ - pr_debug("Invalid IO port %llu for SystemIO register in _CPC\n", - gas_t->address); - goto out_free; + if (!unsupported) { + unsupported = (cpc_reg_is_writable(i - 2) && + (gas_t->bit_offset || + gas_t->bit_width != access_width)) || + !cpc_reg_access_aligned(gas_t, + access_size) || + gas_t->address > + U16_MAX - (access_size - 1); + } + if (unsupported) { + pr_debug("CPU%d: _CPC register %u %s\n", + pr->id, i - 2, reason); + unsupported_regs |= BIT(i - 2); + continue; } if (!osc_cpc_flexible_adr_space_confirmed) { pr_debug("Flexible address space capability not supported\n"); @@ -1871,6 +1890,11 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) "PCC"); if (ret) goto out_free; + ret = cpc_validate_non_mmio_overlaps(cpc_ptr, + ACPI_ADR_SPACE_SYSTEM_IO, + "SystemIO"); + if (ret) + goto out_free; ret = cpc_validate_bound_controls(cpc_ptr); if (ret) @@ -2016,11 +2040,13 @@ static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) *val = 0; size = GET_BIT_WIDTH(reg); - if (IS_ENABLED(CONFIG_HAS_IOPORT) && - reg->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { + if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { u32 val_u32; acpi_status status; + if (!IS_ENABLED(CONFIG_HAS_IOPORT)) + return -EOPNOTSUPP; + status = acpi_os_read_port((acpi_io_address)reg->address, &val_u32, size); if (ACPI_FAILURE(status)) { @@ -2029,7 +2055,7 @@ static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) return -EFAULT; } - *val = val_u32; + *val = MASK_VAL_READ(reg, val_u32); return 0; } else if (reg->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { if (pcc_ss_id < 0 || !pcc_data[pcc_ss_id]) @@ -2109,10 +2135,12 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) size = GET_BIT_WIDTH(reg); - if (IS_ENABLED(CONFIG_HAS_IOPORT) && - reg->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { + if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { acpi_status status; + if (!IS_ENABLED(CONFIG_HAS_IOPORT)) + return -EOPNOTSUPP; + status = acpi_os_write_port((acpi_io_address)reg->address, (u32)val, size); if (ACPI_FAILURE(status)) { @@ -2701,12 +2729,6 @@ int cppc_set_epp_perf(int cpu, struct cppc_perf_ctrls *perf_ctrls, bool enable) auto_sel_pcc = cpc_is_writable(auto_sel_reg) && CPC_IN_PCC(auto_sel_reg); epp_pcc = cpc_is_writable(epp_set_reg) && CPC_IN_PCC(epp_set_reg); - if ((cpc_is_writable(auto_sel_reg) && - CPC_IN_SYSTEM_IO(auto_sel_reg) && - !cpc_system_io_write_supported(auto_sel_reg)) || - (cpc_is_writable(epp_set_reg) && CPC_IN_SYSTEM_IO(epp_set_reg) && - !cpc_system_io_write_supported(epp_set_reg))) - return -EOPNOTSUPP; auto_sel_non_pcc = cpc_is_writable(auto_sel_reg) && !auto_sel_pcc; epp_non_pcc = cpc_is_writable(epp_set_reg) && !epp_pcc; From 908a87126a7e484d60754104705d875b38c2cc20 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:57 +0100 Subject: [PATCH 36/89] ACPI: CPPC: Validate PCC overlaps across processors PCC shared-memory offsets are physical within a subspace, but the existing overlap check stops at one _CPC package. Two processors can therefore pass probe with writable ranges which overlap in the same PCC subspace. This is unsafe in the performance path, where CPUs may stage requests concurrently while holding the shared side of pcc_lock. Partially overlapping stores can construct a payload which belongs to neither request before a doorbell submits it. Index retained PCC byte ranges by subspace and physical interval. The registry checks entries within the new descriptor as well as other CPUs, replacing the separate same-descriptor PCC overlap pass. Permit read-only overlap and exact aliases of the same logical CPPC entry. The per-subspace payload lock serializes each exact-alias field store, including multi-byte fields copied with byte-oriented I/O. Reject every other overlap involving a writable entry. Each CPC descriptor owns its interval records. Remove them when processor teardown unpublishes the descriptor and on probe failure. Fixes: 80b8286aeec0 ("ACPI / CPPC: support for batching CPPC requests") Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-13-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 178 +++++++++++++++++++++++++++++++++++++-- include/acpi/cppc_acpi.h | 2 + 2 files changed, 175 insertions(+), 5 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 10cf96236dfb..fb9766f2cdd8 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -113,6 +113,18 @@ struct cpc_sysmem_node { bool registered; }; +struct cpc_non_mmio_node { + struct rb_node rb; + u64 subtree_last; + u64 start; + u64 last; + struct cpc_desc *desc; + unsigned int reg_idx; + u8 space_id; + u8 pcc_ss_id; + bool registered; +}; + #define CPC_SYSMEM_START(node) ((node)->start) #define CPC_SYSMEM_LAST(node) ((node)->last) @@ -123,6 +135,16 @@ INTERVAL_TREE_DEFINE(struct cpc_sysmem_node, rb, u64, subtree_last, static struct rb_root_cached cpc_sysmem_tree = RB_ROOT_CACHED; static DEFINE_MUTEX(cpc_sysmem_lock); +#define CPC_NON_MMIO_START(node) ((node)->start) +#define CPC_NON_MMIO_LAST(node) ((node)->last) + +INTERVAL_TREE_DEFINE(struct cpc_non_mmio_node, rb, u64, subtree_last, + CPC_NON_MMIO_START, CPC_NON_MMIO_LAST, static inline, + cpc_non_mmio_itree) + +static struct rb_root_cached cpc_pcc_trees[MAX_PCC_SUBSPACES]; +static DEFINE_MUTEX(cpc_non_mmio_lock); + static struct cpc_sysmem_node *cpc_sysmem_first(u64 start, u64 last) { return cpc_sysmem_itree_iter_first(&cpc_sysmem_tree, start, last); @@ -652,6 +674,150 @@ static int cpc_validate_non_mmio_overlaps(struct cpc_desc *cpc_desc, return 0; } +static struct rb_root_cached *cpc_non_mmio_tree(u8 space_id, u8 pcc_ss_id) +{ + if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM) + return &cpc_pcc_trees[pcc_ss_id]; + return NULL; +} + +static int cpc_validate_non_mmio_pair(const struct cpc_non_mmio_node *a, + const struct cpc_non_mmio_node *b) +{ + bool a_writable = cpc_reg_is_writable(a->reg_idx); + bool b_writable = cpc_reg_is_writable(b->reg_idx); + const char *name; + + if (!a_writable && !b_writable) + return 0; + + if (a->reg_idx == b->reg_idx && a->start == b->start && + a->last == b->last) + return 0; + + name = "PCC"; + pr_err("CPU%d: %s _CPC register %u conflicts with CPU%d register %u\n", + a->desc->cpu_id, name, a->reg_idx, b->desc->cpu_id, + b->reg_idx); + return -EINVAL; +} + +static void cpc_unregister_non_mmio_desc_locked(struct cpc_desc *cpc_desc) +{ + unsigned int i; + + if (!cpc_desc->non_mmio_nodes) + return; + + for (i = 0; i < cpc_desc->num_entries - 2; i++) { + struct cpc_non_mmio_node *node = &cpc_desc->non_mmio_nodes[i]; + struct rb_root_cached *tree; + + if (!node->registered) + continue; + + tree = cpc_non_mmio_tree(node->space_id, node->pcc_ss_id); + cpc_non_mmio_itree_remove(node, tree); + } + + kfree(cpc_desc->non_mmio_nodes); + cpc_desc->non_mmio_nodes = NULL; +} + +static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, + int pcc_ss_id) +{ + unsigned int nr_regs = cpc_desc->num_entries - 2; + unsigned int i; + int ret = 0; + bool found = false; + + for (i = 0; i < nr_regs; i++) { + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; + u8 space_id; + + if (!CPC_SUPPORTED(reg) || reg->type != ACPI_TYPE_BUFFER) + continue; + space_id = reg->cpc_entry.reg.space_id; + if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { + found = true; + break; + } + } + if (!found) + return 0; + + cpc_desc->non_mmio_nodes = kcalloc(nr_regs, + sizeof(*cpc_desc->non_mmio_nodes), + GFP_KERNEL); + if (!cpc_desc->non_mmio_nodes) + return -ENOMEM; + + mutex_lock(&cpc_non_mmio_lock); + + for (i = 0; i < nr_regs; i++) { + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; + struct cpc_non_mmio_node *match, *node; + struct rb_root_cached *tree; + u8 space_id; + u64 size; + + if (!CPC_SUPPORTED(reg) || reg->type != ACPI_TYPE_BUFFER) + continue; + + space_id = reg->cpc_entry.reg.space_id; + if (space_id != ACPI_ADR_SPACE_PLATFORM_COMM) + continue; + + if (pcc_ss_id < 0) { + ret = -EINVAL; + goto out_unregister; + } + + node = &cpc_desc->non_mmio_nodes[i]; + size = cpc_non_mmio_access_size(reg); + node->start = reg->cpc_entry.reg.address; + node->last = node->start + size - 1; + node->desc = cpc_desc; + node->reg_idx = i; + node->space_id = space_id; + node->pcc_ss_id = pcc_ss_id; + tree = cpc_non_mmio_tree(space_id, node->pcc_ss_id); + + match = cpc_non_mmio_itree_iter_first(tree, node->start, + node->last); + while (match) { + ret = cpc_validate_non_mmio_pair(node, match); + if (ret) + goto out_unregister; + + match = cpc_non_mmio_itree_iter_next(match, node->start, + node->last); + } + + cpc_non_mmio_itree_insert(node, tree); + node->registered = true; + } + + mutex_unlock(&cpc_non_mmio_lock); + return 0; + +out_unregister: + cpc_unregister_non_mmio_desc_locked(cpc_desc); + mutex_unlock(&cpc_non_mmio_lock); + return ret; +} + +static void cpc_unregister_non_mmio_desc(struct cpc_desc *cpc_desc) +{ + if (!cpc_desc->non_mmio_nodes) + return; + + mutex_lock(&cpc_non_mmio_lock); + cpc_unregister_non_mmio_desc_locked(cpc_desc); + mutex_unlock(&cpc_non_mmio_lock); +} + static void cpc_mark_rmw_lock_users(struct cpc_desc *cpc_desc) { int i; @@ -985,6 +1151,7 @@ static void cppc_free_desc(struct cpc_desc *cpc_ptr) { unsigned int i; + cpc_unregister_non_mmio_desc(cpc_ptr); cpc_unregister_sysmem_desc(cpc_ptr); for (i = 2; i < cpc_ptr->num_entries; i++) { @@ -1885,11 +2052,6 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; } - ret = cpc_validate_non_mmio_overlaps(cpc_ptr, - ACPI_ADR_SPACE_PLATFORM_COMM, - "PCC"); - if (ret) - goto out_free; ret = cpc_validate_non_mmio_overlaps(cpc_ptr, ACPI_ADR_SPACE_SYSTEM_IO, "SystemIO"); @@ -1900,6 +2062,10 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (ret) goto out_free; + ret = cpc_register_non_mmio_desc(cpc_ptr, pcc_subspace_id); + if (ret) + goto out_free; + /* Everything looks okay */ pr_debug("Parsed CPC struct for CPU: %d\n", pr->id); @@ -1917,6 +2083,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) "acpi_cppc"); if (ret) { per_cpu(cpc_desc_ptr, pr->id) = NULL; + cpc_unregister_non_mmio_desc(cpc_ptr); cpc_unregister_sysmem_desc(cpc_ptr); kobject_put(&cpc_ptr->kobj); goto out_pcc_put; @@ -1961,6 +2128,7 @@ void acpi_cppc_processor_exit(struct acpi_processor *pr) pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, pr->id); per_cpu(cpc_desc_ptr, pr->id) = NULL; kobject_del(&cpc_ptr->kobj); + cpc_unregister_non_mmio_desc(cpc_ptr); cpc_unregister_sysmem_desc(cpc_ptr); pcc_data_put(pcc_ss_id); diff --git a/include/acpi/cppc_acpi.h b/include/acpi/cppc_acpi.h index 53186da6a654..4b5d94f853dc 100644 --- a/include/acpi/cppc_acpi.h +++ b/include/acpi/cppc_acpi.h @@ -80,6 +80,7 @@ struct cpc_register_resource { }; struct cpc_sysmem_node; +struct cpc_non_mmio_node; /* Container to hold the CPC details for each CPU */ struct cpc_desc { @@ -93,6 +94,7 @@ struct cpc_desc { struct cpc_register_resource cpc_regs[MAX_CPC_REG_ENT]; struct acpi_psd_package domain_info; struct cpc_sysmem_node *sysmem_nodes; + struct cpc_non_mmio_node *non_mmio_nodes; struct kobject kobj; }; From 3051865865a88b41c5df881d44a6e3d1e85675ba Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:58 +0100 Subject: [PATCH 37/89] ACPI: CPPC: Validate SystemIO overlaps across processors System I/O port space is global, but the overlap check only compares registers within one _CPC package. Whole-width accesses from different processors can therefore target partially overlapping port ranges and clobber each other. Extend the physical interval registry to SystemIO and remove the redundant same-descriptor overlap pass, now covered by the registry. Retain the direction-aware policy from the SystemIO layout check: a read-only entry may alias a write-only control, but not a readable control whose readback would then select the wrong register. Also permit read-only overlaps and exact full-width aliases of the same logical writable CPPC entry. Reject other overlaps involving a writable entry. Fixes: a2c8f92bea5f ("ACPI: CPPC: Implement support for SystemIO registers") Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-14-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 77 +++++++++------------------------------- 1 file changed, 16 insertions(+), 61 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index fb9766f2cdd8..b1c29a4e5d8e 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -143,6 +143,7 @@ INTERVAL_TREE_DEFINE(struct cpc_non_mmio_node, rb, u64, subtree_last, cpc_non_mmio_itree) static struct rb_root_cached cpc_pcc_trees[MAX_PCC_SUBSPACES]; +static struct rb_root_cached cpc_sysio_tree = RB_ROOT_CACHED; static DEFINE_MUTEX(cpc_non_mmio_lock); static struct cpc_sysmem_node *cpc_sysmem_first(u64 start, u64 last) @@ -625,59 +626,12 @@ static bool cpc_non_mmio_overlap_conflicts(u8 space_id, bool a_writable, return a_writable || b_writable; } -static int cpc_validate_non_mmio_overlaps(struct cpc_desc *cpc_desc, - u8 space_id, const char *name) -{ - unsigned int i, j; - - for (i = 0; i < cpc_desc->num_entries - 2; i++) { - struct cpc_register_resource *a = &cpc_desc->cpc_regs[i]; - const struct cpc_reg *a_gas; - u64 a_size; - - if (!CPC_SUPPORTED(a) || a->type != ACPI_TYPE_BUFFER || - a->cpc_entry.reg.space_id != space_id) - continue; - - a_gas = &a->cpc_entry.reg; - a_size = cpc_non_mmio_access_size(a); - for (j = i + 1; j < cpc_desc->num_entries - 2; j++) { - struct cpc_register_resource *b = &cpc_desc->cpc_regs[j]; - const struct cpc_reg *b_gas; - bool a_writable, b_writable; - u64 b_size; - - if (!CPC_SUPPORTED(b) || b->type != ACPI_TYPE_BUFFER || - b->cpc_entry.reg.space_id != space_id) - continue; - - b_gas = &b->cpc_entry.reg; - b_size = cpc_non_mmio_access_size(b); - a_writable = cpc_reg_is_writable(i); - b_writable = cpc_reg_is_writable(j); - if (!cpc_non_mmio_overlap_conflicts(space_id, - a_writable, b_writable, - cpc_reg_is_write_only(cpc_desc, i), - cpc_reg_is_write_only(cpc_desc, j))) - continue; - if (a_gas->address < b_gas->address ? - b_gas->address - a_gas->address >= a_size : - a_gas->address - b_gas->address >= b_size) - continue; - - pr_err("CPU%d: overlapping writable %s _CPC registers %u and %u\n", - cpc_desc->cpu_id, name, i, j); - return -EINVAL; - } - } - - return 0; -} - static struct rb_root_cached *cpc_non_mmio_tree(u8 space_id, u8 pcc_ss_id) { if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM) return &cpc_pcc_trees[pcc_ss_id]; + if (space_id == ACPI_ADR_SPACE_SYSTEM_IO) + return &cpc_sysio_tree; return NULL; } @@ -688,14 +642,18 @@ static int cpc_validate_non_mmio_pair(const struct cpc_non_mmio_node *a, bool b_writable = cpc_reg_is_writable(b->reg_idx); const char *name; - if (!a_writable && !b_writable) + if (!cpc_non_mmio_overlap_conflicts(a->space_id, a_writable, + b_writable, + cpc_reg_is_write_only(a->desc, a->reg_idx), + cpc_reg_is_write_only(b->desc, b->reg_idx))) return 0; if (a->reg_idx == b->reg_idx && a->start == b->start && a->last == b->last) return 0; - name = "PCC"; + name = a->space_id == ACPI_ADR_SPACE_PLATFORM_COMM ? + "PCC" : "SystemIO"; pr_err("CPU%d: %s _CPC register %u conflicts with CPU%d register %u\n", a->desc->cpu_id, name, a->reg_idx, b->desc->cpu_id, b->reg_idx); @@ -739,7 +697,8 @@ static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, if (!CPC_SUPPORTED(reg) || reg->type != ACPI_TYPE_BUFFER) continue; space_id = reg->cpc_entry.reg.space_id; - if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM) { + if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM || + space_id == ACPI_ADR_SPACE_SYSTEM_IO) { found = true; break; } @@ -766,10 +725,11 @@ static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, continue; space_id = reg->cpc_entry.reg.space_id; - if (space_id != ACPI_ADR_SPACE_PLATFORM_COMM) + if (space_id != ACPI_ADR_SPACE_PLATFORM_COMM && + space_id != ACPI_ADR_SPACE_SYSTEM_IO) continue; - if (pcc_ss_id < 0) { + if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM && pcc_ss_id < 0) { ret = -EINVAL; goto out_unregister; } @@ -781,7 +741,8 @@ static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, node->desc = cpc_desc; node->reg_idx = i; node->space_id = space_id; - node->pcc_ss_id = pcc_ss_id; + node->pcc_ss_id = space_id == ACPI_ADR_SPACE_PLATFORM_COMM ? + pcc_ss_id : 0; tree = cpc_non_mmio_tree(space_id, node->pcc_ss_id); match = cpc_non_mmio_itree_iter_first(tree, node->start, @@ -2052,12 +2013,6 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; } - ret = cpc_validate_non_mmio_overlaps(cpc_ptr, - ACPI_ADR_SPACE_SYSTEM_IO, - "SystemIO"); - if (ret) - goto out_free; - ret = cpc_validate_bound_controls(cpc_ptr); if (ret) goto out_free; From a97aaf2aedae282eb668397428046cbd52435334 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:27:59 +0100 Subject: [PATCH 38/89] ACPI: CPPC: Clear Performance Limited without a stale read The Performance Limited status bits remain set until reset or OSPM clears them by writing zero. ACPI does not define the effect of writing one, so a plain read/write implementation may set a status bit when OSPM writes one. cppc_set_perf_limited() used a read-modify-write. An excursion reported between the transactions could be cleared by the stale write. Conversely, writing ones directly to bits not selected for clearing can manufacture an event. Clearing both bits with a literal zero is portable without a read; reject selective clears, which require an interlocked RMW that Linux cannot provide across all CPPC address spaces. A partial SystemMemory or SystemIO field needs an RMW to preserve the access unit. Keep it readable, but reject clears. Classify a field as a writer during overlap validation only when both its _CPC semantics and validated resource permit writing. Other writers can replay stale status, so drop only disjoint optional SystemMemory controls and reject required or overlapping writers. A present Autonomous Selection Enable control is required, because hiding it could leave autonomous mode enabled while OSPM believes it disabled. Retain inaccessible Performance Limited ranges without accessing them, so overlap validation cannot hide asynchronous status. For PCC, record the claim in its own subspace without letting it select the channel for usable controls. Bounds-check only entries in the acquired subspace. A status range that exceeds its shared region still protects its known inbounds portion from overlap with a writer. Likewise retain a SystemIO status range whose first port is valid but whose complete access extends beyond 0xffff. The retained range only rejects an overlapping valid writer. Fixes: 13c45a26635f ("ACPI: CPPC: add APIs and sysfs interface for perf_limited") Reported-by: Sashiko Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian.loehle%40arm.com Reported-by: Sumit Gupta Link: https://lore.kernel.org/lkml/d5f1ea9b-53b7-4db2-983a-b5be8e71a371@arm.com/ Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-15-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- .../ABI/testing/sysfs-devices-system-cpu | 14 +- drivers/acpi/cppc_acpi.c | 352 ++++++++++++++---- 2 files changed, 292 insertions(+), 74 deletions(-) diff --git a/Documentation/ABI/testing/sysfs-devices-system-cpu b/Documentation/ABI/testing/sysfs-devices-system-cpu index 82d10d556cc8..73c8c204820d 100644 --- a/Documentation/ABI/testing/sysfs-devices-system-cpu +++ b/Documentation/ABI/testing/sysfs-devices-system-cpu @@ -335,12 +335,16 @@ Description: Performance Limited Read to check if platform throttling (thermal/power/current limits) caused delivered performance to fall below the requested level. A non-zero value indicates throttling occurred. + If firmware provides the register but Linux cannot access it + safely, reads return the literal string "". - Write the bitmask of bits to clear: - - - 0x1 = clear bit 0 (desired performance excursion) - - 0x2 = clear bit 1 (minimum performance excursion) - - 0x3 = clear both bits + Write 0x3 to clear both bits. Selectively clearing one bit is + not supported because ACPI does not define the effect of writing + one to these status bits and an interlocked read-modify-write is + not generally available. + Writing 0x3 returns -EOPNOTSUPP when firmware describes the + register using an access which Linux cannot clear safely; reads + may remain available in that case. The platform sets these bits; OSPM can only clear them. diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index b1c29a4e5d8e..6d130381245e 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -413,6 +413,16 @@ static void cpc_disable_reg(struct cpc_desc *cpc_desc, unsigned int reg_idx) reg->cpc_entry.int_value = 0; } +static bool cpc_optional_writer_can_be_disabled(unsigned int reg_idx) +{ + if (!IS_OPTIONAL_CPC_REG(reg_idx) || !cpc_reg_is_writable(reg_idx) || + reg_idx == MIN_PERF || reg_idx == MAX_PERF || reg_idx == ENABLE || + reg_idx == AUTO_SEL_ENABLE) + return false; + + return true; +} + static bool cpc_sysmem_reg_needs_rmw(const struct cpc_register_resource *reg) { const struct cpc_reg *gas = ®->cpc_entry.reg; @@ -443,6 +453,22 @@ static int cpc_validate_sysmem_reg(struct cpc_desc *cpc_desc, if (!cpc_reg_access_aligned(gas, access_size)) goto invalid; + if (reg_idx == PERF_LIMITED) { + if (access_width == 64 && !IS_ENABLED(CONFIG_64BIT)) { + pr_warn_once("CPU%d: Performance Limited register cannot be accessed atomically; keeping its range reserved\n", + cpc_desc->cpu_id); + cpc_desc->cpc_regs[reg_idx].cpc_entry.read_unsupported = true; + cpc_desc->cpc_regs[reg_idx].cpc_entry.write_unsupported = true; + return 0; + } + + if (gas->bit_offset || gas->bit_width != access_width) { + pr_warn_once("CPU%d: Performance Limited register cannot be cleared safely; keeping it readable\n", + cpc_desc->cpu_id); + cpc_desc->cpc_regs[reg_idx].cpc_entry.write_unsupported = true; + } + } + return 0; invalid: @@ -452,12 +478,17 @@ static int cpc_validate_sysmem_reg(struct cpc_desc *cpc_desc, access_size = access_width / 8; if (gas->bit_width) access_size = max(access_size, cpc_sysmem_field_size(gas)); - if (cpc_reg_is_write_only(cpc_desc, reg_idx) && gas->address && - access_size && gas->address <= U64_MAX - (access_size - 1)) { + if ((cpc_reg_is_write_only(cpc_desc, reg_idx) || + reg_idx == PERF_LIMITED) && gas->address && access_size && + gas->address <= U64_MAX - (access_size - 1)) { struct cpc_register_resource *reg = &cpc_desc->cpc_regs[reg_idx]; - pr_warn("CPU%d: _CPC v%d register %u is inaccessible; keeping its range reserved\n", - cpc_desc->cpu_id, cpc_desc->version, reg_idx); + if (reg_idx == PERF_LIMITED) + pr_warn_once("CPU%d: _CPC v%d register %u is inaccessible; keeping its range reserved\n", + cpc_desc->cpu_id, cpc_desc->version, reg_idx); + else + pr_warn("CPU%d: _CPC v%d register %u is inaccessible; keeping its range reserved\n", + cpc_desc->cpu_id, cpc_desc->version, reg_idx); reg->cpc_entry.read_unsupported = true; reg->cpc_entry.write_unsupported = true; return 0; @@ -477,6 +508,9 @@ static bool cpc_immutable_autonomous(const struct cpc_desc *cpc_desc) reg->cpc_entry.int_value == 1; } +static bool cpc_retain_pcc_status(struct cpc_desc *cpc_desc, + unsigned int reg_idx); + static int cpc_resolve_unsupported(struct cpc_desc *cpc_desc, u32 unsupported) { @@ -502,6 +536,18 @@ static int cpc_resolve_unsupported(struct cpc_desc *cpc_desc, if (!(unsupported & BIT(i))) continue; + /* CPPC control does not depend on Performance Limited status. */ + if (i == PERF_LIMITED) { + if (CPC_IN_PCC(&cpc_desc->cpc_regs[i]) && + cpc_retain_pcc_status(cpc_desc, i)) + continue; + + pr_warn_once("CPU%d: ignoring inaccessible Performance Limited register\n", + cpc_desc->cpu_id); + cpc_disable_reg(cpc_desc, i); + continue; + } + if (i == DESIRED_PERF && cpc_immutable_autonomous(cpc_desc)) { pr_warn("CPU%d: ignoring inaccessible Desired Performance register in autonomous mode\n", cpc_desc->cpu_id); @@ -576,8 +622,60 @@ static int cpc_validate_bound_controls(struct cpc_desc *cpc_desc) return 0; } +static bool +cpc_retain_pcc_status(struct cpc_desc *cpc_desc, unsigned int reg_idx) +{ + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[reg_idx]; + const struct cpc_reg *gas = ®->cpc_entry.reg; + u64 size; + + if (reg_idx != PERF_LIMITED || !gas->bit_width) + return false; + + size = DIV_ROUND_UP((u64)gas->bit_offset + gas->bit_width, 8); + if (!size || gas->address > U64_MAX - (size - 1)) + return false; + + reg->cpc_entry.read_unsupported = true; + reg->cpc_entry.write_unsupported = true; + pr_warn_once("CPU%d: Performance Limited register cannot be accessed; keeping its PCC range reserved\n", + cpc_desc->cpu_id); + return true; +} + +static bool +cpc_retain_sysio_status(struct cpc_desc *cpc_desc, unsigned int reg_idx) +{ + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[reg_idx]; + const struct cpc_reg *gas = ®->cpc_entry.reg; + + if (reg_idx != PERF_LIMITED || !gas->bit_width) + return false; + + /* Retain any in-range portion for overlap validation only. */ + if (gas->address > U16_MAX) + return false; + + pr_warn_once("CPU%d: Performance Limited register cannot be accessed; keeping its SystemIO range reserved\n", + cpc_desc->cpu_id); + reg->cpc_entry.read_unsupported = true; + reg->cpc_entry.write_unsupported = true; + return true; +} + +static u64 cpc_non_mmio_access_size(const struct cpc_register_resource *reg) +{ + const struct cpc_reg *gas = ®->cpc_entry.reg; + + if (gas->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) + return DIV_ROUND_UP((u64)gas->bit_offset + gas->bit_width, 8); + + return max((u64)cpc_reg_access_width(gas) / 8, + DIV_ROUND_UP((u64)gas->bit_offset + gas->bit_width, 8)); +} + static void cpc_validate_pcc_bounds(struct cpc_desc *cpc_desc, - struct cppc_pcc_data *data, + int pcc_ss_id, struct cppc_pcc_data *data, u32 *unsupported) { u64 shmem_size = data->pcc_channel->shmem_size; @@ -593,7 +691,9 @@ static void cpc_validate_pcc_bounds(struct cpc_desc *cpc_desc, continue; gas = ®->cpc_entry.reg; - access_size = gas->bit_width / 8; + if (gas->access_width != pcc_ss_id) + continue; + access_size = cpc_non_mmio_access_size(reg); if (shmem_size >= CPC_PCC_HEADER_SIZE && gas->address <= shmem_size - CPC_PCC_HEADER_SIZE && access_size <= shmem_size - CPC_PCC_HEADER_SIZE - gas->address) @@ -605,14 +705,19 @@ static void cpc_validate_pcc_bounds(struct cpc_desc *cpc_desc, } } -static u64 cpc_non_mmio_access_size(const struct cpc_register_resource *reg) +static bool cpc_pcc_access_needed(const struct cpc_desc *cpc_desc) { - const struct cpc_reg *gas = ®->cpc_entry.reg; + unsigned int i; - if (gas->space_id == ACPI_ADR_SPACE_PLATFORM_COMM) - return gas->bit_width / 8; + for (i = 0; i < cpc_desc->num_entries - 2; i++) { + const struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; - return cpc_reg_access_width(gas) / 8; + if (CPC_SUPPORTED(reg) && CPC_IN_PCC(reg) && + (cpc_is_readable(reg) || cpc_is_writable(reg))) + return true; + } + + return false; } static bool cpc_non_mmio_overlap_conflicts(u8 space_id, bool a_writable, @@ -626,6 +731,14 @@ static bool cpc_non_mmio_overlap_conflicts(u8 space_id, bool a_writable, return a_writable || b_writable; } +static bool +cpc_sysio_perf_limited_conflicts(unsigned int a_idx, bool a_writable, + unsigned int b_idx, bool b_writable) +{ + return (a_idx == PERF_LIMITED && b_writable) || + (b_idx == PERF_LIMITED && a_writable); +} + static struct rb_root_cached *cpc_non_mmio_tree(u8 space_id, u8 pcc_ss_id) { if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM) @@ -635,21 +748,45 @@ static struct rb_root_cached *cpc_non_mmio_tree(u8 space_id, u8 pcc_ss_id) return NULL; } +static bool cpc_same_non_mmio_register(const struct cpc_non_mmio_node *a, + const struct cpc_non_mmio_node *b) +{ + const struct cpc_reg *a_gas = + &a->desc->cpc_regs[a->reg_idx].cpc_entry.reg; + const struct cpc_reg *b_gas = + &b->desc->cpc_regs[b->reg_idx].cpc_entry.reg; + + return a->space_id == b->space_id && a->pcc_ss_id == b->pcc_ss_id && + a->reg_idx == b->reg_idx && a->start == b->start && + a->last == b->last && a_gas->bit_offset == b_gas->bit_offset && + a_gas->bit_width == b_gas->bit_width && + (a->space_id == ACPI_ADR_SPACE_PLATFORM_COMM || + cpc_reg_access_width(a_gas) == cpc_reg_access_width(b_gas)); +} + static int cpc_validate_non_mmio_pair(const struct cpc_non_mmio_node *a, const struct cpc_non_mmio_node *b) { - bool a_writable = cpc_reg_is_writable(a->reg_idx); - bool b_writable = cpc_reg_is_writable(b->reg_idx); + const struct cpc_register_resource *a_reg; + const struct cpc_register_resource *b_reg; + bool a_writable, b_writable; const char *name; + a_reg = &a->desc->cpc_regs[a->reg_idx]; + b_reg = &b->desc->cpc_regs[b->reg_idx]; + a_writable = cpc_reg_is_writable(a->reg_idx) && cpc_is_writable(a_reg); + b_writable = cpc_reg_is_writable(b->reg_idx) && cpc_is_writable(b_reg); + if (!cpc_non_mmio_overlap_conflicts(a->space_id, a_writable, b_writable, cpc_reg_is_write_only(a->desc, a->reg_idx), - cpc_reg_is_write_only(b->desc, b->reg_idx))) + cpc_reg_is_write_only(b->desc, b->reg_idx)) && + !(a->space_id == ACPI_ADR_SPACE_SYSTEM_IO && + cpc_sysio_perf_limited_conflicts(a->reg_idx, a_writable, + b->reg_idx, b_writable))) return 0; - if (a->reg_idx == b->reg_idx && a->start == b->start && - a->last == b->last) + if (cpc_same_non_mmio_register(a, b)) return 0; name = a->space_id == ACPI_ADR_SPACE_PLATFORM_COMM ? @@ -682,8 +819,7 @@ static void cpc_unregister_non_mmio_desc_locked(struct cpc_desc *cpc_desc) cpc_desc->non_mmio_nodes = NULL; } -static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, - int pcc_ss_id) +static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc) { unsigned int nr_regs = cpc_desc->num_entries - 2; unsigned int i; @@ -729,11 +865,6 @@ static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, space_id != ACPI_ADR_SPACE_SYSTEM_IO) continue; - if (space_id == ACPI_ADR_SPACE_PLATFORM_COMM && pcc_ss_id < 0) { - ret = -EINVAL; - goto out_unregister; - } - node = &cpc_desc->non_mmio_nodes[i]; size = cpc_non_mmio_access_size(reg); node->start = reg->cpc_entry.reg.address; @@ -742,7 +873,7 @@ static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, node->reg_idx = i; node->space_id = space_id; node->pcc_ss_id = space_id == ACPI_ADR_SPACE_PLATFORM_COMM ? - pcc_ss_id : 0; + reg->cpc_entry.reg.access_width : 0; tree = cpc_non_mmio_tree(space_id, node->pcc_ss_id); match = cpc_non_mmio_itree_iter_first(tree, node->start, @@ -755,7 +886,6 @@ static int cpc_register_non_mmio_desc(struct cpc_desc *cpc_desc, match = cpc_non_mmio_itree_iter_next(match, node->start, node->last); } - cpc_non_mmio_itree_insert(node, tree); node->registered = true; } @@ -786,7 +916,8 @@ static void cpc_mark_rmw_lock_users(struct cpc_desc *cpc_desc) for (i = 0; i < cpc_desc->num_entries - 2; i++) { struct cpc_register_resource *reg = &cpc_desc->cpc_regs[i]; - if (CPC_SUPPORTED(reg) && CPC_IN_SYSTEM_MEMORY(reg)) + if (CPC_SUPPORTED(reg) && CPC_IN_SYSTEM_MEMORY(reg) && + cpc_is_writable(reg)) reg->cpc_entry.use_rmw_lock = cpc_sysmem_reg_needs_rmw(reg); } @@ -878,6 +1009,7 @@ static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, bool a_writable, b_writable; bool fields_overlap; + /* The overlap helper includes each descriptor's conservative claim. */ if (!CPC_SUPPORTED(a) || !CPC_IN_SYSTEM_MEMORY(a) || !CPC_SUPPORTED(b) || !CPC_IN_SYSTEM_MEMORY(b) || !cpc_sysmem_access_units_overlap(a, b)) @@ -917,6 +1049,15 @@ static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, goto conflict; } + /* + * The platform may set Performance Limited asynchronously. A write to + * another field in the same access unit could write back stale status + * bits, which an OSPM lock cannot prevent. + */ + if ((a_idx == PERF_LIMITED && b_writable) || + (b_idx == PERF_LIMITED && a_writable)) + goto conflict; + /* A full-width writer must not overwrite another logical field. */ if (fields_overlap && ((a_writable && b_writable) || @@ -948,6 +1089,43 @@ static int cpc_validate_sysmem_pair(const struct cpc_desc *a_desc, return -EINVAL; } +static bool cpc_disable_new_sysmem_writer(struct cpc_desc *cpc_desc, + unsigned int reg_idx, + const struct cpc_sysmem_node *node) +{ + struct cpc_register_resource *reg = &cpc_desc->cpc_regs[reg_idx]; + struct cpc_sysmem_node *match; + unsigned int status_cpu = 0; + bool found = false; + + if (!cpc_optional_writer_can_be_disabled(reg_idx) || + !cpc_is_writable(reg)) + return false; + + match = cpc_sysmem_first(node->start, node->last); + while (match) { + const struct cpc_register_resource *status; + + if (match->reg_idx == PERF_LIMITED) { + status = &match->desc->cpc_regs[PERF_LIMITED]; + if (!status->cpc_entry.reg.bit_width || + cpc_sysmem_fields_overlap(reg, status)) + return false; + status_cpu = match->desc->cpu_id; + found = true; + } + + match = cpc_sysmem_next(match, node->start, node->last); + } + if (!found) + return false; + + pr_warn_once("CPU%d: ignoring optional _CPC register %u sharing CPU%d Performance Limited access unit\n", + cpc_desc->cpu_id, reg_idx, status_cpu); + cpc_disable_reg(cpc_desc, reg_idx); + return true; +} + static void cpc_unregister_sysmem_desc_locked(struct cpc_desc *cpc_desc) { unsigned int i; @@ -1030,15 +1208,19 @@ static int cpc_register_sysmem_desc(struct cpc_desc *cpc_desc) INIT_LIST_HEAD(&node->aliases); INIT_LIST_HEAD(&node->alias_node); + /* Performance Limited precedes every optional writer we may disable. */ + if (cpc_disable_new_sysmem_writer(cpc_desc, i, node)) + continue; + match = cpc_sysmem_first(node->start, node->last); while (match) { struct cpc_register_resource *match_reg; + match_reg = &match->desc->cpc_regs[match->reg_idx]; ret = cpc_validate_sysmem_pair(cpc_desc, i, match->desc, match->reg_idx); if (ret) goto out_unregister; - match_reg = &match->desc->cpc_regs[match->reg_idx]; if (cpc_desc == match->desc) { reg->cpc_entry.use_rmw_lock = true; match_reg->cpc_entry.use_rmw_lock = true; @@ -1519,8 +1701,6 @@ static int register_pcc_channel(int pcc_ss_idx) pcc_chan = pcc_mbox_request_channel(&cppc_mbox_cl, pcc_ss_idx); if (IS_ERR(pcc_chan)) { - pr_err("Failed to find PCC channel for subspace %d\n", - pcc_ss_idx); ret = -ENODEV; goto out_unlock; } @@ -1686,7 +1866,6 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (per_cpu(cpc_desc_ptr, pr->id)) return 0; - per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; if (!osc_sb_cppc2_support_acked) { @@ -1839,29 +2018,37 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) gas_t->bit_width == 16 || gas_t->bit_width == 32 || gas_t->bit_width == 64; + bool unsupported; - if (!gas_t->bit_width || gas_t->bit_width > 64 || - gas_t->bit_offset || gas_t->bit_width % 8 || - (cpc_reg_is_writable(i - 2) && - (!write_width_supported || - (!wide_write && gas_t->bit_width > 32)))) { - unsupported_regs |= BIT(i - 2); + unsupported = !gas_t->bit_width || + gas_t->bit_width > 64 || + gas_t->bit_offset || + gas_t->bit_width % 8 || + (cpc_reg_is_writable(i - 2) && + (!write_width_supported || + (!wide_write && gas_t->bit_width > 32))); + if (unsupported) { + if (!cpc_retain_pcc_status(cpc_ptr, i - 2)) + unsupported_regs |= BIT(i - 2); continue; } if (pcc_subspace_id < 0) { pcc_subspace_id = gas_t->access_width; + } else if (pcc_subspace_id != gas_t->access_width) { + pr_debug("Mismatched PCC ids in _CPC for CPU:%d\n", + pr->id); + ret = -EINVAL; + goto out_free; + } + + if (!pcc_data_ref) { err = pcc_data_alloc(pcc_subspace_id); if (err) { ret = err; goto out_free; } pcc_data_ref = true; - } else if (pcc_subspace_id != gas_t->access_width) { - pr_debug("Mismatched PCC ids in _CPC for CPU:%d\n", - pr->id); - ret = -EINVAL; - goto out_free; } } else if (gas_t->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { if (!IS_NULL_REG(gas_t)) { @@ -1899,6 +2086,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) u64 access_size; const char *reason = "uses unsupported SystemIO geometry"; unsigned int access_width; + bool partial = false; bool unsupported; access_width = cpc_reg_access_width(gas_t); @@ -1916,22 +2104,32 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) gas_t->bit_offset >= access_width || gas_t->bit_width > access_width - gas_t->bit_offset; + partial = gas_t->bit_offset || + gas_t->bit_width != access_width; } if (!unsupported) { unsupported = (cpc_reg_is_writable(i - 2) && - (gas_t->bit_offset || - gas_t->bit_width != access_width)) || + i - 2 != PERF_LIMITED && + cpc_is_writable(&cpc_ptr->cpc_regs[i - 2]) && + partial) || !cpc_reg_access_aligned(gas_t, access_size) || gas_t->address > U16_MAX - (access_size - 1); } if (unsupported) { + if (cpc_retain_sysio_status(cpc_ptr, i - 2)) + continue; pr_debug("CPU%d: _CPC register %u %s\n", pr->id, i - 2, reason); unsupported_regs |= BIT(i - 2); continue; } + if (i - 2 == PERF_LIMITED && partial) { + pr_warn_once("CPU%d: Performance Limited register cannot be cleared safely; keeping it readable\n", + cpc_ptr->cpu_id); + cpc_ptr->cpc_regs[i - 2].cpc_entry.write_unsupported = true; + } if (!osc_cpc_flexible_adr_space_confirmed) { pr_debug("Flexible address space capability not supported\n"); ret = -EOPNOTSUPP; @@ -1964,7 +2162,9 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; } } - per_cpu(cpu_pcc_subspace_idx, pr->id) = pcc_subspace_id; + + per_cpu(cpu_pcc_subspace_idx, pr->id) = pcc_data_ref ? + pcc_subspace_id : -1; ret = cpc_resolve_unsupported(cpc_ptr, unsupported_regs); if (ret) @@ -1999,25 +2199,35 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) goto out_free; /* Register PCC channel once for all PCC subspace ID. */ - if (pcc_subspace_id >= 0) { + if (pcc_data_ref) { ret = register_pcc_channel(pcc_subspace_id); - if (ret) + if (ret) { + pr_err("Failed to find PCC channel for subspace %d\n", + pcc_subspace_id); goto out_free; - } - if (pcc_subspace_id >= 0) { - cpc_validate_pcc_bounds(cpc_ptr, pcc_data[pcc_subspace_id], + } + + cpc_validate_pcc_bounds(cpc_ptr, pcc_subspace_id, + pcc_data[pcc_subspace_id], &unsupported_regs); ret = cpc_resolve_unsupported(cpc_ptr, unsupported_regs); if (ret) goto out_free; + + /* A range-only status entry needs the channel only for bounds. */ + if (!cpc_pcc_access_needed(cpc_ptr)) { + pcc_data_put(pcc_subspace_id); + pcc_data_ref = false; + per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; + } } ret = cpc_validate_bound_controls(cpc_ptr); if (ret) goto out_free; - ret = cpc_register_non_mmio_desc(cpc_ptr, pcc_subspace_id); + ret = cpc_register_non_mmio_desc(cpc_ptr); if (ret) goto out_free; @@ -2155,6 +2365,9 @@ static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) int pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, cpu); struct cpc_reg *reg = ®_res->cpc_entry.reg; + if (!cpc_is_readable(reg_res)) + return -EOPNOTSUPP; + if (reg_res->type == ACPI_TYPE_INTEGER) { *val = reg_res->cpc_entry.int_value; return 0; @@ -2249,13 +2462,10 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) unsigned int i; bool locked = false; - if (reg_res->type != ACPI_TYPE_BUFFER) + if (!cpc_is_writable(reg_res)) return -EOPNOTSUPP; reg = ®_res->cpc_entry.reg; - if (IS_NULL_REG(reg)) - return -EOPNOTSUPP; - size = GET_BIT_WIDTH(reg); if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { @@ -2414,14 +2624,20 @@ static int cppc_get_reg_val(int cpu, enum cppc_regs reg_idx, u64 *val) reg = &cpc_desc->cpc_regs[reg_idx]; - /* Desired may be absent for immutable autonomous selection. */ + /* + * Desired and Performance Limited may be disabled despite not being + * generally optional. + */ if ((reg->type == ACPI_TYPE_INTEGER && - (IS_OPTIONAL_CPC_REG(reg_idx) || reg_idx == DESIRED_PERF) && + (IS_OPTIONAL_CPC_REG(reg_idx) || reg_idx == DESIRED_PERF || + reg_idx == PERF_LIMITED) && !reg->cpc_entry.int_value) || (reg->type != ACPI_TYPE_INTEGER && IS_NULL_REG(®->cpc_entry.reg))) { pr_debug("CPC register is not supported\n"); return -EOPNOTSUPP; } + if (!cpc_is_readable(reg)) + return -EOPNOTSUPP; if (CPC_IN_PCC(reg)) return cppc_get_reg_val_in_pcc(cpu, reg, val); @@ -3412,7 +3628,7 @@ EXPORT_SYMBOL_GPL(cppc_get_perf_limited); /** * cppc_set_perf_limited() - Clear bits in the Performance Limited register. * @cpu: CPU on which to write register. - * @bits_to_clear: Bitmask of bits to clear in the perf_limited register. + * @bits_to_clear: Zero for no-op or CPPC_PERF_LIMITED_MASK to clear both bits. * * The Performance Limited register contains two sticky bits set by platform: * - Bit 0 (Desired_Excursion): Set when delivered performance is constrained @@ -3421,31 +3637,29 @@ EXPORT_SYMBOL_GPL(cppc_get_perf_limited); * below minimum performance. * * These bits are sticky and remain set until OSPM explicitly clears them. - * This function only allows clearing bits (the platform sets them). + * Selective clears are unsupported because they require an interlocked RMW. * * Return: 0 for success, -EINVAL for invalid bits, -EIO on register * access failure, -EOPNOTSUPP if not supported. */ int cppc_set_perf_limited(int cpu, u64 bits_to_clear) { - u64 current_val, new_val; - int ret; - /* Only bits 0 and 1 are valid */ - if (bits_to_clear & ~CPPC_PERF_LIMITED_MASK) + if (bits_to_clear & ~(u64)CPPC_PERF_LIMITED_MASK) return -EINVAL; if (!bits_to_clear) return 0; - ret = cppc_get_perf_limited(cpu, ¤t_val); - if (ret) - return ret; + /* + * Writing zero clears both bits without depending on how a platform + * treats written ones. ACPI does not define the effect of writing one, + * so a selective clear cannot be implemented without an interlocked RMW. + */ + if (bits_to_clear != CPPC_PERF_LIMITED_MASK) + return -EOPNOTSUPP; - /* Clear the specified bits */ - new_val = current_val & ~bits_to_clear; - - return cppc_set_reg_val(cpu, PERF_LIMITED, new_val); + return cppc_set_reg_val(cpu, PERF_LIMITED, 0); } EXPORT_SYMBOL_GPL(cppc_set_perf_limited); From 487d1ad7535e4ef5a404985b170a356a9f827326 Mon Sep 17 00:00:00 2001 From: Sumit Gupta Date: Wed, 16 Sep 2026 17:28:00 +0100 Subject: [PATCH 39/89] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41 NVIDIA T41 firmware describes Performance Limited as a two-bit field at offset zero in a DWord SystemMemory access unit. Generic CPPC code must keep such a field read-only because preserving the remainder when clearing it requires a read-modify-write which cannot be interlocked with platform updates. The remaining bits of this access unit are unimplemented on T41: they read as zero, writes have no side effects, and no other register uses them. The Performance Limited register therefore owns the complete access unit, but shipped firmware does not describe that property accurately. Add a CPPC platform-quirk table keyed by the DSDT header and carry quirk behavior through explicit flags. Cache a successful table lookup, copy each GAS into the driver's private descriptor, and apply fixups before layout validation, mapping and overlap registration. Distinguish a genuine non-match from a table-header lookup failure in acpi_match_platform_list(). Propagate lookup errors from CPPC probe without caching them, so a transient mapping failure cannot disable the workaround for every later processor. Existing matcher callers still treat all negative results as no match. For the known T41 layout only, widen a two-bit Performance Limited field at offset zero to its 32-bit access width. Clearing both status bits can then be issued as one DWord write of zero without a stale read. Corrected firmware which reports the full width is unchanged. The workaround therefore lapses automatically when corrected firmware ships. Link: https://lore.kernel.org/lkml/d5f1ea9b-53b7-4db2-983a-b5be8e71a371@arm.com/ Signed-off-by: Sumit Gupta [ Rework quirk matching and fixup placement; propagate lookup failures without caching them. ] Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-16-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 72 ++++++++++++++++++++++++++++++++++++++++ drivers/acpi/utils.c | 13 ++++++-- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 6d130381245e..0e218f2be0fe 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -330,6 +330,68 @@ static unsigned int cpc_reg_access_width(const struct cpc_reg *reg) return reg->bit_width; } +enum cpc_platform_quirk { + CPC_QUIRK_PERF_LIMITED_OWNS_UNIT = BIT(0), +}; + +static const struct acpi_platform_list cpc_platform_quirk_list[] = { + { + .oem_id = "NVIDIA", + .oem_table_id = "T41", + .table = ACPI_SIG_DSDT, + .pred = all_versions, + .reason = "Performance Limited owns its access unit", + .data = CPC_QUIRK_PERF_LIMITED_OWNS_UNIT, + }, + { } +}; + +static DEFINE_MUTEX(cpc_platform_quirk_lock); +static bool cpc_platform_quirks_initialized; +static u32 cpc_platform_quirks; + +static int cpc_get_platform_quirks(u32 *quirks) +{ + int idx, ret = 0; + + mutex_lock(&cpc_platform_quirk_lock); + if (!cpc_platform_quirks_initialized) { + idx = acpi_match_platform_list(cpc_platform_quirk_list); + if (idx < 0 && idx != -ENODEV) { + ret = idx; + goto out; + } + if (idx >= 0) + cpc_platform_quirks = cpc_platform_quirk_list[idx].data; + cpc_platform_quirks_initialized = true; + } + *quirks = cpc_platform_quirks; +out: + mutex_unlock(&cpc_platform_quirk_lock); + + return ret; +} + +static void cpc_apply_platform_quirks(struct cpc_reg *reg, + unsigned int reg_idx, u32 quirks) +{ + unsigned int access_width; + + if (!(quirks & CPC_QUIRK_PERF_LIMITED_OWNS_UNIT) || + reg_idx != PERF_LIMITED || + reg->space_id != ACPI_ADR_SPACE_SYSTEM_MEMORY || + reg->bit_width != 2 || reg->bit_offset) + return; + + access_width = cpc_reg_access_width(reg); + if (access_width != 32) + return; + + reg->bit_width = access_width; + pr_info_once("firmware quirk: Performance Limited owns its access unit, using Bit Width %u\n", + access_width); +} + static u64 cpc_sysmem_access_size(const struct cpc_register_resource *reg) { unsigned int width = cpc_reg_access_width(®->cpc_entry.reg); @@ -1857,6 +1919,7 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) acpi_handle handle = pr->handle; unsigned int num_ent, i, cpc_rev; u32 unsupported_regs = 0; + u32 platform_quirks; int pcc_subspace_id = -1; bool pcc_data_ref = false; bool cpc_present = false; @@ -1866,6 +1929,12 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) if (per_cpu(cpc_desc_ptr, pr->id)) return 0; + ret = cpc_get_platform_quirks(&platform_quirks); + if (ret) { + pr_err("CPU%d: failed to match CPPC platform quirks: %d\n", + pr->id, ret); + return ret; + } per_cpu(cpu_pcc_subspace_idx, pr->id) = -1; if (!osc_sb_cppc2_support_acked) { @@ -2002,6 +2071,9 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) cpc_ptr->cpc_regs[i - 2].type = ACPI_TYPE_BUFFER; memcpy(&cpc_ptr->cpc_regs[i - 2].cpc_entry.reg, gas_t, sizeof(*gas_t)); + gas_t = &cpc_ptr->cpc_regs[i - 2].cpc_entry.reg; + cpc_apply_platform_quirks(gas_t, i - 2, + platform_quirks); /* * The PCC Subspace index is encoded inside diff --git a/drivers/acpi/utils.c b/drivers/acpi/utils.c index d499b72574ab..7c584d490d6a 100644 --- a/drivers/acpi/utils.c +++ b/drivers/acpi/utils.c @@ -1069,19 +1069,26 @@ EXPORT_SYMBOL(acpi_dev_is_video_device); * @plat: pointer to acpi_platform_list table terminated by a NULL entry * * Return the matched index if the system is found in the platform list. - * Otherwise, return a negative error code. + * Return -ENODEV for no match, or another negative error code if a table + * header could not be read and no entry matched. */ int acpi_match_platform_list(const struct acpi_platform_list *plat) { struct acpi_table_header hdr; + acpi_status status; + int ret = -ENODEV; int idx = 0; if (acpi_disabled) return -ENODEV; for (; plat->oem_id[0]; plat++, idx++) { - if (ACPI_FAILURE(acpi_get_table_header(plat->table, 0, &hdr))) + status = acpi_get_table_header(plat->table, 0, &hdr); + if (ACPI_FAILURE(status)) { + if (status != AE_NOT_FOUND) + ret = status == AE_NO_MEMORY ? -ENOMEM : -EIO; continue; + } if (strncmp(plat->oem_id, hdr.oem_id, ACPI_OEM_ID_SIZE)) continue; @@ -1096,6 +1103,6 @@ int acpi_match_platform_list(const struct acpi_platform_list *plat) return idx; } - return -ENODEV; + return ret; } EXPORT_SYMBOL(acpi_match_platform_list); From 6b016c5c96448c0a5f09328d37f66562d154a5e5 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:28:01 +0100 Subject: [PATCH 40/89] ACPI: CPPC: Validate FFH register fields before hardware access The x86 CPPC FFH accessors construct masks and shift values using the firmware's Bit Width and Bit Offset without checking that the field fits in an MSR. A zero-width field or a field extending beyond bit 63 can therefore cause an invalid shift. The safe MSR accessors only handle an access fault, not invalid field arithmetic after a successful read. Also, the 64-bit GAS address is implicitly narrowed to the 32-bit MSR number. A descriptor with nonzero upper address bits can access a different MSR from the one described by firmware. The arm64 AMU counter readers have the same unchecked field arithmetic. Validate the field bounds before reading a counter, including both descriptors in the paired counter path. Dispatch FFH accesses before decoding the GAS Access Size in the common read and write paths. Otherwise, a large access_width can trigger an invalid shift in GET_BIT_WIDTH() before the architecture validates its register. That field has architecture-specific FFH semantics and is not needed by the generic memory/port accessor on this path. Validate x86 MSR addresses and share the field-bounds checks between related accessors within each architecture. Fixes: a6cbcdd5ab5f ("ACPI / CPPC: Add support for functional fixed hardware address") Fixes: 68c5debcc06d ("arm64: implement CPPC FFH support using AMUs") Fixes: f489c948028b ("ACPI: CPPC: Fix access width used for PCC registers") Cc: All applicable Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-17-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- arch/arm64/kernel/topology.c | 12 +++++++++++- arch/x86/kernel/acpi/cppc.c | 14 ++++++++++++++ drivers/acpi/cppc_acpi.c | 10 ++++++---- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/arch/arm64/kernel/topology.c b/arch/arm64/kernel/topology.c index d28438f8b83f..2c030e78d5e4 100644 --- a/arch/arm64/kernel/topology.c +++ b/arch/arm64/kernel/topology.c @@ -470,6 +470,12 @@ static void amu_read_core_const_ctrs(void *val) cpu_read_corecnt(&ctrs->corecnt); } +static bool cpc_ffh_reg_valid(const struct cpc_reg *reg) +{ + return reg->bit_width && reg->bit_width <= 64 && + reg->bit_offset <= 64 - reg->bit_width; +} + static u64 cpc_ffh_extract_bits(const struct cpc_reg *reg, u64 val) { val &= GENMASK_ULL(reg->bit_offset + reg->bit_width - 1, @@ -506,7 +512,8 @@ int cpc_read_ffh_fb_ctrs(int cpu, struct cpc_reg *reg1, u64 *val1, struct amu_ffh_ctrs ctrs; int ret; - if (!is_amu_ctr_reg(reg1) || !is_amu_ctr_reg(reg2)) + if (!is_amu_ctr_reg(reg1) || !is_amu_ctr_reg(reg2) || + !cpc_ffh_reg_valid(reg1) || !cpc_ffh_reg_valid(reg2)) return -EINVAL; ret = counters_read_on_cpu(cpu, amu_read_core_const_ctrs, &ctrs); @@ -530,6 +537,9 @@ int cpc_read_ffh(int cpu, struct cpc_reg *reg, u64 *val) { int ret = -EOPNOTSUPP; + if (!cpc_ffh_reg_valid(reg)) + return -EINVAL; + switch ((u64)reg->address) { case CPC_FFH_CTR_CORE: ret = counters_read_on_cpu(cpu, cpu_read_corecnt, val); diff --git a/arch/x86/kernel/acpi/cppc.c b/arch/x86/kernel/acpi/cppc.c index bbade0da5130..d2185b7a2030 100644 --- a/arch/x86/kernel/acpi/cppc.c +++ b/arch/x86/kernel/acpi/cppc.c @@ -5,6 +5,7 @@ */ #include +#include #include #include @@ -45,10 +46,20 @@ bool cpc_ffh_supported(void) return true; } +static bool cpc_ffh_reg_valid(const struct cpc_reg *reg) +{ + return reg->address <= U32_MAX && reg->bit_width && + reg->bit_width <= 64 && + reg->bit_offset <= 64 - reg->bit_width; +} + int cpc_read_ffh(int cpunum, struct cpc_reg *reg, u64 *val) { int err; + if (!cpc_ffh_reg_valid(reg)) + return -EINVAL; + err = rdmsrq_safe_on_cpu(cpunum, reg->address, val); if (!err) { u64 mask = GENMASK_ULL(reg->bit_offset + reg->bit_width - 1, @@ -65,6 +76,9 @@ int cpc_write_ffh(int cpunum, struct cpc_reg *reg, u64 val) u64 rd_val; int err; + if (!cpc_ffh_reg_valid(reg)) + return -EINVAL; + err = rdmsrq_safe_on_cpu(cpunum, reg->address, &rd_val); if (!err) { u64 mask = GENMASK_ULL(reg->bit_offset + reg->bit_width - 1, diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 0e218f2be0fe..96a94df24231 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -2446,6 +2446,9 @@ static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) } *val = 0; + if (reg->space_id == ACPI_ADR_SPACE_FIXED_HARDWARE) + return cpc_read_ffh(cpu, reg, val); + size = GET_BIT_WIDTH(reg); if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { @@ -2490,8 +2493,6 @@ static int cpc_read(int cpu, struct cpc_register_resource *reg_res, u64 *val) return 0; } else if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) vaddr = reg_res->sys_mem_vaddr; - else if (reg->space_id == ACPI_ADR_SPACE_FIXED_HARDWARE) - return cpc_read_ffh(cpu, reg, val); else return acpi_os_read_memory((acpi_physical_address)reg->address, val, size); @@ -2538,6 +2539,9 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) return -EOPNOTSUPP; reg = ®_res->cpc_entry.reg; + if (reg->space_id == ACPI_ADR_SPACE_FIXED_HARDWARE) + return cpc_write_ffh(cpu, reg, val); + size = GET_BIT_WIDTH(reg); if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { @@ -2581,8 +2585,6 @@ static int cpc_write(int cpu, struct cpc_register_resource *reg_res, u64 val) return 0; } else if (reg->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) vaddr = reg_res->sys_mem_vaddr; - else if (reg->space_id == ACPI_ADR_SPACE_FIXED_HARDWARE) - return cpc_write_ffh(cpu, reg, val); else return acpi_os_write_memory((acpi_physical_address)reg->address, val, size); From 36e96b469da24868f727b4cd168d7cf01cbd163d Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:28:02 +0100 Subject: [PATCH 41/89] ACPI: CPPC: Propagate errors from cross-CPU FFH calls The arm64 and RISC-V CPPC FFH accessors ignore the return value of smp_call_function_single(). If the target CPU is offline, the call can return -ENXIO without running the callback. The accessors then consume callback output which was never initialized and may report success. CPU offlining leaves the CPU's CPPC sysfs attributes present. On arm64, the AMU feature mask does not exclude an offline CPU, so a paired counter read can expose uninitialized stack values through feedback_ctrs. RISC-V has the same problem in both SBI and CSR reads, and its write paths also inspect an uninitialized callback status after a failed cross-CPU call. Return the SMP call error before using callback output. The arm64 single and paired counter readers already check the helper's return value. For RISC-V, also check the callback status before copying a read value: an unsupported CSR sets the error but does not initialize the value. Fixes: 68c5debcc06d ("arm64: implement CPPC FFH support using AMUs") Fixes: 30f3ffbee86b ("ACPI: RISC-V: Add CPPC driver") Cc: All applicable Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-18-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- arch/arm64/kernel/topology.c | 2 +- drivers/acpi/riscv/cppc.c | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/arch/arm64/kernel/topology.c b/arch/arm64/kernel/topology.c index 2c030e78d5e4..55bd8fa3e95c 100644 --- a/arch/arm64/kernel/topology.c +++ b/arch/arm64/kernel/topology.c @@ -427,7 +427,7 @@ int counters_read_on_cpu(int cpu, smp_call_func_t func, void *val) return -EPERM; func(val); } else { - smp_call_function_single(cpu, func, val, 1); + return smp_call_function_single(cpu, func, val, 1); } return 0; diff --git a/drivers/acpi/riscv/cppc.c b/drivers/acpi/riscv/cppc.c index 42c1a9052470..4ea4ddedd91f 100644 --- a/drivers/acpi/riscv/cppc.c +++ b/drivers/acpi/riscv/cppc.c @@ -97,6 +97,7 @@ bool cpc_ffh_supported(void) int cpc_read_ffh(int cpu, struct cpc_reg *reg, u64 *val) { struct sbi_cppc_data data; + int ret; if (WARN_ON_ONCE(irqs_disabled())) return -EPERM; @@ -107,19 +108,27 @@ int cpc_read_ffh(int cpu, struct cpc_reg *reg, u64 *val) data.reg = FFH_CPPC_SBI_REG(reg->address); - smp_call_function_single(cpu, sbi_cppc_read, &data, 1); + ret = smp_call_function_single(cpu, sbi_cppc_read, &data, 1); + if (ret) + return ret; + if (data.ret.error) + return sbi_err_map_linux_errno(data.ret.error); *val = data.ret.value; - return (data.ret.error) ? sbi_err_map_linux_errno(data.ret.error) : 0; + return 0; } else if (FFH_CPPC_TYPE(reg->address) == FFH_CPPC_CSR) { data.reg = FFH_CPPC_CSR_NUM(reg->address); - smp_call_function_single(cpu, cppc_ffh_csr_read, &data, 1); + ret = smp_call_function_single(cpu, cppc_ffh_csr_read, &data, 1); + if (ret) + return ret; + if (data.ret.error) + return data.ret.error; *val = data.ret.value; - return data.ret.error; + return 0; } return -EINVAL; @@ -128,6 +137,7 @@ int cpc_read_ffh(int cpu, struct cpc_reg *reg, u64 *val) int cpc_write_ffh(int cpu, struct cpc_reg *reg, u64 val) { struct sbi_cppc_data data; + int ret; if (WARN_ON_ONCE(irqs_disabled())) return -EPERM; @@ -139,14 +149,18 @@ int cpc_write_ffh(int cpu, struct cpc_reg *reg, u64 val) data.reg = FFH_CPPC_SBI_REG(reg->address); data.val = val; - smp_call_function_single(cpu, sbi_cppc_write, &data, 1); + ret = smp_call_function_single(cpu, sbi_cppc_write, &data, 1); + if (ret) + return ret; return (data.ret.error) ? sbi_err_map_linux_errno(data.ret.error) : 0; } else if (FFH_CPPC_TYPE(reg->address) == FFH_CPPC_CSR) { data.reg = FFH_CPPC_CSR_NUM(reg->address); data.val = val; - smp_call_function_single(cpu, cppc_ffh_csr_write, &data, 1); + ret = smp_call_function_single(cpu, cppc_ffh_csr_write, &data, 1); + if (ret) + return ret; return data.ret.error; } From 4bcc60d326c5acaa28c5913259decb22a33a83a1 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:28:03 +0100 Subject: [PATCH 42/89] ACPI: CPPC: Accept requests to retain immutable autonomous selection ACPI permits Autonomous Selection Enable to be Integer 1 when the platform always selects performance autonomously. cppc_get_auto_sel() returns that value, but cppc_set_auto_sel() rejects the Integer even when asked to keep autonomous selection enabled. AMD shared-memory initialization consequently fails in active or guided mode after successfully reading the capability. Accept a request to keep this immutable field enabled without issuing a register access. Expose the immutable encoding to callers, separately from the current value of a writable register. Before an AMD shared-memory transition to passive mode, reject immutable autonomous selection on any present CPU. Otherwise a guided-to-passive transition ignores the failed disable and reports the wrong mode, while active-to-passive unregisters the working driver before passive policy initialization fails. Check under the driver mode lock before invoking either transition, including known offline CPUs. Serialize this capability query with descriptor publication and removal. ACPI processor removal drops the descriptor before taking the CPU hotplug write lock, so the caller's cpus_read_lock() alone cannot protect the lookup. Unpublish under the same mutex before releasing a descriptor, including when adding its kobject fails. The cppc-cpufreq auto_select attribute can also request one while autonomous selection is already enabled. If the subsequent bounds update fails, its rollback requests zero and clears the cached mode regardless of the previous state. For an immutable field the disable request fails, leaving the cached mode wrong. Save and restore the previous mode instead. This also avoids disabling an already-enabled writable control on that error path. Fixes: c984f5d5d45b ("ACPI: CPPC: Add auto select register read/write support") Fixes: ea3db45ae476 ("cpufreq: cppc: Update MIN_PERF/MAX_PERF in target callbacks") Cc: All applicable Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-19-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/acpi/cppc_acpi.c | 42 +++++++++++++++++++++++++++++++--- drivers/cpufreq/amd-pstate.c | 21 +++++++++++++---- drivers/cpufreq/cppc_cpufreq.c | 5 ++-- include/acpi/cppc_acpi.h | 7 ++++++ 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/drivers/acpi/cppc_acpi.c b/drivers/acpi/cppc_acpi.c index 96a94df24231..80e2e6b32ce3 100644 --- a/drivers/acpi/cppc_acpi.c +++ b/drivers/acpi/cppc_acpi.c @@ -100,6 +100,15 @@ static DEFINE_PER_CPU(int, cpu_pcc_subspace_idx); */ static DEFINE_PER_CPU(struct cpc_desc *, cpc_desc_ptr); +/* Protect immutable capability queries against descriptor removal. */ +static DEFINE_MUTEX(cpc_desc_lock); + +static void cpc_set_desc(unsigned int cpu, struct cpc_desc *desc) +{ + guard(mutex)(&cpc_desc_lock); + per_cpu(cpc_desc_ptr, cpu) = desc; +} + struct cpc_sysmem_node { struct rb_node rb; u64 subtree_last; @@ -2314,12 +2323,12 @@ int acpi_cppc_processor_probe(struct acpi_processor *pr) } /* Plug PSD data into this CPU's CPC descriptor. */ - per_cpu(cpc_desc_ptr, pr->id) = cpc_ptr; + cpc_set_desc(pr->id, cpc_ptr); ret = kobject_init_and_add(&cpc_ptr->kobj, &cppc_ktype, &cpu_dev->kobj, "acpi_cppc"); if (ret) { - per_cpu(cpc_desc_ptr, pr->id) = NULL; + cpc_set_desc(pr->id, NULL); cpc_unregister_non_mmio_desc(cpc_ptr); cpc_unregister_sysmem_desc(cpc_ptr); kobject_put(&cpc_ptr->kobj); @@ -2363,7 +2372,7 @@ void acpi_cppc_processor_exit(struct acpi_processor *pr) } pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, pr->id); - per_cpu(cpc_desc_ptr, pr->id) = NULL; + cpc_set_desc(pr->id, NULL); kobject_del(&cpc_ptr->kobj); cpc_unregister_non_mmio_desc(cpc_ptr); cpc_unregister_sysmem_desc(cpc_ptr); @@ -2766,6 +2775,11 @@ static int cppc_set_reg_val(int cpu, enum cppc_regs reg_idx, u64 val) reg = &cpc_desc->cpc_regs[reg_idx]; + /* Integer 1 describes autonomous selection that is always enabled. */ + if (reg_idx == AUTO_SEL_ENABLE && reg->type == ACPI_TYPE_INTEGER && + reg->cpc_entry.int_value == 1) + return val == 1 ? 0 : -EOPNOTSUPP; + /* if a register is writeable, it must be a buffer and not null */ if (!cpc_is_writable(reg)) { pr_debug("CPC register is not supported\n"); @@ -3310,6 +3324,28 @@ int cppc_get_auto_sel(int cpu, bool *enable) } EXPORT_SYMBOL_GPL(cppc_get_auto_sel); +/** + * cppc_auto_sel_is_immutable - Check for always-enabled autonomous selection. + * @cpu: CPU whose _CPC descriptor to check. + * + * Context: Process context. + * Return: true for Integer 1, false for a register or an absent descriptor. + */ +bool cppc_auto_sel_is_immutable(int cpu) +{ + struct cpc_desc *cpc_desc; + struct cpc_register_resource *reg; + + guard(mutex)(&cpc_desc_lock); + cpc_desc = per_cpu(cpc_desc_ptr, cpu); + if (!cpc_desc) + return false; + + reg = &cpc_desc->cpc_regs[AUTO_SEL_ENABLE]; + return reg->type == ACPI_TYPE_INTEGER && reg->cpc_entry.int_value == 1; +} +EXPORT_SYMBOL_GPL(cppc_auto_sel_is_immutable); + /** * cppc_set_auto_sel - Write autonomous selection register. * @cpu : CPU to which to write register. diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 6ee2d43b7b95..0a8b1aaa2660 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -1794,7 +1794,7 @@ EXPORT_SYMBOL_FOR_PSTATE_UT(amd_pstate_get_status); int amd_pstate_update_status(const char *buf, size_t size) { - int mode_idx; + int cpu, mode_idx; if (size > strlen("passive") || size < strlen("active")) return -EINVAL; @@ -1803,12 +1803,23 @@ int amd_pstate_update_status(const char *buf, size_t size) if (mode_idx < 0) return mode_idx; - if (mode_state_machine[cppc_state][mode_idx]) { - guard(mutex)(&amd_pstate_driver_lock); - return mode_state_machine[cppc_state][mode_idx](mode_idx); + guard(mutex)(&amd_pstate_driver_lock); + + if (!mode_state_machine[cppc_state][mode_idx]) + return 0; + + if (mode_idx == AMD_PSTATE_PASSIVE && + !cpu_feature_enabled(X86_FEATURE_CPPC)) { + guard(cpus_read_lock)(); + + /* Check offline CPUs too, before changing or removing the driver. */ + for_each_present_cpu(cpu) { + if (cppc_auto_sel_is_immutable(cpu)) + return -EOPNOTSUPP; + } } - return 0; + return mode_state_machine[cppc_state][mode_idx](mode_idx); } EXPORT_SYMBOL_FOR_PSTATE_UT(amd_pstate_update_status); diff --git a/drivers/cpufreq/cppc_cpufreq.c b/drivers/cpufreq/cppc_cpufreq.c index af46f913e907..9dde24d3e645 100644 --- a/drivers/cpufreq/cppc_cpufreq.c +++ b/drivers/cpufreq/cppc_cpufreq.c @@ -882,6 +882,7 @@ static ssize_t store_auto_select(struct cpufreq_policy *policy, const char *buf, size_t count) { struct cppc_cpudata *cpu_data = policy->driver_data; + bool old_auto_sel = cpu_data->perf_ctrls.auto_sel; bool val; int ret; @@ -910,8 +911,8 @@ static ssize_t store_auto_select(struct cpufreq_policy *policy, if (ret) { cpu_data->perf_ctrls.min_perf = old_min_perf; cpu_data->perf_ctrls.max_perf = old_max_perf; - cppc_set_auto_sel(policy->cpu, false); - cpu_data->perf_ctrls.auto_sel = false; + cppc_set_auto_sel(policy->cpu, old_auto_sel); + cpu_data->perf_ctrls.auto_sel = old_auto_sel; return ret; } } diff --git a/include/acpi/cppc_acpi.h b/include/acpi/cppc_acpi.h index 4b5d94f853dc..3f0005abac64 100644 --- a/include/acpi/cppc_acpi.h +++ b/include/acpi/cppc_acpi.h @@ -197,6 +197,7 @@ extern int cppc_set_epp(int cpu, u64 epp_val); extern int cppc_get_auto_act_window(int cpu, u64 *auto_act_window); extern int cppc_set_auto_act_window(int cpu, u64 auto_act_window); extern int cppc_get_auto_sel(int cpu, bool *enable); +bool cppc_auto_sel_is_immutable(int cpu); extern int cppc_set_auto_sel(int cpu, bool enable); extern int cppc_get_perf_limited(int cpu, u64 *perf_limited); extern int cppc_set_perf_limited(int cpu, u64 bits_to_clear); @@ -298,6 +299,12 @@ static inline int cppc_get_auto_sel(int cpu, bool *enable) { return -EOPNOTSUPP; } + +static inline bool cppc_auto_sel_is_immutable(int cpu) +{ + return false; +} + static inline int cppc_set_auto_sel(int cpu, bool enable) { return -EOPNOTSUPP; From 21e018235d99bc86249b034c06646df8757b2852 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:28:04 +0100 Subject: [PATCH 43/89] cpufreq: CPPC: Select the frequency-invariance callback per CPU cppc_cpufreq_cpu_fie_init() initializes deferred work only for CPUs whose feedback counters use PCC. However, if any CPU in a shared policy uses PCC, it installs the PCC tick callback for the entire policy. With FIE enabled, a non-PCC CPU then queues an uninitialized irq_work and can call a NULL function pointer when that work runs. Choose the frequency-invariance callback separately for each CPU using the recorded work-initialization state. Processor removal can unpublish the CPC descriptor before callback registration, so querying its counter transport again can incorrectly select direct reads for a PCC CPU. Register the callbacks after all counter initialization has completed, so a failed initial read for an online CPU cannot leave a partially registered policy. PCC CPUs use their initialized deferred work, while non-PCC CPUs read their own counters directly from the tick. Fixes: 997c021abc6e ("cpufreq: CPPC: Update FIE arch_freq_scale in ticks for non-PCC regs") Cc: All applicable Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-20-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/cpufreq/cppc_cpufreq.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/drivers/cpufreq/cppc_cpufreq.c b/drivers/cpufreq/cppc_cpufreq.c index 9dde24d3e645..c0fa9073e4d3 100644 --- a/drivers/cpufreq/cppc_cpufreq.c +++ b/drivers/cpufreq/cppc_cpufreq.c @@ -150,7 +150,7 @@ static struct scale_freq_data cppc_sftd_pcc = { static void cppc_cpufreq_cpu_fie_init(struct cpufreq_policy *policy) { - struct scale_freq_data *sftd = &cppc_sftd; + struct scale_freq_data *sftd; struct cppc_freq_invariance *cppc_fi; int cpu, ret; @@ -165,7 +165,6 @@ static void cppc_cpufreq_cpu_fie_init(struct cpufreq_policy *policy) kthread_init_work(&cppc_fi->work, cppc_scale_freq_workfn); init_irq_work(&cppc_fi->irq_work, cppc_irq_work); cppc_fi->pcc_work_initialized = true; - sftd = &cppc_sftd_pcc; } ret = cppc_get_perf_ctrs(cpu, &cppc_fi->prev_perf_fb_ctrs); @@ -181,8 +180,15 @@ static void cppc_cpufreq_cpu_fie_init(struct cpufreq_policy *policy) } } - /* Register for freq-invariance */ - topology_set_scale_freq_source(sftd, policy->cpus); + /* A shared policy may contain both PCC and non-PCC counters. */ + for_each_cpu(cpu, policy->cpus) { + cppc_fi = &per_cpu(cppc_freq_inv, cpu); + if (cppc_fi->pcc_work_initialized) + sftd = &cppc_sftd_pcc; + else + sftd = &cppc_sftd; + topology_set_scale_freq_source(sftd, cpumask_of(cpu)); + } } /* From 995e7d4680706e47154adb68f0b682485ea8d873 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Wed, 16 Sep 2026 17:28:05 +0100 Subject: [PATCH 44/89] cpufreq: CPPC: Create the FIE worker before enabling PCC callbacks cppc_freq_invariance_init() creates the PCC worker only if an online CPU currently has PCC counters. A shared policy can also initialize an offline PCC member. When that CPU comes online, its callback queues work through the missing kworker_fie and dereferences NULL. A later hot-added PCC policy can encounter the same missing worker. Create the worker when policy initialization encounters PCC counters, before initializing their work and publishing any callbacks. Serialize creation and reuse the worker until driver teardown. This covers offline policy members and later PCC policies without creating an unused deadline worker on direct-counter-only systems. If worker allocation or scheduling setup fails, leave FIE unregistered for that policy. Keep the global FIE setting unchanged so direct-counter policies remain independent of worker setup and existing policies still drain their initialized work during teardown. Publish kworker_fie only after successful setup, allowing a later policy initialization to retry. Fixes: 997c021abc6e ("cpufreq: CPPC: Update FIE arch_freq_scale in ticks for non-PCC regs") Cc: All applicable Signed-off-by: Christian Loehle Tested-by: Sumit Gupta Link: https://patch.msgid.link/20260916162805.1039247-21-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- drivers/cpufreq/cppc_cpufreq.c | 39 ++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/drivers/cpufreq/cppc_cpufreq.c b/drivers/cpufreq/cppc_cpufreq.c index c0fa9073e4d3..09e55bfdca88 100644 --- a/drivers/cpufreq/cppc_cpufreq.c +++ b/drivers/cpufreq/cppc_cpufreq.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -50,10 +51,12 @@ struct cppc_freq_invariance { static DEFINE_PER_CPU(struct cppc_freq_invariance, cppc_freq_inv); static struct kthread_worker *kworker_fie; +static DEFINE_MUTEX(cppc_fie_lock); static int cppc_perf_from_fbctrs(u64 reference_perf, struct cppc_perf_fb_ctrs *fb_ctrs_t0, struct cppc_perf_fb_ctrs *fb_ctrs_t1); +static int cppc_fie_kworker_init(void); /** * __cppc_scale_freq_tick - CPPC arch_freq_scale updater for frequency invariance @@ -162,6 +165,9 @@ static void cppc_cpufreq_cpu_fie_init(struct cpufreq_policy *policy) cppc_fi->cpu = cpu; cppc_fi->cpu_data = policy->driver_data; if (cppc_perf_ctrs_in_pcc_cpu(cpu)) { + if (cppc_fie_kworker_init()) + return; + kthread_init_work(&cppc_fi->work, cppc_scale_freq_workfn); init_irq_work(&cppc_fi->irq_work, cppc_irq_work); cppc_fi->pcc_work_initialized = true; @@ -216,8 +222,9 @@ static void cppc_cpufreq_cpu_fie_exit(struct cpufreq_policy *policy) } } -static void cppc_fie_kworker_init(void) +static int cppc_fie_kworker_init(void) { + struct kthread_worker *worker; struct sched_attr attr = { .size = sizeof(struct sched_attr), .sched_policy = SCHED_DEADLINE, @@ -233,23 +240,28 @@ static void cppc_fie_kworker_init(void) }; int ret; - kworker_fie = kthread_run_worker(0, "cppc_fie"); - if (IS_ERR(kworker_fie)) { + guard(mutex)(&cppc_fie_lock); + + if (kworker_fie) + return 0; + + worker = kthread_run_worker(0, "cppc_fie"); + if (IS_ERR(worker)) { pr_warn("%s: failed to create kworker_fie: %ld\n", __func__, - PTR_ERR(kworker_fie)); - fie_disabled = FIE_DISABLED; - kworker_fie = NULL; - return; + PTR_ERR(worker)); + return PTR_ERR(worker); } - ret = sched_setattr_nocheck(kworker_fie->task, &attr); + ret = sched_setattr_nocheck(worker->task, &attr); if (ret) { pr_warn("%s: failed to set SCHED_DEADLINE: %d\n", __func__, ret); - kthread_destroy_worker(kworker_fie); - fie_disabled = FIE_DISABLED; - kworker_fie = NULL; + kthread_destroy_worker(worker); + return ret; } + + kworker_fie = worker; + return 0; } static void __init cppc_freq_invariance_init(void) @@ -264,11 +276,6 @@ static void __init cppc_freq_invariance_init(void) fie_disabled = FIE_ENABLED; } } - - if (fie_disabled || !perf_ctrs_in_pcc) - return; - - cppc_fie_kworker_init(); } static void cppc_freq_invariance_exit(void) From 50fb9e04c9b1fe822baccba57801e2bc7795552e Mon Sep 17 00:00:00 2001 From: Xueqin Luo Date: Tue, 25 Aug 2026 21:14:00 +0800 Subject: [PATCH 45/89] cpufreq/amd-pstate: Restore previous EPP if profile_name allocation fails amd_pstate_set_dynamic_epp() first applies the EPP value derived from the current platform profile via amd_pstate_set_epp() and only then allocates the profile name and registers the platform profile handler and the power supply notifier. All failures after the EPP has been programmed jump to the cleanup label, which restores the previously programmed EPP value. However, if the kasprintf() of the profile name fails, the function returns -ENOMEM directly, bypassing the cleanup label, so the EPP value programmed above is never rolled back even though enabling dynamic EPP has failed. Make the allocation failure path jump to the cleanup label as well, so that the previous EPP value is restored consistently on all error paths. Fixes: b7294b627598 ("cpufreq/amd-pstate: Add dynamic EPP as an "energy_performance_preference" mode") Signed-off-by: Xueqin Luo Link: https://lore.kernel.org/r/20260825131400.1509428-1-luoxueqin@kylinos.cn Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 83960a6336b9..8b2eb72c3973 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -1381,8 +1381,10 @@ static int amd_pstate_set_dynamic_epp(struct cpufreq_policy *policy) return ret; cpudata->profile_name = kasprintf(GFP_KERNEL, "amd-pstate-epp-cpu%d", cpudata->cpu); - if (!cpudata->profile_name) - return -ENOMEM; + if (!cpudata->profile_name) { + ret = -ENOMEM; + goto cleanup; + } cpudata->ppdev = platform_profile_register(get_cpu_device(policy->cpu), cpudata->profile_name, From b400642c5e93863ec17ea15fcbd3efc81c299531 Mon Sep 17 00:00:00 2001 From: Wentao Guan Date: Fri, 18 Sep 2026 17:09:10 +0800 Subject: [PATCH 46/89] cpufreq/amd-pstate: Skip auto_sel write when it already matches the mode Since commit 9dfd13f80c85 ("cpufreq/amd-pstate: Toggle auto_sel in active mode on shared memory systems"), shmem_init_perf() programs AUTONOMOUS_SELECTION_ENABLE unconditionally, including in active mode. Platforms that exclusively support autonomous selection report this field as an Integer of 1, as required by the ACPI specification, instead of as a register descriptor. cppc_get_reg_val() reads such a field back successfully, but cppc_set_reg_val() rejects the write with -EOPNOTSUPP because the entry is not an ACPI_TYPE_BUFFER. shmem_init_perf() propagates that error, so every amd_pstate_epp_cpu_init() fails, cpufreq_register_driver() ends up with an empty policy list and returns -ENODEV, and amd-pstate refuses to load at all: amd_pstate: failed to set auto_sel, ret: -95 amd_pstate: Failed to initialize CPU 0: -95 ... amd_pstate: failed to register with return -19 Skip the write when the register already holds the value the requested mode needs. Active and guided mode are fixed on these platforms, which report auto_sel as 1, while passive mode still fails to load on a platform that cannot disable autonomous selection, exactly as it did before the offending commit. Fixes: 9dfd13f80c85 ("cpufreq/amd-pstate: Toggle auto_sel in active mode on shared memory systems") Cc: stable@vger.kernel.org Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Tested-by: LFRon Signed-off-by: Wentao Guan Link: https://lore.kernel.org/r/20260918090909.86544-1-guanwentao@uniontech.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 8b2eb72c3973..c93a4b765dff 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -609,8 +609,10 @@ static int shmem_init_perf(struct amd_cpudata *cpudata) return 0; } - ret = cppc_set_auto_sel(cpudata->cpu, - (cppc_state == AMD_PSTATE_PASSIVE) ? 0 : 1); + if ((cppc_state != AMD_PSTATE_PASSIVE) == auto_sel) + return 0; + + ret = cppc_set_auto_sel(cpudata->cpu, !auto_sel); if (ret) pr_warn("failed to set auto_sel, ret: %d\n", ret); From 33392343c74e417375b87032c66078a3bbc1dc91 Mon Sep 17 00:00:00 2001 From: Mayank Rungta Date: Wed, 9 Sep 2026 15:48:18 -0700 Subject: [PATCH 47/89] PM: sleep: Add DPM watchdog to prepare/late/early/noirq/complete phases Extend the DPM watchdog to wrap device_prepare, device_suspend_late, device_suspend_noirq, device_resume_noirq, device_resume_early, and device_complete callbacks. If a driver hangs during these transitions, the watchdog will fire and dump a stack trace to help identify the offending driver. To prevent false-positive timeouts, the watchdog is set only after waiting for subordinate (during suspend) and superior (during resume) devices. With this change, DPM watchdog coverage is extended across all phases of system sleep transitions. Signed-off-by: Mayank Rungta Reviewed-by: Douglas Anderson Reviewed-by: Tzung-Bi Shih Link: https://patch.msgid.link/20260909224818.2177311-1-mrungta@google.com Signed-off-by: Rafael J. Wysocki --- drivers/base/power/main.c | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c index e130da428141..4e0d6357a37c 100644 --- a/drivers/base/power/main.c +++ b/drivers/base/power/main.c @@ -802,6 +802,7 @@ static void device_resume_noirq(struct device *dev, pm_message_t state, bool asy const char *info = NULL; bool skip_resume; int error = 0; + DECLARE_DPM_WATCHDOG_ON_STACK(wd); TRACE_DEVICE(dev); TRACE_RESUME(0); @@ -827,6 +828,7 @@ static void device_resume_noirq(struct device *dev, pm_message_t state, bool asy if (!dpm_wait_for_superior(dev, async)) goto Out; + dpm_watchdog_set(&wd, dev); skip_resume = dev_pm_skip_resume(dev); /* * If the driver callback is skipped below or by the middle layer @@ -871,6 +873,7 @@ static void device_resume_noirq(struct device *dev, pm_message_t state, bool asy error = dpm_run_callback(callback, dev, state, info); Skip: + dpm_watchdog_clear(&wd); dev->power.is_noirq_suspended = false; Out: @@ -971,6 +974,7 @@ static void device_resume_early(struct device *dev, pm_message_t state, bool asy pm_callback_t callback = NULL; const char *info = NULL; int error = 0; + DECLARE_DPM_WATCHDOG_ON_STACK(wd); TRACE_DEVICE(dev); TRACE_RESUME(0); @@ -987,6 +991,7 @@ static void device_resume_early(struct device *dev, pm_message_t state, bool asy if (!dpm_wait_for_superior(dev, async)) goto Out; + dpm_watchdog_set(&wd, dev); if (dev->pm_domain) { info = "early power domain "; callback = pm_late_early_op(&dev->pm_domain->ops, state); @@ -1004,7 +1009,7 @@ static void device_resume_early(struct device *dev, pm_message_t state, bool asy goto Run; if (dev_pm_skip_resume(dev)) - goto Skip; + goto End; if (dev->driver && dev->driver->pm) { info = "early driver "; @@ -1014,6 +1019,9 @@ static void device_resume_early(struct device *dev, pm_message_t state, bool asy Run: error = dpm_run_callback(callback, dev, state, info); +End: + dpm_watchdog_clear(&wd); + Skip: dev->power.is_late_suspended = false; pm_runtime_enable(dev); @@ -1281,10 +1289,12 @@ static void device_complete(struct device *dev, pm_message_t state) { void (*callback)(struct device *) = NULL; const char *info = NULL; + DECLARE_DPM_WATCHDOG_ON_STACK(wd); if (dev->power.syscore) goto out; + dpm_watchdog_set(&wd, dev); device_lock(dev); if (dev->pm_domain) { @@ -1312,6 +1322,7 @@ static void device_complete(struct device *dev, pm_message_t state) } device_unlock(dev); + dpm_watchdog_clear(&wd); out: /* If enabling runtime PM for the device is blocked, unblock it. */ @@ -1508,6 +1519,7 @@ static void device_suspend_noirq(struct device *dev, pm_message_t state, bool as pm_callback_t callback = NULL; const char *info = NULL; int error = 0; + DECLARE_DPM_WATCHDOG_ON_STACK(wd); TRACE_DEVICE(dev); TRACE_SUSPEND(0); @@ -1520,6 +1532,7 @@ static void device_suspend_noirq(struct device *dev, pm_message_t state, bool as if (dev->power.syscore || dev->power.direct_complete) goto Complete; + dpm_watchdog_set(&wd, dev); if (dev->pm_domain) { info = "noirq power domain "; callback = pm_noirq_op(&dev->pm_domain->ops, state); @@ -1550,7 +1563,7 @@ static void device_suspend_noirq(struct device *dev, pm_message_t state, bool as WRITE_ONCE(async_error, error); dpm_save_failed_dev(dev_name(dev)); pm_dev_err(dev, state, async ? " async noirq" : " noirq", error); - goto Complete; + goto End; } Skip: @@ -1569,6 +1582,9 @@ static void device_suspend_noirq(struct device *dev, pm_message_t state, bool as if (dev->power.must_resume) dpm_superior_set_must_resume(dev); +End: + dpm_watchdog_clear(&wd); + Complete: complete_all(&dev->power.completion); TRACE_SUSPEND(error); @@ -1703,6 +1719,7 @@ static void device_suspend_late(struct device *dev, pm_message_t state, bool asy pm_callback_t callback = NULL; const char *info = NULL; int error = 0; + DECLARE_DPM_WATCHDOG_ON_STACK(wd); TRACE_DEVICE(dev); TRACE_SUSPEND(0); @@ -1720,6 +1737,8 @@ static void device_suspend_late(struct device *dev, pm_message_t state, bool asy if (dev->power.direct_complete) goto Complete; + dpm_watchdog_set(&wd, dev); + /* * After this point, any runtime PM operations targeting the device * will fail until the corresponding pm_runtime_enable() call in @@ -1761,13 +1780,16 @@ static void device_suspend_late(struct device *dev, pm_message_t state, bool asy dpm_save_failed_dev(dev_name(dev)); pm_dev_err(dev, state, async ? " async late" : " late", error); pm_runtime_enable(dev); - goto Complete; + goto End; } dpm_propagate_wakeup_to_parent(dev); Skip: dev->power.is_late_suspended = true; +End: + dpm_watchdog_clear(&wd); + Complete: TRACE_SUSPEND(error); complete_all(&dev->power.completion); @@ -2201,6 +2223,7 @@ static int device_prepare(struct device *dev, pm_message_t state) int (*callback)(struct device *) = NULL; bool smart_suspend; int ret = 0; + DECLARE_DPM_WATCHDOG_ON_STACK(wd); /* * If a device's parent goes into runtime suspend at the wrong time, @@ -2220,6 +2243,7 @@ static int device_prepare(struct device *dev, pm_message_t state) if (dev->power.syscore) return 0; + dpm_watchdog_set(&wd, dev); device_lock(dev); dev->power.wakeup_path = false; @@ -2245,6 +2269,7 @@ static int device_prepare(struct device *dev, pm_message_t state) unlock: device_unlock(dev); + dpm_watchdog_clear(&wd); if (ret < 0) { suspend_report_result(dev, callback, ret); From 53d39bfd205da83ff1f67a7da75d1505bd781f3d Mon Sep 17 00:00:00 2001 From: Hemanth Selam Date: Mon, 7 Sep 2026 12:17:21 +0530 Subject: [PATCH 48/89] PM: clk: fix typo "sucessfully" in comment Correct "sucessfully" to "successfully", reported by scripts/checkpatch.pl using the misspelling list in scripts/spelling.txt. Only touches comments, no code changes. Signed-off-by: Hemanth Selam Reviewed-by: Randy Dunlap [ rjw: Subject adjustment ] Link: https://patch.msgid.link/20260907064725.10686-2-hemanth.selam@gmail.com Signed-off-by: Rafael J. Wysocki --- drivers/base/power/clock_ops.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/base/power/clock_ops.c b/drivers/base/power/clock_ops.c index 59bb37e8244c..3b8ba27802a8 100644 --- a/drivers/base/power/clock_ops.c +++ b/drivers/base/power/clock_ops.c @@ -250,7 +250,7 @@ EXPORT_SYMBOL_GPL(pm_clk_add); * * Add the clock to the list of clocks used for the power management of @dev. * The power-management code will take control of the clock reference, so - * callers should not call clk_put() on @clk after this function sucessfully + * callers should not call clk_put() on @clk after this function successfully * returned. */ int pm_clk_add_clk(struct device *dev, struct clk *clk) From 459199ccd1813b0cb9193962a474c2e24b1b373f Mon Sep 17 00:00:00 2001 From: Sumeet Pawnikar Date: Sun, 13 Sep 2026 00:41:55 +0530 Subject: [PATCH 49/89] powercap: intel_rapl: Use %pe to print error pointers symbolically Replace PTR_ERR() and %ld with %pe and pass the original pointer directly to pr_debug(). The %pe format specifier prints a symbolic error name (e.g. -ENOMEM) when CONFIG_SYMBOLIC_ERRNAME is enabled, otherwise it falls back gracefully and prints the raw integer value. This makes messages more readable without any functional change. Signed-off-by: Sumeet Pawnikar Link: https://patch.msgid.link/20260912191156.185448-6-sumeet4linux@gmail.com Signed-off-by: Rafael J. Wysocki --- drivers/powercap/intel_rapl_msr.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/powercap/intel_rapl_msr.c b/drivers/powercap/intel_rapl_msr.c index 2c1b799d3c54..d6686d03056a 100644 --- a/drivers/powercap/intel_rapl_msr.c +++ b/drivers/powercap/intel_rapl_msr.c @@ -583,8 +583,8 @@ static int intel_rapl_msr_init(void) rapl_msr_platdev = platform_device_register_data(NULL, "intel_rapl_msr", 0, def, sizeof(*def)); if (IS_ERR(rapl_msr_platdev)) - pr_debug("intel_rapl_msr device register failed, ret:%ld\n", - PTR_ERR(rapl_msr_platdev)); + pr_debug("intel_rapl_msr device register failed, ret:%pe\n", + rapl_msr_platdev); return 0; } From bd3135fba0f37e1de31b1e19499660d1aaf9f19e Mon Sep 17 00:00:00 2001 From: Syazwina Fazrul Date: Tue, 15 Sep 2026 23:00:55 +0100 Subject: [PATCH 50/89] Documentation: power/cpuidle: Fix repeating phrase remove repeating 'with the' phrase Signed-off-by: Syazwina Fazrul Acked-by: Randy Dunlap Link: https://patch.msgid.link/20260915220105.14655-1-norsyazwina.fazrul@gmail.com Signed-off-by: Rafael J. Wysocki --- Documentation/admin-guide/pm/cpuidle.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/admin-guide/pm/cpuidle.rst b/Documentation/admin-guide/pm/cpuidle.rst index be4c1120e3f0..e91ebd432f6f 100644 --- a/Documentation/admin-guide/pm/cpuidle.rst +++ b/Documentation/admin-guide/pm/cpuidle.rst @@ -316,7 +316,7 @@ interval" value. Now, the governor is ready to walk the list of idle states and choose one of them. For this purpose, it compares the target residency of each state with -the predicted idle duration and the exit latency of it with the with the latency +the predicted idle duration and the exit latency of it with the latency limit coming from the power management quality of service, or `PM QoS `_, framework. It selects the state with the target residency closest to the predicted idle duration, but still below it, and exit latency that does not exceed the From d1706f9da9f40dffe8621cc0ee03b847f41f1d6d Mon Sep 17 00:00:00 2001 From: Joshua Crofts Date: Sat, 19 Sep 2026 10:24:28 +0200 Subject: [PATCH 51/89] PM: runtime: call pm_runtime_dont_use_autosuspend() on reinit() Add a pm_runtime_dont_use_autosuspend() call to pm_runtime_reinit() to ensure autosuspend is disabled on driver teardown. Suggested-by: Frank Li Signed-off-by: Joshua Crofts Link: https://patch.msgid.link/20260919-move-dont-use-autosuspend-v1-1-f6e2d1315c23@gmail.com Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c index bb008dfe85a1..655c0b8af095 100644 --- a/drivers/base/power/runtime.c +++ b/drivers/base/power/runtime.c @@ -1890,6 +1890,9 @@ void pm_runtime_reinit(struct device *dev) if (dev->power.runtime_status == RPM_ACTIVE) pm_runtime_set_suspended(dev); + if (dev->power.use_autosuspend) + pm_runtime_dont_use_autosuspend(dev); + if (dev->power.irq_safe) { spin_lock_irq(&dev->power.lock); dev->power.irq_safe = 0; From f58dacfbadb7be7c7dbb2b95e9341ad56710849e Mon Sep 17 00:00:00 2001 From: Fan Wu Date: Tue, 22 Sep 2026 08:10:52 +0000 Subject: [PATCH 52/89] PM: EM: Fix use-after-free of perf domain in netlink doit handlers The doit handlers dropped em_pd_list_mutex before dereferencing the perf domain, so a concurrent device unregister could free it while the reply was being built. Encode the reply from em_perf_domain_for_id() callbacks, which run under the mutex, and allocate the skb outside of it, like the dumpit path already does. This issue was found by an in-house static analysis tool. Fixes: 380ff27af25e ("PM: EM: Add dump to get-perf-domains in the EM YNL spec") Fixes: f2d2946eaa5c ("PM: EM: Implement em_nl_get_pd_table_doit()") Cc: All applicable Co-developed-by: Song Li Signed-off-by: Song Li Signed-off-by: Fan Wu Link: https://patch.msgid.link/20260922081052.321581-1-fanwu01@zju.edu.cn Signed-off-by: Rafael J. Wysocki --- kernel/power/em_netlink.c | 126 ++++++++++++++++++++++-------------- kernel/power/em_netlink.h | 8 ++- kernel/power/energy_model.c | 8 ++- 3 files changed, 89 insertions(+), 53 deletions(-) diff --git a/kernel/power/em_netlink.c b/kernel/power/em_netlink.c index 4d4fd29bd2be..1f68d456a713 100644 --- a/kernel/power/em_netlink.c +++ b/kernel/power/em_netlink.c @@ -95,41 +95,62 @@ static int __em_nl_get_pd_for_dump(struct em_perf_domain *pd, void *data) return ret; } -int dev_energymodel_nl_get_perf_domains_doit(struct sk_buff *skb, - struct genl_info *info) -{ - int id, ret = -EMSGSIZE, msg_sz = 0; - int cmd = info->genlhdr->cmd; - struct em_perf_domain *pd; +struct em_nl_doit_ctx { + struct genl_info *info; + int cmd; struct sk_buff *msg; +}; + +static int __em_nl_get_pd_doit_fill(struct em_perf_domain *pd, void *data) +{ + struct em_nl_doit_ctx *ctx = data; void *hdr; + hdr = genlmsg_put_reply(ctx->msg, ctx->info, &dev_energymodel_nl_family, 0, + ctx->cmd); + if (!hdr) + return -EMSGSIZE; + + if (__em_nl_get_pd(pd, ctx->msg)) { + genlmsg_cancel(ctx->msg, hdr); + return -EMSGSIZE; + } + + genlmsg_end(ctx->msg, hdr); + return 0; +} + +int dev_energymodel_nl_get_perf_domains_doit(struct sk_buff *skb, + struct genl_info *info) +{ + struct em_nl_doit_ctx ctx = { + .info = info, + .cmd = info->genlhdr->cmd, + }; + struct sk_buff *msg; + int id, ret, msg_sz = 0; + if (!info->attrs[DEV_ENERGYMODEL_A_PERF_DOMAIN_PERF_DOMAIN_ID]) return -EINVAL; id = nla_get_u32(info->attrs[DEV_ENERGYMODEL_A_PERF_DOMAIN_PERF_DOMAIN_ID]); - pd = em_perf_domain_get_by_id(id); - if (!pd) - return -EINVAL; - __em_nl_get_pd_size(pd, &msg_sz); + /* Encode under em_pd_list_mutex, like the dumpit path. */ + ret = em_perf_domain_for_id(id, __em_nl_get_pd_size, &msg_sz); + if (ret) + return ret; + msg = genlmsg_new(msg_sz, GFP_KERNEL); if (!msg) return -ENOMEM; - hdr = genlmsg_put_reply(msg, info, &dev_energymodel_nl_family, 0, cmd); - if (!hdr) - goto out_free_msg; - - ret = __em_nl_get_pd(pd, msg); + ctx.msg = msg; + ret = em_perf_domain_for_id(id, __em_nl_get_pd_doit_fill, &ctx); if (ret) - goto out_cancel_msg; - genlmsg_end(msg, hdr); + goto out_free_msg; return genlmsg_reply(msg, info); -out_cancel_msg: - genlmsg_cancel(msg, hdr); out_free_msg: nlmsg_free(msg); return ret; @@ -148,19 +169,6 @@ int dev_energymodel_nl_get_perf_domains_dumpit(struct sk_buff *skb, return for_each_em_perf_domain(__em_nl_get_pd_for_dump, &ctx); } -static struct em_perf_domain *__em_nl_get_pd_table_id(struct nlattr **attrs) -{ - struct em_perf_domain *pd; - int id; - - if (!attrs[DEV_ENERGYMODEL_A_PERF_TABLE_PERF_DOMAIN_ID]) - return NULL; - - id = nla_get_u32(attrs[DEV_ENERGYMODEL_A_PERF_TABLE_PERF_DOMAIN_ID]); - pd = em_perf_domain_get_by_id(id); - return pd; -} - static int __em_nl_get_pd_table_size(const struct em_perf_domain *pd) { int id_sz, ps_sz; @@ -245,34 +253,58 @@ int __em_nl_get_pd_table(struct sk_buff *msg, const struct em_perf_domain *pd) return -EMSGSIZE; } -int dev_energymodel_nl_get_perf_table_doit(struct sk_buff *skb, - struct genl_info *info) +static int __em_nl_get_pd_table_size_cb(struct em_perf_domain *pd, void *data) { - int cmd = info->genlhdr->cmd; - int msg_sz, ret = -EMSGSIZE; - struct em_perf_domain *pd; - struct sk_buff *msg; + *(int *)data = __em_nl_get_pd_table_size(pd); + return 0; +} + +static int __em_nl_get_pd_table_doit_fill(struct em_perf_domain *pd, + void *data) +{ + struct em_nl_doit_ctx *ctx = data; void *hdr; - pd = __em_nl_get_pd_table_id(info->attrs); - if (!pd) + hdr = genlmsg_put_reply(ctx->msg, ctx->info, &dev_energymodel_nl_family, 0, + ctx->cmd); + if (!hdr) + return -EMSGSIZE; + + if (__em_nl_get_pd_table(ctx->msg, pd)) + return -EMSGSIZE; + + genlmsg_end(ctx->msg, hdr); + return 0; +} + +int dev_energymodel_nl_get_perf_table_doit(struct sk_buff *skb, + struct genl_info *info) +{ + struct em_nl_doit_ctx ctx = { + .info = info, + .cmd = info->genlhdr->cmd, + }; + struct sk_buff *msg; + int id, ret, msg_sz; + + if (!info->attrs[DEV_ENERGYMODEL_A_PERF_TABLE_PERF_DOMAIN_ID]) return -EINVAL; - msg_sz = __em_nl_get_pd_table_size(pd); + id = nla_get_u32(info->attrs[DEV_ENERGYMODEL_A_PERF_TABLE_PERF_DOMAIN_ID]); + + ret = em_perf_domain_for_id(id, __em_nl_get_pd_table_size_cb, &msg_sz); + if (ret) + return ret; msg = genlmsg_new(msg_sz, GFP_KERNEL); if (!msg) return -ENOMEM; - hdr = genlmsg_put_reply(msg, info, &dev_energymodel_nl_family, 0, cmd); - if (!hdr) - goto out_free_msg; - - ret = __em_nl_get_pd_table(msg, pd); + ctx.msg = msg; + ret = em_perf_domain_for_id(id, __em_nl_get_pd_table_doit_fill, &ctx); if (ret) goto out_free_msg; - genlmsg_end(msg, hdr); return genlmsg_reply(msg, info); out_free_msg: diff --git a/kernel/power/em_netlink.h b/kernel/power/em_netlink.h index 583d7f1c3939..bc98a3c278b9 100644 --- a/kernel/power/em_netlink.h +++ b/kernel/power/em_netlink.h @@ -12,7 +12,8 @@ #if defined(CONFIG_ENERGY_MODEL) && defined(CONFIG_NET) int for_each_em_perf_domain(int (*cb)(struct em_perf_domain*, void *), void *data); -struct em_perf_domain *em_perf_domain_get_by_id(int id); +int em_perf_domain_for_id(int id, int (*cb)(struct em_perf_domain *, void *), + void *data); void em_notify_pd_created(const struct em_perf_domain *pd); void em_notify_pd_deleted(const struct em_perf_domain *pd); void em_notify_pd_updated(const struct em_perf_domain *pd); @@ -24,9 +25,10 @@ int for_each_em_perf_domain(int (*cb)(struct em_perf_domain*, void *), return -EINVAL; } static inline -struct em_perf_domain *em_perf_domain_get_by_id(int id) +int em_perf_domain_for_id(int id, int (*cb)(struct em_perf_domain *, void *), + void *data) { - return NULL; + return -EINVAL; } static inline void em_notify_pd_created(const struct em_perf_domain *pd) {} diff --git a/kernel/power/energy_model.c b/kernel/power/energy_model.c index e610cf8e9a06..a76089e2ce8c 100644 --- a/kernel/power/energy_model.c +++ b/kernel/power/energy_model.c @@ -1031,7 +1031,9 @@ int for_each_em_perf_domain(int (*cb)(struct em_perf_domain*, void *), return 0; } -struct em_perf_domain *em_perf_domain_get_by_id(int id) +/* Run @cb on the matching domain with em_pd_list_mutex held. */ +int em_perf_domain_for_id(int id, int (*cb)(struct em_perf_domain *, void *), + void *data) { struct em_perf_domain *pd; @@ -1040,9 +1042,9 @@ struct em_perf_domain *em_perf_domain_get_by_id(int id) list_for_each_entry(pd, &em_pd_list, node) { if (pd->id == id) - return pd; + return cb(pd, data); } - return NULL; + return -EINVAL; } #endif From 4c91a2f4805c3b44849526330c4600d4bd2de176 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Tue, 29 Sep 2026 19:49:49 -0700 Subject: [PATCH 53/89] PM: runtime: Correct pm_runtime_autosuspend_expiration() doc The "adjusted to be nonzero" comment may be a relic from when this API previously used jiffies, although I'm not quite sure about that either. In any case, it doesn't seem correct today. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260929194956.v3.1.Ifc8b2d1742729cc82f46220d85c6382fdee1fc8b@changeid Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c index 655c0b8af095..bb366a295a73 100644 --- a/drivers/base/power/runtime.c +++ b/drivers/base/power/runtime.c @@ -169,7 +169,7 @@ static void pm_runtime_cancel_pending(struct device *dev) * Compute the autosuspend-delay expiration time based on the device's * power.last_busy time. If the delay has already expired or is disabled * (negative) or the power.use_autosuspend flag isn't set, return 0. - * Otherwise return the expiration time in nanoseconds (adjusted to be nonzero). + * Otherwise return the expiration time in nanoseconds. * * This function may be called either with or without dev->power.lock held. * Either way it can be racy, since power.last_busy may be updated at any time. From f667dd6713d6c5b1140d7e8d73b776f898dfb654 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Tue, 29 Sep 2026 19:49:50 -0700 Subject: [PATCH 54/89] PM: runtime: More kerneldoc formatting In taking another pass at these docs, I found some more inconsistencies. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260929194956.v3.2.Ia1cae8a40a24662df11553c225f4438eab65b7db@changeid Signed-off-by: Rafael J. Wysocki --- drivers/base/power/runtime.c | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/drivers/base/power/runtime.c b/drivers/base/power/runtime.c index bb366a295a73..304a2b123420 100644 --- a/drivers/base/power/runtime.c +++ b/drivers/base/power/runtime.c @@ -1254,12 +1254,11 @@ static int pm_runtime_get_conditional(struct device *dev, bool ign_usage_count) } /** - * pm_runtime_get_if_active - Bump up runtime PM usage counter if the device is - * in active state + * pm_runtime_get_if_active - Conditionally bump up runtime PM usage counter. * @dev: Target device. * * Increment the runtime PM usage counter of @dev if its runtime PM status is - * already %RPM_ACTIVE + * already %RPM_ACTIVE. * * Return: * * %-EINVAL: Runtime PM is disabled for @dev. The usage counter is not incremented. @@ -1607,7 +1606,6 @@ static void pm_runtime_set_suspended_action(void *data) /** * devm_pm_runtime_set_active_enabled - set_active version of devm_pm_runtime_enable. - * * @dev: Device to handle. */ int devm_pm_runtime_set_active_enabled(struct device *dev) @@ -1634,11 +1632,10 @@ static void pm_runtime_disable_action(void *data) /** * devm_pm_runtime_enable - devres-enabled version of pm_runtime_enable. + * @dev: Device to handle. * * NOTE: this will also handle calling pm_runtime_dont_use_autosuspend() for * you at driver exit time if needed. - * - * @dev: Device to handle. */ int devm_pm_runtime_enable(struct device *dev) { @@ -1655,7 +1652,6 @@ static void pm_runtime_put_noidle_action(void *data) /** * devm_pm_runtime_get_noresume - devres-enabled version of pm_runtime_get_noresume. - * * @dev: Device to handle. */ int devm_pm_runtime_get_noresume(struct device *dev) From 437b4ed6cb5848027fe9b9c8d0da495eef7f8de3 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Tue, 29 Sep 2026 19:49:51 -0700 Subject: [PATCH 55/89] PM: runtime: Misc improvements to runtime_pm.rst There are several small errors and omissions, as well as new updates (pm_runtime_resume_and_get(), devm_pm_runtime_enable()) we should incorporate. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260929194956.v3.3.I383681b22c12d7caee976cb91aa90d1a94d4a591@changeid Signed-off-by: Rafael J. Wysocki --- Documentation/power/runtime_pm.rst | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/Documentation/power/runtime_pm.rst b/Documentation/power/runtime_pm.rst index 39fdeeda7a1e..59b634fd6829 100644 --- a/Documentation/power/runtime_pm.rst +++ b/Documentation/power/runtime_pm.rst @@ -229,6 +229,8 @@ It is safe to execute the following helper functions from interrupt context: - pm_request_resume() - pm_runtime_get_noresume() - pm_runtime_get() +- pm_runtime_get_if_active() +- pm_runtime_get_if_in_use() - pm_runtime_put_noidle() - pm_runtime_put() - pm_runtime_put_autosuspend() @@ -238,6 +240,9 @@ It is safe to execute the following helper functions from interrupt context: - pm_runtime_set_active() - pm_runtime_set_suspended() - pm_runtime_suspended() +- pm_runtime_active() +- pm_runtime_status_suspended() +- pm_runtime_enabled() - pm_runtime_mark_last_busy() - pm_runtime_autosuspend_expiration() @@ -249,6 +254,7 @@ functions may also be used in interrupt context: - pm_runtime_autosuspend() - pm_runtime_resume() - pm_runtime_get_sync() +- pm_runtime_resume_and_get() - pm_runtime_put_sync() - pm_runtime_put_sync_suspend() - pm_runtime_put_sync_autosuspend() @@ -258,7 +264,7 @@ functions may also be used in interrupt context: Initially, the runtime PM is disabled for all devices, which means that the majority of the runtime PM helper functions described in Section 4 will return --EAGAIN until pm_runtime_enable() is called for the device. +-EACCES until pm_runtime_enable() is called for the device. In addition to that, the initial runtime PM status of all devices is 'suspended', but it need not reflect the actual physical state of the device. @@ -287,7 +293,7 @@ enabled earlier by calling pm_runtime_enable(). Note, if the device may execute pm_runtime calls during the probe (such as if it is registered with a subsystem that may call back in) then the -pm_runtime_get_sync() call paired with a pm_runtime_put() call will be +pm_runtime_resume_and_get() call paired with a pm_runtime_put() call will be appropriate to ensure that the device is not put back to sleep during the probe. This can happen with systems such as the network device layer. @@ -315,7 +321,10 @@ removal of their drivers. Drivers in ->remove() callback should undo the runtime PM changes done in ->probe(). Usually this means calling pm_runtime_disable(), -pm_runtime_dont_use_autosuspend() etc. +pm_runtime_dont_use_autosuspend() etc. Alternatively, drivers can use +devm_pm_runtime_enable() during probe, which automatically takes care of +calling pm_runtime_disable() and pm_runtime_dont_use_autosuspend() upon driver +detachment. The user space can effectively disallow the driver of the device to power manage it at run time by changing the value of its /sys/devices/.../power/control @@ -426,7 +435,7 @@ out the following operations: Subsystems may wish to conserve code space by using the set of generic power management callbacks provided by the PM core, defined in -driver/base/power/generic_ops.c: +drivers/base/power/generic_ops.c: .. kernel-doc:: drivers/base/power/generic_ops.c :export: @@ -441,8 +450,8 @@ subsystem-level dev_pm_ops structure. Device drivers that wish to use the same function as a system suspend, freeze, poweroff and runtime suspend callback, and similarly for system resume, thaw, restore, and runtime resume, can achieve similar behaviour with the help of the -DEFINE_RUNTIME_DEV_PM_OPS() defined in include/linux/pm_runtime.h (possibly setting its -last argument to NULL). +DEFINE_RUNTIME_DEV_PM_OPS() macro defined in include/linux/pm_runtime.h +(possibly setting its last argument to NULL). 8. "No-Callback" Devices ======================== @@ -505,7 +514,7 @@ registration the length should be controlled by user space, using the In order to use autosuspend, subsystems or drivers must call pm_runtime_use_autosuspend() (preferably before registering the device), and -thereafter they should use the various `*_autosuspend()` helper functions +thereafter they should use the various \*_autosuspend() helper functions instead of the non-autosuspend counterparts:: Instead of: pm_runtime_suspend use: pm_runtime_autosuspend; @@ -515,7 +524,7 @@ instead of the non-autosuspend counterparts:: Drivers may also continue to use the non-autosuspend helper functions; they will behave normally, which means sometimes taking the autosuspend delay into -account (see pm_runtime_idle). The autosuspend variants of the functions also +account (see pm_runtime_idle()). The autosuspend variants of the functions also call pm_runtime_mark_last_busy(). Under some circumstances a driver or subsystem may want to prevent a device @@ -558,7 +567,7 @@ Here is a schematic pseudo-code example:: int foo_runtime_suspend(struct device *dev) { - struct foo_priv foo = container_of(dev, ...); + struct foo_priv *foo = container_of(dev, ...); int ret = 0; lock(&foo->private_lock); @@ -574,7 +583,7 @@ Here is a schematic pseudo-code example:: int foo_runtime_resume(struct device *dev) { - struct foo_priv foo = container_of(dev, ...); + struct foo_priv *foo = container_of(dev, ...); lock(&foo->private_lock); /* ... resume the device ... */ From 86122467eea539ac13d40b6938abd1fbb40fb640 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Tue, 29 Sep 2026 19:49:52 -0700 Subject: [PATCH 56/89] PM: runtime: Add "Section" hyperlinks Add reStructuredText targets for each section, and use them throughout to make the generated HTML more navigable. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260929194956.v3.4.I7666a5802ea59359afce4ff7d0b1af668e5f5292@changeid Signed-off-by: Rafael J. Wysocki --- Documentation/power/runtime_pm.rst | 48 ++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/Documentation/power/runtime_pm.rst b/Documentation/power/runtime_pm.rst index 59b634fd6829..8e93766bdf99 100644 --- a/Documentation/power/runtime_pm.rst +++ b/Documentation/power/runtime_pm.rst @@ -8,6 +8,8 @@ Runtime Power Management Framework for I/O Devices (C) 2014 Intel Corp., Rafael J. Wysocki +.. _Section 1: + 1. Introduction =============== @@ -37,6 +39,8 @@ The runtime PM callbacks present in 'struct dev_pm_ops', the device runtime PM fields of 'struct dev_pm_info' and the core helper functions provided for runtime PM are described below. +.. _Section 2: + 2. Device Runtime PM Callbacks ============================== @@ -80,8 +84,8 @@ the PM core that it is safe to run the ->runtime_suspend(), ->runtime_resume() and ->runtime_idle() callbacks for the given device in atomic context with interrupts disabled. This implies that the callback routines in question must not block or sleep, but it also means that the synchronous helper functions -listed at the end of Section 4 may be used for that device within an interrupt -handler or generally in an atomic context. +listed at the end of `Section 4`_ may be used for that device within an +interrupt handler or generally in an atomic context. The subsystem-level suspend callback, if present, is _entirely_ _responsible_ for handling the suspend of the device as appropriate, which may, but need not @@ -105,9 +109,9 @@ knows what to do to handle the device). * If the suspend callback returns an error code different from -EBUSY and -EAGAIN, the PM core regards this as a fatal error and will refuse to run - the helper functions described in Section 4 for the device until its status - is directly set to either 'active', or 'suspended' (the PM core provides - special helper functions for this purpose). + the helper functions described in `Section 4`_ for the device until its + status is directly set to either 'active', or 'suspended' (the PM core + provides special helper functions for this purpose). In particular, if the driver requires remote wakeup capability (i.e. hardware mechanism allowing the device to request a change of its power state, such as @@ -132,10 +136,10 @@ what to do to handle the device). 'active'. * If the resume callback returns an error code, the PM core regards this as a - fatal error and will refuse to run the helper functions described in Section - 4 for the device, until its status is directly set to either 'active', or - 'suspended' (by means of special helper functions provided by the PM core - for this purpose). + fatal error and will refuse to run the helper functions described in + `Section 4`_ for the device, until its status is directly set to either + 'active', or 'suspended' (by means of special helper functions provided by + the PM core for this purpose). The idle callback (a subsystem-level one, if present, or the driver one) is executed by the PM core whenever the device appears to be idle, which is @@ -158,9 +162,9 @@ call to pm_runtime_autosuspend(). To prevent this (for example, if the callback routine has started a delayed suspend), the routine must return a non-zero value. Negative error return codes are ignored by the PM core. -The helper functions provided by the PM core, described in Section 4, guarantee -that the following constraints are met with respect to runtime PM callbacks for -one device: +The helper functions provided by the PM core, described in `Section 4`_, +guarantee that the following constraints are met with respect to runtime PM +callbacks for one device: (1) The callbacks are mutually exclusive (e.g. it is forbidden to execute ->runtime_suspend() in parallel with ->runtime_resume() or with another @@ -200,6 +204,8 @@ rules: scheduled requests to execute the other callbacks for the same device, except for scheduled autosuspends. +.. _Section 3: + 3. Runtime PM Device Fields =========================== @@ -210,6 +216,8 @@ state. .. kernel-doc:: include/linux/pm.h :identifiers: dev_pm_info +.. _Section 4: + 4. Runtime PM Device Helper Functions ===================================== @@ -259,12 +267,14 @@ functions may also be used in interrupt context: - pm_runtime_put_sync_suspend() - pm_runtime_put_sync_autosuspend() +.. _Section 5: + 5. Runtime PM Initialization, Device Probing and Removal ======================================================== Initially, the runtime PM is disabled for all devices, which means that the -majority of the runtime PM helper functions described in Section 4 will return --EACCES until pm_runtime_enable() is called for the device. +majority of the runtime PM helper functions described in `Section 4`_ will +return -EACCES until pm_runtime_enable() is called for the device. In addition to that, the initial runtime PM status of all devices is 'suspended', but it need not reflect the actual physical state of the device. @@ -287,7 +297,7 @@ pm_runtime_set_suspended(). If the default initial runtime PM status of the device (i.e. 'suspended') reflects the actual state of the device, its bus type's or its driver's ->probe() callback will likely need to wake it up using one of the PM core's -helper functions described in Section 4. In that case, pm_runtime_resume() +helper functions described in `Section 4`_. In that case, pm_runtime_resume() should be used. Of course, for this purpose the device's runtime PM has to be enabled earlier by calling pm_runtime_enable(). @@ -338,6 +348,8 @@ value of /sys/devices/.../power/control to "auto" to allow the driver to power manage the device at run time, the driver may confuse it by using pm_runtime_forbid() this way. +.. _Section 6: + 6. Runtime PM and System Sleep ============================== @@ -430,6 +442,8 @@ out the following operations: callback and right after executing the subsystem-level .complete() callback for it, respectively. +.. _Section 7: + 7. Generic subsystem callbacks ============================== @@ -453,6 +467,8 @@ restore, and runtime resume, can achieve similar behaviour with the help of the DEFINE_RUNTIME_DEV_PM_OPS() macro defined in include/linux/pm_runtime.h (possibly setting its last argument to NULL). +.. _Section 8: + 8. "No-Callback" Devices ======================== @@ -489,6 +505,8 @@ in subsystems/drivers, the PM core allows runtime PM callbacks to be unassigned. More precisely, if a callback pointer is NULL, the PM core will act as though there was a callback and it returned 0. +.. _Section 9: + 9. Autosuspend, or automatically-delayed suspends ================================================= From aedc51762a0d3d4a50d1542b59aafcd6060c4b39 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Tue, 29 Sep 2026 19:49:53 -0700 Subject: [PATCH 57/89] PM: runtime: Clarify ->runtime_idle() callback return value handling This doc says "Negative error return codes are ignored by the PM core" for ->runtime_idle(). This is misleading: any non-zero return value (including negative error codes such as -EBUSY or -EAGAIN) tells the PM core to abort automatic suspension of the device, which is commonly used by subsystems to prevent immediate runtime suspend. Clarify that unlike ->runtime_suspend() and ->runtime_resume(), the PM core does not treat negative return values from ->runtime_idle() as a fatal device error, and that any non-zero value stops the PM core from suspending the device. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260929194956.v3.5.If6e26acac7770c65446dd0b81f891948abb38973@changeid Signed-off-by: Rafael J. Wysocki --- Documentation/power/runtime_pm.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Documentation/power/runtime_pm.rst b/Documentation/power/runtime_pm.rst index 8e93766bdf99..6fe20ad8d3d9 100644 --- a/Documentation/power/runtime_pm.rst +++ b/Documentation/power/runtime_pm.rst @@ -158,9 +158,14 @@ suspending the device are satisfied) and to queue up a suspend request for the device in that case. If there is no idle callback, or if the callback returns 0, then the PM core will attempt to carry out a runtime suspend of the device, also respecting devices configured for autosuspend. In essence this means a -call to pm_runtime_autosuspend(). To prevent this (for example, if the callback -routine has started a delayed suspend), the routine must return a non-zero -value. Negative error return codes are ignored by the PM core. +call to pm_runtime_autosuspend(). + +To prevent this suspension (for example, if the callback routine has scheduled +a delayed suspend or determined the device cannot be idle), the routine must +return a non-zero value (typically -EBUSY or -EAGAIN). Unlike +->runtime_suspend() and ->runtime_resume(), the PM core does not treat negative +return codes from ->runtime_idle() as a fatal device error; any non-zero value +simply stops the PM core from suspending the device. The helper functions provided by the PM core, described in `Section 4`_, guarantee that the following constraints are met with respect to runtime PM From dd2f2b9b1082c1e4283cb5e0aea0bc8ebd966c81 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Tue, 29 Sep 2026 19:49:54 -0700 Subject: [PATCH 58/89] PM: runtime: Clarify driver callback expectations and structure Section 2 Section 2 describes the three runtime callbacks, but: 1) it's fairly dense to read (150+ lines); and 2) it glosses over a big point -- that it's uncommon for drivers to implement ->runtime_idle() Add a note up-front to help direct the reader about #2, and add headings to try to break up the text a bit. Signed-off-by: Brian Norris Link: https://patch.msgid.link/20260929194956.v3.6.If19ee35d3b80d115f264484b21fb003a962762fa@changeid Signed-off-by: Rafael J. Wysocki --- Documentation/power/runtime_pm.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Documentation/power/runtime_pm.rst b/Documentation/power/runtime_pm.rst index 6fe20ad8d3d9..68903d93a3e7 100644 --- a/Documentation/power/runtime_pm.rst +++ b/Documentation/power/runtime_pm.rst @@ -54,6 +54,14 @@ There are three device runtime PM callbacks defined in 'struct dev_pm_ops':: ... }; +Most device drivers only need to implement ->runtime_suspend() and +->runtime_resume(). The ->runtime_idle() callback is optional and rarely +implemented by peripheral device drivers, as the PM core automatically handles +suspension and autosuspend when ->runtime_idle() is omitted (or returns 0). + +Subsystem and Driver Callbacks +------------------------------ + The ->runtime_suspend(), ->runtime_resume() and ->runtime_idle() callbacks are executed by the PM core for the device's subsystem that may be either of the following: @@ -87,6 +95,9 @@ not block or sleep, but it also means that the synchronous helper functions listed at the end of `Section 4`_ may be used for that device within an interrupt handler or generally in an atomic context. +Callback Semantics +------------------ + The subsystem-level suspend callback, if present, is _entirely_ _responsible_ for handling the suspend of the device as appropriate, which may, but need not include executing the device driver's own ->runtime_suspend() callback (from the @@ -167,6 +178,9 @@ return a non-zero value (typically -EBUSY or -EAGAIN). Unlike return codes from ->runtime_idle() as a fatal device error; any non-zero value simply stops the PM core from suspending the device. +Core Guarantees and Synchronization Rules +----------------------------------------- + The helper functions provided by the PM core, described in `Section 4`_, guarantee that the following constraints are met with respect to runtime PM callbacks for one device: From 9df3e4316c2ee861074b631b8a232ae37ce093ab Mon Sep 17 00:00:00 2001 From: "Mario Limonciello (AMD)" Date: Sat, 26 Sep 2026 17:55:51 -0500 Subject: [PATCH 59/89] PM: sleep: Add resume event mapping for PMSG_POWEROFF PM_EVENT_POWEROFF (0x0800) and PMSG_POWEROFF were already defined in include/linux/pm.h, and pm_op(), pm_late_early_op() and pm_noirq_op() already dispatch PM_EVENT_POWEROFF to the same ops->poweroff*() callbacks as PM_EVENT_HIBERNATE. Add the matching entry to resume_event() so that when a poweroff suspend sequence is aborted (for example a device's ->poweroff_noirq() callback fails and the PM core rolls the already-suspended devices back), those devices are resumed with PMSG_RESTORE and their ->restore*() callbacks run. Map it to PMSG_RESTORE, mirroring the existing PM_EVENT_HIBERNATE handling. This is correct because both events suspend via ops->poweroff*(), whose counterpart is ops->restore*(). Mapping to PMSG_ON would be a no-op: it is what the default return already yields, and pm_op()/pm_late_early_op()/pm_noirq_op() have no PM_EVENT_ON case, so they would return NULL and silently skip every resume callback on the abort path. This completes the PMSG_POWEROFF infrastructure needed to support differentiating between hibernation and shutdown sequences when re-using callbacks for common code. Hibernation is started by writing a hibernation method (such as 'platform' 'shutdown', or 'reboot') to use into /sys/power/disk and writing 'disk' to /sys/power/state. Shutdown is initiated with the reboot() syscall with arguments on whether to halt the system or power it off. Tested-by: Eric Naim Signed-off-by: Mario Limonciello (AMD) [ rjw: Subject tweak ] Link: https://patch.msgid.link/20260926225555.2916525-1-mario.limonciello@amd.com Signed-off-by: Rafael J. Wysocki --- drivers/base/power/main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c index 4e0d6357a37c..88a31e09e4f3 100644 --- a/drivers/base/power/main.c +++ b/drivers/base/power/main.c @@ -1482,6 +1482,7 @@ static pm_message_t resume_event(pm_message_t sleep_state) case PM_EVENT_QUIESCE: return PMSG_RECOVER; case PM_EVENT_HIBERNATE: + case PM_EVENT_POWEROFF: return PMSG_RESTORE; } return PMSG_ON; From 29671cabfc09afb1ee2426b21173cdde276a95ac Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Sat, 26 Sep 2026 13:21:17 +0800 Subject: [PATCH 60/89] x86/hibernate: Ignore page-zero RAM in E820 checksum The legacy kexec_load path reconstructs the E820 map exported through sysfs. kexec-tools leaves the first 1 KiB unavailable for the real-mode transition, so a kernel entered through kexec_load can see conventional RAM starting at 0x400. A subsequent firmware boot reports the same RAM range starting at zero. The hibernation E820 checksum compares those byte representations and rejects the image, even though the maps agree from page one onwards. A QEMU guest showed this transition: firmware: RAM [0-0x9fbff] kexec_load: gap [0-0x3ff], RAM [0x400-0x9fbff] firmware: RAM [0-0x9fbff] The checksum covers e820_table_firmware, which trim_bios_range() leaves alone. The nosave regions come from e820_table instead, where trim_bios_range() has already turned conventional RAM in page zero into reserved memory, so page zero never occurs in the image, whatever the firmware map says about it. Canonicalise only the conventional-RAM portion below PAGE_SIZE before calculating the checksum. Preserve RESERVED, ACPI, NVS, UNUSABLE, PMEM and all other E820 types so that changes to exceptional mappings remain detectable. This is deliberately narrower than Marco Scardovi's June proposal to checksum only RAM and its opt-in relaxed_memmap successor. Rafael noted that ignoring non-RAM changes could hide moved ACPI or UEFI regions still used by the resumed kernel. This patch preserves every non-RAM entry and ignores only RAM within page zero, which common setup already reserves and excludes from the image. Bump RESTORE_MAGIC, since the checksum semantics change, so that an image written by a kernel without this change is refused by the magic check instead of being compared under different rules. In the same guest, with the page-zero difference kept, hibernation and resume now work after both a direct boot and a kexec_load boot. An image written by an unpatched kernel is refused at resume with "Unrecognized hibernate image header format!", and the firmware-booted kernel carries on. Fixes: 62a03defeabd ("PM / hibernate: Verify the consistent of e820 memory map by md5 digest") Reported-by: Roberto Ricci Signed-off-by: Matthias Goergens Link: https://lore.kernel.org/all/Z-hYWc9LtBU1Yhtg@desktop0a/ Link: https://lists.openwall.net/linux-kernel/2025/04/04/1372 Link: https://lore.kernel.org/all/CAJZ5v0jmOj0WBtMTvbnaD+2b0bTFowA=JWrqRzaaCYpHpai1Nw@mail.gmail.com/ Link: https://lore.kernel.org/all/20260623165724.10753-1-scardracs@disroot.org/ Link: https://patch.msgid.link/20260926052117.3613637-1-matthias.goergens@gmail.com Signed-off-by: Rafael J. Wysocki --- arch/x86/power/hibernate.c | 51 +++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/arch/x86/power/hibernate.c b/arch/x86/power/hibernate.c index a2294c1649f6..dbb340fb24ad 100644 --- a/arch/x86/power/hibernate.c +++ b/arch/x86/power/hibernate.c @@ -63,6 +63,25 @@ struct restore_data_record { unsigned long e820_checksum; }; +static bool trim_e820_page_zero_ram(struct e820_entry *entry) +{ + u64 lowmem_size; + + /* + * Page zero is BIOS-owned and registered as nosave. Boot loaders may + * therefore omit part of its conventional RAM entry without changing + * any memory available to the image. Preserve all other E820 types. + */ + if (entry->type != E820_TYPE_RAM || entry->addr >= PAGE_SIZE) + return true; + + lowmem_size = min_t(u64, entry->size, PAGE_SIZE - entry->addr); + entry->addr += lowmem_size; + entry->size -= lowmem_size; + + return entry->size; +} + /** * compute_e820_crc32 - calculate crc32 of a given e820 table * @@ -70,18 +89,38 @@ struct restore_data_record { * * Return: the resulting checksum */ -static inline u32 compute_e820_crc32(struct e820_table *table) +static u32 compute_e820_crc32(struct e820_table *table) { - int size = offsetof(struct e820_table, entries) + - sizeof(struct e820_entry) * table->nr_entries; + struct e820_entry entry; + u32 crc = ~0; + u32 nr_entries = 0; + u32 i; - return ~crc32_le(~0, (unsigned char const *)table, size); + for (i = 0; i < table->nr_entries; i++) { + entry = table->entries[i]; + if (trim_e820_page_zero_ram(&entry)) + nr_entries++; + } + + crc = crc32_le(crc, (unsigned char const *)&nr_entries, + sizeof(nr_entries)); + + for (i = 0; i < table->nr_entries; i++) { + entry = table->entries[i]; + if (!trim_e820_page_zero_ram(&entry)) + continue; + + crc = crc32_le(crc, (unsigned char const *)&entry, + sizeof(entry)); + } + + return ~crc; } #ifdef CONFIG_X86_64 -#define RESTORE_MAGIC 0x23456789ABCDEF02UL +#define RESTORE_MAGIC 0x23456789ABCDEF03UL #else -#define RESTORE_MAGIC 0x12345679UL +#define RESTORE_MAGIC 0x1234567AUL #endif /** From 706081c6b9327dd611dd72b20130a77f0f059b6e Mon Sep 17 00:00:00 2001 From: Karl Mehltretter Date: Sat, 12 Sep 2026 00:14:43 +0200 Subject: [PATCH 61/89] OPP: Fix the return value in dev_pm_opp_get_of_node() kernel-doc dev_pm_opp_get_of_node() returns NULL when the OPP is invalid, but its kernel-doc says "else 0 on success", which was never true. Say NULL. Fixes: e2f4b5f8dc59 ("PM / OPP: Implement dev_pm_opp_get_of_node()") Assisted-by: LLM Signed-off-by: Karl Mehltretter Signed-off-by: Viresh Kumar --- drivers/opp/of.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/opp/of.c b/drivers/opp/of.c index 2f3bbde9a9e1..ea9380079578 100644 --- a/drivers/opp/of.c +++ b/drivers/opp/of.c @@ -1409,7 +1409,7 @@ bool dev_pm_opp_of_has_required_opp(struct device *dev) * dev_pm_opp_get_of_node() - Gets the DT node corresponding to an opp * @opp: opp for which DT node has to be returned for * - * Return: DT node corresponding to the opp, else 0 on success. + * Return: DT node corresponding to the opp, else NULL. * * The caller needs to put the node with of_node_put() after using it. */ From b667a680917119827916bf2eb7d6f8de9c47531f Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Mon, 21 Sep 2026 21:23:47 -0500 Subject: [PATCH 62/89] cpufreq/amd-pstate: Fix TOCTOU when changing driver mode via sysfs amd_pstate_update_status() evaluates mode_state_machine[cppc_state] [mode_idx] before taking amd_pstate_driver_lock, and then re-evaluates it again once the lock is held. cppc_state is global and only stable under the lock, so concurrent sysfs writes race: CPU 0 CPU 1 ----- ----- // cppc_state == A, mode == B if (mode_state_machine[A][B]) // sees non-NULL if (mode_state_machine[A][B]) guard(&amd_pstate_driver_lock); mode_state_machine[A][B](B); cppc_state = B; guard(&amd_pstate_driver_lock); // cppc_state is now B mode_state_machine[B][B](B); // NULL -> NULL deref The re-read under the lock can resolve to a self-transition (NULL pointer, immediate NULL deref) or to an unexpected transition that redundantly runs amd_pstate_driver_cleanup(), double-freeing current_pstate_driver->attr. Take the lock first, then read cppc_state and resolve the transition function exactly once into a local before calling it, so the check and the call observe the same state. Reported-by: Sashiko Closes: https://sashiko.dev/#/bug/linux-f86ff3a6-55df-4eef-8e4d-62ac270dba25 Fixes: 6f0b13f16f7a ("cpufreq/amd-pstate: Overhaul locking") Signed-off-by: Mario Limonciello Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Link: https://lore.kernel.org/r/20260922022347.1270746-1-mario.limonciello@amd.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index c93a4b765dff..d054720abee3 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -1886,6 +1886,7 @@ EXPORT_SYMBOL_FOR_PSTATE_UT(amd_pstate_get_status); int amd_pstate_update_status(const char *buf, size_t size) { + cppc_mode_transition_fn fn; int mode_idx; if (size > strlen("passive") || size < strlen("active")) @@ -1895,10 +1896,12 @@ int amd_pstate_update_status(const char *buf, size_t size) if (mode_idx < 0) return mode_idx; - if (mode_state_machine[cppc_state][mode_idx]) { - guard(mutex)(&amd_pstate_driver_lock); - return mode_state_machine[cppc_state][mode_idx](mode_idx); - } + guard(mutex)(&amd_pstate_driver_lock); + + /* Resolve the transition under the lock; cppc_state is only stable while held. */ + fn = mode_state_machine[cppc_state][mode_idx]; + if (fn) + return fn(mode_idx); return 0; } From 52763c00945ab30241d9c097547600b9e234e559 Mon Sep 17 00:00:00 2001 From: Vishal Badole Date: Tue, 29 Sep 2026 02:05:23 +0530 Subject: [PATCH 63/89] cpufreq: amd-pstate: Update Zen6 client EPP tuning values The per-CPU-type EPP values for the Zen6 client platform are further tuned based on platform characterization. Adjust the performance and low-power core entries so each core type uses its intended EPP settings. Reviewed-by: Mario Limonciello Signed-off-by: Vishal Badole Link: https://lore.kernel.org/r/20260928203523.1290623-1-Vishal.Badole@amd.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index d054720abee3..8d5886f1e905 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -211,7 +211,7 @@ static const struct amd_pstate_epp_soc epp_soc_zen6_client __initconst = { .performance = 25, .balance_performance = 51, .balance_power = 64, - .power = 64, + .power = 115, }, .efficiency_core = { .performance = 25, @@ -221,9 +221,9 @@ static const struct amd_pstate_epp_soc epp_soc_zen6_client __initconst = { }, .low_power_core = { .performance = 25, - .balance_performance = 51, + .balance_performance = 64, .balance_power = 64, - .power = 115, + .power = 64, }, }; From 4817d3e2b90dac50eb11cee32a0cf92d294261ea Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Thu, 1 Oct 2026 09:23:39 -0500 Subject: [PATCH 64/89] ACPI: CPPC: Refactor boost ratio handling Rename amd_get_boost_ratio_numerator() to amd_get_effective_highest_perf() to better reflect that it returns the effective highest performance value for a CPU, accounting for preferred cores and heterogeneous topologies. Add a new helper amd_get_boost_ratio() for doing both for boost ratio calculations and update callers to use it. This refactoring is needed because on some AMD systems (Zen6+), the boost ratio should be calculated from frequency values rather than performance values. Newer versions of the ACPI will introduce a proper interface for getting this information, but for now hardcode values so that these systems perform as intended and to unblock future scheduler work while we continue to wait. For now use some temporary hardcoded values for Zen6 platforms. Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Acked-by: Rafael J. Wysocki (Intel) Signed-off-by: Mario Limonciello Link: https://lore.kernel.org/r/20261001142341.2934582-2-mario.limonciello@amd.com Signed-off-by: Mario Limonciello --- arch/x86/kernel/acpi/cppc.c | 166 ++++++++++++++++++++++----------- drivers/cpufreq/acpi-cpufreq.c | 5 +- drivers/cpufreq/amd-pstate.c | 14 ++- include/acpi/cppc_acpi.h | 14 ++- 4 files changed, 134 insertions(+), 65 deletions(-) diff --git a/arch/x86/kernel/acpi/cppc.c b/arch/x86/kernel/acpi/cppc.c index bbade0da5130..f230e9323187 100644 --- a/arch/x86/kernel/acpi/cppc.c +++ b/arch/x86/kernel/acpi/cppc.c @@ -10,6 +10,7 @@ #include #include #include +#include #define CPPC_HIGHEST_PERF_PERFORMANCE 196 #define CPPC_HIGHEST_PERF_PREFCORE 166 @@ -81,31 +82,18 @@ int cpc_write_ffh(int cpunum, struct cpc_reg *reg, u64 val) static void amd_set_max_freq_ratio(void) { - struct cppc_perf_caps perf_caps; - u64 numerator, nominal_perf; + u64 numerator, denominator; u64 perf_ratio; int rc; - rc = cppc_get_perf_caps(0, &perf_caps); + rc = amd_get_boost_ratio(0, &numerator, &denominator); if (rc) { - pr_debug("Could not retrieve perf counters (%d)\n", rc); - return; - } - - rc = amd_get_boost_ratio_numerator(0, &numerator); - if (rc) { - pr_debug("Could not retrieve highest performance (%d)\n", rc); - return; - } - nominal_perf = perf_caps.nominal_perf; - - if (!nominal_perf) { - pr_debug("Could not retrieve nominal performance\n"); + pr_debug("Could not retrieve boost ratio (%d)\n", rc); return; } /* midpoint between max_boost and max_P */ - perf_ratio = (div_u64(numerator * SCHED_CAPACITY_SCALE, nominal_perf) + SCHED_CAPACITY_SCALE) >> 1; + perf_ratio = (div_u64(numerator * SCHED_CAPACITY_SCALE, denominator) + SCHED_CAPACITY_SCALE) >> 1; freq_invariance_set_perf_ratio(perf_ratio, false); } @@ -225,35 +213,30 @@ int amd_detect_prefcore(bool *detected) EXPORT_SYMBOL_GPL(amd_detect_prefcore); /** - * amd_get_boost_ratio_numerator: Get the numerator to use for boost ratio calculation - * @cpu: CPU to get numerator for. - * @numerator: Output variable for numerator. + * amd_get_effective_highest_perf: Get the effective highest performance value + * @cpu: CPU to get highest performance for. * - * Determine the numerator to use for calculating the boost ratio on - * a CPU. On systems that support preferred cores, this will be a hardcoded - * value. On other systems this will the highest performance register value. + * Get the effective highest performance value for a CPU, accounting for + * preferred cores and heterogeneous topologies. On systems with preferred + * cores, this may be a hardcoded value. On heterogeneous systems, this + * may be a per-CPU value. On other systems, this is the shared highest + * performance value. * - * If booting the system with amd-pstate enabled but preferred cores disabled then - * the correct boost numerator will be returned to match hardware capabilities - * even if the preferred cores scheduling hints are not enabled. - * - * Return: 0 for success, negative error code otherwise. + * Return: Effective highest performance value, or negative error code. */ -int amd_get_boost_ratio_numerator(unsigned int cpu, u64 *numerator) +int amd_get_effective_highest_perf(unsigned int cpu) { bool prefcore; int ret; u32 tmp; ret = amd_detect_prefcore(&prefcore); - if (ret) + if (ret < 0) return ret; /* without preferred cores, return the highest perf register value */ - if (!prefcore) { - *numerator = boost_numerator; - return 0; - } + if (!prefcore) + return boost_numerator; /* * For AMD CPUs with Family ID 19H and Model ID range 0x70 to 0x7f, @@ -263,8 +246,7 @@ int amd_get_boost_ratio_numerator(unsigned int cpu, u64 *numerator) if (cpu_feature_enabled(X86_FEATURE_ZEN4)) { switch (boot_cpu_data.x86_model) { case 0x70 ... 0x7f: - *numerator = CPPC_HIGHEST_PERF_PERFORMANCE; - return 0; + return CPPC_HIGHEST_PERF_PERFORMANCE; default: break; } @@ -272,28 +254,100 @@ int amd_get_boost_ratio_numerator(unsigned int cpu, u64 *numerator) /* detect if running on heterogeneous design */ if (cpu_feature_enabled(X86_FEATURE_AMD_HTR_CORES)) { - switch (cpu_data(cpu).topo.cpu_type) { - case TOPO_CPU_TYPE_UNKNOWN: - case TOPO_CPU_TYPE_ANY: - pr_warn("Undefined core type found for cpu %d\n", cpu); - break; - case TOPO_CPU_TYPE_PERFORMANCE: - /* use the max scale for performance cores */ - *numerator = CPPC_HIGHEST_PERF_PERFORMANCE; - return 0; - case TOPO_CPU_TYPE_LOW_POWER: - case TOPO_CPU_TYPE_EFFICIENCY: - /* use the highest perf value for efficiency and low-power cores */ - ret = amd_get_highest_perf(cpu, &tmp); - if (ret) - return ret; - *numerator = tmp; - return 0; - } + if (cpu_feature_enabled(X86_FEATURE_ZEN5) && + cpu_data(cpu).topo.cpu_type == TOPO_CPU_TYPE_PERFORMANCE) + return CPPC_HIGHEST_PERF_PERFORMANCE; + + /* Zen 5 efficiency, and Zen 6+ */ + ret = amd_get_highest_perf(cpu, &tmp); + if (ret < 0) + return ret; + + return tmp; } - *numerator = CPPC_HIGHEST_PERF_PREFCORE; + return CPPC_HIGHEST_PERF_PREFCORE; +} +EXPORT_SYMBOL_GPL(amd_get_effective_highest_perf); + +struct amd_max_freq { + u32 performance; + u32 efficiency; + u32 low_power; +}; + +static const struct amd_max_freq max_freq_zen6_client = { + .performance = 5025, + .efficiency = 3524, + .low_power = 2399, +}; + +int amd_get_max_frequency(unsigned int cpu) +{ + enum x86_topology_cpu_type type = cpu_data(cpu).topo.cpu_type; + const struct amd_max_freq *max_freq = NULL; + + if (cpu_feature_enabled(X86_FEATURE_ZEN6) && + cpu_feature_enabled(X86_FEATURE_AMD_HTR_CORES)) + max_freq = &max_freq_zen6_client; + + if (!max_freq) + return 0; + + switch (type) { + case TOPO_CPU_TYPE_PERFORMANCE: + return max_freq->performance; + case TOPO_CPU_TYPE_EFFICIENCY: + return max_freq->efficiency; + case TOPO_CPU_TYPE_LOW_POWER: + return max_freq->low_power; + default: + break; + } return 0; } -EXPORT_SYMBOL_GPL(amd_get_boost_ratio_numerator); +EXPORT_SYMBOL_GPL(amd_get_max_frequency); + +/** + * amd_get_boost_ratio: Get numerator and denominator for boost ratio + * @cpu: CPU to get the boost ratio for. + * @numerator: Output variable for numerator. + * @denominator: Output variable for denominator. + * + * Get the numerator and denominator for calculating the boost ratio. + * + * Return: 0 for success, negative error code otherwise. + */ +int amd_get_boost_ratio(unsigned int cpu, u64 *numerator, u64 *denominator) +{ + struct cppc_perf_caps perf_caps; + int max_frequency; + int ret; + + ret = cppc_get_perf_caps(cpu, &perf_caps); + if (ret) + return ret; + + max_frequency = amd_get_max_frequency(cpu); + /* Use frequency values if available */ + if (max_frequency && perf_caps.nominal_freq) { + *numerator = max_frequency; + *denominator = perf_caps.nominal_freq; + return 0; + } + + /* Fall back to performance values */ + ret = amd_get_effective_highest_perf(cpu); + if (ret < 0) + return ret; + + *numerator = ret; + + *denominator = perf_caps.nominal_perf; + if (!*denominator) + return -EINVAL; + + return 0; +} +EXPORT_SYMBOL_GPL(amd_get_boost_ratio); diff --git a/drivers/cpufreq/acpi-cpufreq.c b/drivers/cpufreq/acpi-cpufreq.c index 10ea6035f4ad..523ee1a06ef8 100644 --- a/drivers/cpufreq/acpi-cpufreq.c +++ b/drivers/cpufreq/acpi-cpufreq.c @@ -640,12 +640,13 @@ static u64 get_max_boost_ratio(unsigned int cpu, u64 *nominal_freq) } if (boot_cpu_data.x86_vendor == X86_VENDOR_AMD) { - ret = amd_get_boost_ratio_numerator(cpu, &highest_perf); - if (ret) { + ret = amd_get_effective_highest_perf(cpu); + if (ret < 0) { pr_debug("CPU%d: Unable to get boost ratio numerator (%d)\n", cpu, ret); return 0; } + highest_perf = ret; } else { highest_perf = perf_caps.highest_perf; } diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 8d5886f1e905..83ab0385a3ea 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -555,11 +555,13 @@ static int msr_init_perf(struct amd_cpudata *cpudata) if (ret) return ret; - ret = amd_get_boost_ratio_numerator(cpudata->cpu, &numerator); - if (ret) + ret = amd_get_effective_highest_perf(cpudata->cpu); + if (ret < 0) return ret; - ret = rdmsrq_on_cpu(cpudata->cpu, MSR_AMD_CPPC_REQ, &cppc_req); + numerator = ret; + + ret = rdmsrq_safe_on_cpu(cpudata->cpu, MSR_AMD_CPPC_REQ, &cppc_req); if (ret) return ret; @@ -590,10 +592,12 @@ static int shmem_init_perf(struct amd_cpudata *cpudata) if (ret) return ret; - ret = amd_get_boost_ratio_numerator(cpudata->cpu, &numerator); - if (ret) + ret = amd_get_effective_highest_perf(cpudata->cpu); + if (ret < 0) return ret; + numerator = ret; + perf.highest_perf = numerator; perf.max_limit_perf = numerator; perf.min_limit_perf = cppc_perf.lowest_perf; diff --git a/include/acpi/cppc_acpi.h b/include/acpi/cppc_acpi.h index 94a6277edab2..4c0b11a718e3 100644 --- a/include/acpi/cppc_acpi.h +++ b/include/acpi/cppc_acpi.h @@ -192,7 +192,9 @@ extern int cppc_set_auto_sel(int cpu, bool enable); extern int cppc_get_perf_limited(int cpu, u64 *perf_limited); extern int cppc_set_perf_limited(int cpu, u64 bits_to_clear); extern int amd_get_highest_perf(unsigned int cpu, u32 *highest_perf); -extern int amd_get_boost_ratio_numerator(unsigned int cpu, u64 *numerator); +extern int amd_get_effective_highest_perf(unsigned int cpu); +extern int amd_get_max_frequency(unsigned int cpu); +extern int amd_get_boost_ratio(unsigned int cpu, u64 *numerator, u64 *denominator); extern int amd_detect_prefcore(bool *detected); #else /* !CONFIG_ACPI_CPPC_LIB */ static inline int cppc_get_desired_perf(int cpunum, u64 *desired_perf) @@ -305,7 +307,15 @@ static inline int amd_get_highest_perf(unsigned int cpu, u32 *highest_perf) { return -ENODEV; } -static inline int amd_get_boost_ratio_numerator(unsigned int cpu, u64 *numerator) +static inline int amd_get_effective_highest_perf(unsigned int cpu) +{ + return -EOPNOTSUPP; +} +static inline int amd_get_max_frequency(unsigned int cpu) +{ + return 0; +} +static inline int amd_get_boost_ratio(unsigned int cpu, u64 *numerator, u64 *denominator) { return -EOPNOTSUPP; } From f9da951d309b24aea5052b76ab9438d7854a17ae Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Thu, 1 Oct 2026 09:23:40 -0500 Subject: [PATCH 65/89] cpufreq/acpi-cpufreq: Use amd_get_boost_ratio() Update get_max_boost_ratio() to use the new amd_get_boost_ratio() helper function instead of calling amd_get_effective_highest_perf() directly. This ensures the boost ratio is calculated correctly on systems where frequency values should be used instead of performance values. Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Acked-by: Rafael J. Wysocki (Intel) Signed-off-by: Mario Limonciello Link: https://lore.kernel.org/r/20261001142341.2934582-3-mario.limonciello@amd.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/acpi-cpufreq.c | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/drivers/cpufreq/acpi-cpufreq.c b/drivers/cpufreq/acpi-cpufreq.c index 523ee1a06ef8..2e74a000cdc7 100644 --- a/drivers/cpufreq/acpi-cpufreq.c +++ b/drivers/cpufreq/acpi-cpufreq.c @@ -626,7 +626,7 @@ static int acpi_cpufreq_blacklist(struct cpuinfo_x86 *c) static u64 get_max_boost_ratio(unsigned int cpu, u64 *nominal_freq) { struct cppc_perf_caps perf_caps; - u64 highest_perf, nominal_perf; + u64 numerator, denominator; int ret; if (acpi_pstate_strict) @@ -640,33 +640,31 @@ static u64 get_max_boost_ratio(unsigned int cpu, u64 *nominal_freq) } if (boot_cpu_data.x86_vendor == X86_VENDOR_AMD) { - ret = amd_get_effective_highest_perf(cpu); - if (ret < 0) { - pr_debug("CPU%d: Unable to get boost ratio numerator (%d)\n", + ret = amd_get_boost_ratio(cpu, &numerator, &denominator); + if (ret) { + pr_debug("CPU%d: Unable to get boost ratio (%d)\n", cpu, ret); return 0; } - highest_perf = ret; } else { - highest_perf = perf_caps.highest_perf; + numerator = perf_caps.highest_perf; + denominator = perf_caps.nominal_perf; } - nominal_perf = perf_caps.nominal_perf; - if (nominal_freq) *nominal_freq = perf_caps.nominal_freq * 1000; - if (!highest_perf || !nominal_perf) { + if (!numerator || !denominator) { pr_debug("CPU%d: highest or nominal performance missing\n", cpu); return 0; } - if (highest_perf < nominal_perf) { + if (numerator < denominator) { pr_debug("CPU%d: nominal performance above highest\n", cpu); return 0; } - return div_u64(highest_perf << SCHED_CAPACITY_SHIFT, nominal_perf); + return div_u64(numerator << SCHED_CAPACITY_SHIFT, denominator); } #else From b3f9051ae263940af4a69212c88fd094401cf92d Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Thu, 1 Oct 2026 09:23:41 -0500 Subject: [PATCH 66/89] cpufreq/amd-pstate: Get Highest Freq for a CPU If the highest frequency is known for a CPU, use this instead of trying to calculate by linear interpolation. Reviewed-by: K Prateek Nayak Tested-by: K Prateek Nayak Signed-off-by: Mario Limonciello Link: https://lore.kernel.org/r/20261001142341.2934582-4-mario.limonciello@amd.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 83ab0385a3ea..743d90ed6af4 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -778,7 +778,10 @@ static void amd_pstate_update_min_max_limit(struct cpufreq_policy *policy) struct amd_cpudata *cpudata = policy->driver_data; union perf_cached perf = READ_ONCE(cpudata->perf); - perf.max_limit_perf = freq_to_perf(perf, cpudata->nominal_freq, policy->max); + if (policy->max >= READ_ONCE(cpudata->max_freq)) + perf.max_limit_perf = perf.highest_perf; + else + perf.max_limit_perf = freq_to_perf(perf, cpudata->nominal_freq, policy->max); WRITE_ONCE(cpudata->max_limit_freq, policy->max); if (cpudata->policy == CPUFREQ_POLICY_PERFORMANCE) { @@ -1104,8 +1107,10 @@ static int amd_pstate_init_freq(struct amd_cpudata *cpudata) WRITE_ONCE(cpudata->nominal_freq, nominal_freq); - /* max_freq is calculated according to (nominal_freq * highest_perf)/nominal_perf */ - max_freq = perf_to_freq(perf, nominal_freq, perf.highest_perf); + /* try to look up from BIOS/quirk first, fall back to (nominal_freq * highest_perf)/nominal_perf */ + max_freq = amd_get_max_frequency(cpudata->cpu) * 1000; + if (!max_freq) + max_freq = perf_to_freq(perf, nominal_freq, perf.highest_perf); WRITE_ONCE(cpudata->max_freq, max_freq); lowest_nonlinear_freq = perf_to_freq(perf, nominal_freq, perf.lowest_nonlinear_perf); From f6d3cbc63935fa8911da87e91ed768824d495470 Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Mon, 21 Sep 2026 14:02:32 -0500 Subject: [PATCH 67/89] cpufreq: amd-pstate: Restore previous mode when changing driver mode fails amd_pstate_change_driver_mode() unregisters the currently active driver before registering the requested mode. If amd_pstate_register_driver() fails for the new mode, the function returns the error without restoring anything, leaving the system with no cpufreq scaling driver at all until a valid mode is manually re-selected. Remember the mode that was active before the transition and, if registering the requested mode fails, register the previous mode again so the system keeps a working scaling driver. The original error is still returned to the caller so the sysfs write reports the failure. Reported-by: Sashiko Closes: https://sashiko.dev/#/bug/linux-5b138ddb-c88b-4f82-8a2d-f39446b625d4 Fixes: 3ca7bc818d8c ("cpufreq: amd-pstate: Add guided mode control support via sysfs") Signed-off-by: Mario Limonciello Link: https://lore.kernel.org/r/20260921190235.3651688-1-mario.limonciello@amd.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index 743d90ed6af4..a1d58d1f5f3a 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -1837,6 +1837,7 @@ static int amd_pstate_change_mode_without_dvr_change(int mode) static int amd_pstate_change_driver_mode(int mode) { + int old_mode = cppc_state; int ret; lockdep_assert_held(&amd_pstate_driver_lock); @@ -1846,10 +1847,16 @@ static int amd_pstate_change_driver_mode(int mode) return ret; ret = amd_pstate_register_driver(mode); - if (ret) - return ret; + if (ret) { + pr_err("Failed to register %s mode, restoring %s mode\n", + amd_pstate_get_mode_string(mode), + amd_pstate_get_mode_string(old_mode)); + if (amd_pstate_register_driver(old_mode)) + pr_err("Failed to restore %s mode\n", + amd_pstate_get_mode_string(old_mode)); + } - return 0; + return ret; } static cppc_mode_transition_fn mode_state_machine[AMD_PSTATE_MAX][AMD_PSTATE_MAX] = { From 8de705037124dd1cd49bd8cae6536d63ec4e1807 Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Mon, 21 Sep 2026 14:02:33 -0500 Subject: [PATCH 68/89] cpufreq: amd-pstate: Propagate cppc_set_auto_sel() errors on mode change amd_pstate_change_mode_without_dvr_change() iterates over the online CPUs and calls cppc_set_auto_sel() to enable or disable hardware autonomous selection, but discards its return value and unconditionally returns 0. If the firmware rejects the request, the failure is hidden: the cpufreq core records the mode transition as successful while the hardware stays in its previous autonomous-selection state. The software mode and the actual hardware behaviour then disagree, breaking the expected frequency scaling. Check the return value of cppc_set_auto_sel() and propagate the first error to the caller so the sysfs write reports the failure. Reported-by: Sashiko Closes: https://sashiko.dev/#/bug/linux-d114a94a-655b-43a6-91b1-9484889726c9 Fixes: 3ca7bc818d8c ("cpufreq: amd-pstate: Add guided mode control support via sysfs") Signed-off-by: Mario Limonciello Link: https://lore.kernel.org/r/20260921190235.3651688-2-mario.limonciello@amd.com Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index a1d58d1f5f3a..d8e2eec6e725 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -1822,6 +1822,7 @@ static int amd_pstate_unregister_driver(int dummy) static int amd_pstate_change_mode_without_dvr_change(int mode) { int cpu = 0; + int ret; cppc_state = mode; @@ -1829,7 +1830,9 @@ static int amd_pstate_change_mode_without_dvr_change(int mode) return 0; for_each_online_cpu(cpu) { - cppc_set_auto_sel(cpu, (cppc_state == AMD_PSTATE_PASSIVE) ? 0 : 1); + ret = cppc_set_auto_sel(cpu, (cppc_state == AMD_PSTATE_PASSIVE) ? 0 : 1); + if (ret) + return ret; } return 0; From 6b64d65c83688db2e58fbd9de7c74090f15271b7 Mon Sep 17 00:00:00 2001 From: Mario Limonciello Date: Mon, 21 Sep 2026 14:02:35 -0500 Subject: [PATCH 69/89] cpufreq: amd-pstate-ut: Fix max_freq and EPP test failures on Zen6 Zen6 platforms expose conditions that cause two amd-pstate unit tests to fail: low-power cores can report max_freq below the shared nominal_freq, and multiple EPP preferences can alias to the same raw value. Relax the max_freq >= nominal_freq assertion to max_freq >= lowest_nonlinear_freq, and tolerate aliased EPP preferences by validating the raw programmed value instead of string comparison. Signed-off-by: K Prateek Nayak Tested-by: Mario Limonciello Signed-off-by: Mario Limonciello --- drivers/cpufreq/amd-pstate-ut.c | 54 ++++++++++++++++++++++++--------- drivers/cpufreq/amd-pstate.c | 44 ++------------------------- drivers/cpufreq/amd-pstate.h | 44 +++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 57 deletions(-) diff --git a/drivers/cpufreq/amd-pstate-ut.c b/drivers/cpufreq/amd-pstate-ut.c index c2c1a166b3a9..506d7bb2e660 100644 --- a/drivers/cpufreq/amd-pstate-ut.c +++ b/drivers/cpufreq/amd-pstate-ut.c @@ -216,7 +216,14 @@ static int amd_pstate_ut_check_perf(u32 index) /* * Check if frequency values are reasonable. - * max_freq >= nominal_freq > lowest_nonlinear_freq > min_freq > 0 + * nominal_freq > lowest_nonlinear_freq >= min_freq > 0 + * max_freq >= lowest_nonlinear_freq + * + * On most parts the boost frequency is the highest, i.e. + * max_freq >= nominal_freq. On heterogeneous designs (e.g. Zen6) a + * low-power core can have a maximum frequency below the shared nominal + * reference frequency, so only require the boost frequency to sit above + * the lowest nonlinear frequency here. * check max freq when set support boost mode. */ static int amd_pstate_ut_check_freq(u32 index) @@ -235,11 +242,11 @@ static int amd_pstate_ut_check_freq(u32 index) cpudata = policy->driver_data; perf = READ_ONCE(cpudata->perf); - if (!((policy->cpuinfo.max_freq >= cpudata->nominal_freq) && + if (!((policy->cpuinfo.max_freq >= cpudata->lowest_nonlinear_freq) && (cpudata->nominal_freq > cpudata->lowest_nonlinear_freq) && (cpudata->lowest_nonlinear_freq >= policy->cpuinfo.min_freq) && (policy->cpuinfo.min_freq > 0))) { - pr_err("%s cpu%d max=%d >= nominal=%d > lowest_nonlinear=%d > min=%d > 0, the formula is incorrect!\n", + pr_err("%s cpu%d max=%d, nominal=%d, lowest_nonlinear=%d, min=%d, the formula is incorrect!\n", __func__, cpu, policy->cpuinfo.max_freq, cpudata->nominal_freq, cpudata->lowest_nonlinear_freq, policy->cpuinfo.min_freq); return -EINVAL; @@ -297,12 +304,12 @@ static int amd_pstate_set_mode(enum amd_pstate_mode mode) static int amd_pstate_ut_epp(u32 index) { - static const char * const epp_strings[] = { - "dynamic", - "power", - "balance_power", - "balance_performance", - "performance", + enum energy_perf_value_index epp_modes[] = { + EPP_INDEX_DYNAMIC, + EPP_INDEX_POWERSAVE, + EPP_INDEX_BALANCE_POWERSAVE, + EPP_INDEX_BALANCE_PERFORMANCE, + EPP_INDEX_PERFORMANCE, }; char *buf __free(cleanup_page) = NULL; struct cpufreq_policy *policy = NULL; @@ -371,9 +378,9 @@ static int amd_pstate_ut_epp(u32 index) } } - for (i = 0; i < ARRAY_SIZE(epp_strings); i++) { + for (i = 0; i < ARRAY_SIZE(epp_modes); i++) { memset(buf, 0, PAGE_SIZE); - snprintf(buf, PAGE_SIZE, "%s", epp_strings[i]); + snprintf(buf, PAGE_SIZE, "%s", energy_perf_strings[epp_modes[i]]); ret = store_energy_performance_preference(policy, buf, strlen(buf)); if (ret < 0) goto out; @@ -389,8 +396,24 @@ static int amd_pstate_ut_epp(u32 index) */ strreplace(buf, '(', '\0'); - if (strcmp(buf, epp_strings[i])) { - pr_err("String EPP value mismatch: %s != %s\n", buf, epp_strings[i]); + if (strcmp(buf, energy_perf_strings[epp_modes[i]])) { + u8 epp = FIELD_GET(AMD_CPPC_EPP_PERF_MASK, cpudata->cppc_req_cached); + + /* + * The preference read back may legitimately differ from the one + * written: some platforms (e.g. Zen6) program the same raw EPP + * value for more than one named preference, so show() reports the + * first name that matches that value. + * + * If strings mismatch, confirm the CPU is programmed with the correct + * EPP value for the mode. This verifies hardware programming while + * tolerating such aliasing in driver. + */ + if (epp == amd_pstate_cpu_epp_values(cpudata->cpu_type)[epp_modes[i]]) + continue; + + pr_err("String EPP value mismatch: %s != %s\n", + buf, energy_perf_strings[epp_modes[i]]); ret = -EINVAL; goto out; } @@ -406,11 +429,12 @@ static int amd_pstate_ut_epp(u32 index) * restore it here before dropping policy reference. */ if (orig_dynamic_epp) { + const char * const dynamic_string = energy_perf_strings[epp_modes[0]]; int ret2; ret2 = store_energy_performance_preference(policy, - epp_strings[0], - strlen(epp_strings[0])); + dynamic_string, + strlen(dynamic_string)); if (!ret && (ret2 < 0)) ret = ret2; } diff --git a/drivers/cpufreq/amd-pstate.c b/drivers/cpufreq/amd-pstate.c index d8e2eec6e725..4bf158f677f3 100644 --- a/drivers/cpufreq/amd-pstate.c +++ b/drivers/cpufreq/amd-pstate.c @@ -44,7 +44,6 @@ #include #include -#include #include #include @@ -89,46 +88,6 @@ static int cppc_state = AMD_PSTATE_UNDEFINED; static bool amd_pstate_prefcore = true; static struct quirk_entry *quirks; -/* - * AMD Energy Preference Performance (EPP) - * The EPP is used in the CCLK DPM controller to drive - * the frequency that a core is going to operate during - * short periods of activity. EPP values will be utilized for - * different OS profiles (balanced, performance, power savings) - * display strings corresponding to EPP index in the - * energy_perf_strings[] - * index String - *------------------------------------- - * 0 default - * 1 performance - * 2 balance_performance - * 3 balance_power - * 4 power - * 5 custom (for raw EPP values) - * 6 dynamic (platform profile driven selection) - */ -enum energy_perf_value_index { - EPP_INDEX_DEFAULT = 0, - EPP_INDEX_PERFORMANCE, - EPP_INDEX_BALANCE_PERFORMANCE, - EPP_INDEX_BALANCE_POWERSAVE, - EPP_INDEX_POWERSAVE, - EPP_INDEX_CUSTOM, - EPP_INDEX_DYNAMIC, - EPP_INDEX_MAX, -}; - -static const char * const energy_perf_strings[] = { - [EPP_INDEX_DEFAULT] = "default", - [EPP_INDEX_PERFORMANCE] = "performance", - [EPP_INDEX_BALANCE_PERFORMANCE] = "balance_performance", - [EPP_INDEX_BALANCE_POWERSAVE] = "balance_power", - [EPP_INDEX_POWERSAVE] = "power", - [EPP_INDEX_CUSTOM] = "custom", - [EPP_INDEX_DYNAMIC] = "dynamic", -}; -static_assert(ARRAY_SIZE(energy_perf_strings) == EPP_INDEX_MAX); - /* * The numeric EPP value programmed for each named preference. First dimension * is CPU type (TOPO_CPU_TYPE_ANY for non-hybrid, TOPO_CPU_TYPE_PERFORMANCE/ @@ -159,7 +118,7 @@ static_assert(ARRAY_SIZE(epp_values) == TOPO_CPU_TYPE_LOW_POWER + 1, * Non-hybrid systems use TOPO_CPU_TYPE_ANY; hybrid systems use the CPU's * actual type (PERFORMANCE/EFFICIENCY/LOW_POWER). */ -static inline u8 *amd_pstate_cpu_epp_values(enum x86_topology_cpu_type cpu_type) +u8 *amd_pstate_cpu_epp_values(enum x86_topology_cpu_type cpu_type) { switch (cpu_type) { case TOPO_CPU_TYPE_PERFORMANCE: @@ -170,6 +129,7 @@ static inline u8 *amd_pstate_cpu_epp_values(enum x86_topology_cpu_type cpu_type) return epp_values[TOPO_CPU_TYPE_ANY]; } } +EXPORT_SYMBOL_FOR_PSTATE_UT(amd_pstate_cpu_epp_values); /** * struct amd_pstate_epp_values - EPP values for the four named preferences diff --git a/drivers/cpufreq/amd-pstate.h b/drivers/cpufreq/amd-pstate.h index c7189d177b81..422188c639b1 100644 --- a/drivers/cpufreq/amd-pstate.h +++ b/drivers/cpufreq/amd-pstate.h @@ -11,6 +11,8 @@ #include #include +#include + #if IS_MODULE(CONFIG_X86_AMD_PSTATE_UT) #define EXPORT_SYMBOL_FOR_PSTATE_UT(symbol) \ EXPORT_SYMBOL_FOR_MODULES(symbol, "amd-pstate-ut") @@ -162,6 +164,48 @@ enum amd_pstate_mode { AMD_PSTATE_MAX, }; +/* + * AMD Energy Preference Performance (EPP) + * The EPP is used in the CCLK DPM controller to drive + * the frequency that a core is going to operate during + * short periods of activity. EPP values will be utilized for + * different OS profiles (balanced, performance, power savings) + * display strings corresponding to EPP index in the + * energy_perf_strings[] + * index String + *------------------------------------- + * 0 default + * 1 performance + * 2 balance_performance + * 3 balance_power + * 4 power + * 5 custom (for raw EPP values) + * 6 dynamic (platform profile driven selection) + */ +enum energy_perf_value_index { + EPP_INDEX_DEFAULT = 0, + EPP_INDEX_PERFORMANCE, + EPP_INDEX_BALANCE_PERFORMANCE, + EPP_INDEX_BALANCE_POWERSAVE, + EPP_INDEX_POWERSAVE, + EPP_INDEX_CUSTOM, + EPP_INDEX_DYNAMIC, + EPP_INDEX_MAX, +}; + +static const char * const energy_perf_strings[] = { + [EPP_INDEX_DEFAULT] = "default", + [EPP_INDEX_PERFORMANCE] = "performance", + [EPP_INDEX_BALANCE_PERFORMANCE] = "balance_performance", + [EPP_INDEX_BALANCE_POWERSAVE] = "balance_power", + [EPP_INDEX_POWERSAVE] = "power", + [EPP_INDEX_CUSTOM] = "custom", + [EPP_INDEX_DYNAMIC] = "dynamic", +}; +static_assert(ARRAY_SIZE(energy_perf_strings) == EPP_INDEX_MAX); + +u8 *amd_pstate_cpu_epp_values(enum x86_topology_cpu_type cpu_type); + static inline u8 freq_to_perf(union perf_cached perf, u32 nominal_freq, unsigned int freq_val) { u32 perf_val = DIV_ROUND_UP_ULL((u64)freq_val * perf.nominal_perf, nominal_freq); From 02c5d7bfa8724789f6620f7787532666edc891a1 Mon Sep 17 00:00:00 2001 From: Mehmet Koseoglu Date: Thu, 1 Oct 2026 13:19:15 +0300 Subject: [PATCH 70/89] rust: cpufreq: reject NULL from cpufreq_cpu_get() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cpufreq_cpu_get() returns either a referenced policy or NULL. PolicyCpu::from_cpu() passed its return value to from_err_ptr(), which rejects ERR_PTR values but accepts NULL. If the lookup fails, Policy::from_raw_mut() therefore constructs a mutable reference from NULL. Dropping the resulting PolicyCpu then passes the invalid pointer to cpufreq_cpu_put(), causing an oops in kobject_put(). Reject NULL with NonNull before constructing the Policy reference. Return ENODEV instead. A KUnit negative-control run reproduced the oops with the original conversion. The same test passed with this change. The reproducer is available on request. Fixes: 6ebdd7c93177 ("rust: cpufreq: Extend abstractions for policy and driver ops") Cc: stable@vger.kernel.org Assisted-by: LLM Signed-off-by: Mehmet Koseoglu Reviewed-by: Onur Özkan Reviewed-by: spidermana Signed-off-by: Viresh Kumar --- rust/kernel/cpufreq.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/rust/kernel/cpufreq.rs b/rust/kernel/cpufreq.rs index affa2b9490ef..b75d692846a2 100644 --- a/rust/kernel/cpufreq.rs +++ b/rust/kernel/cpufreq.rs @@ -14,7 +14,13 @@ cpumask, device::{Bound, Device}, devres, - error::{code::*, from_err_ptr, from_result, to_result, Result, VTABLE_DEFAULT_ERROR}, + error::{ + code::*, + from_result, + to_result, + Result, + VTABLE_DEFAULT_ERROR, // + }, ffi::{c_char, c_ulong}, prelude::*, types::ForeignOwnable, @@ -29,7 +35,10 @@ marker::PhantomData, ops::{Deref, DerefMut}, pin::Pin, - ptr, + ptr::{ + self, + NonNull, // + }, }; use macros::vtable; @@ -687,12 +696,13 @@ fn clear_data(&mut self) -> Option { impl<'a> PolicyCpu<'a> { fn from_cpu(cpu: CpuId) -> Result { // SAFETY: It is safe to call `cpufreq_cpu_get` for any valid CPU. - let ptr = from_err_ptr(unsafe { bindings::cpufreq_cpu_get(u32::from(cpu)) })?; + let ptr = + NonNull::new(unsafe { bindings::cpufreq_cpu_get(u32::from(cpu)) }).ok_or(ENODEV)?; Ok(Self( - // SAFETY: The `ptr` is guaranteed to be valid and remains valid for the lifetime of - // the returned reference. - unsafe { Policy::from_raw_mut(ptr) }, + // SAFETY: `ptr` is non-NULL and `cpufreq_cpu_get()` took a reference on it, so it is + // valid for writing and remains valid for the lifetime of the returned reference. + unsafe { Policy::from_raw_mut(ptr.as_ptr()) }, )) } } From e5b33fa47792375ca7fc49ccadee87d8e4b72929 Mon Sep 17 00:00:00 2001 From: Xueqin Luo Date: Mon, 31 Aug 2026 13:44:38 +0800 Subject: [PATCH 71/89] cpufreq: sparc-us2e: fix frequency table index copy-paste error In us2e_freq_cpu_init(), the last two frequency writes both use index [2] instead of [3] and [4] respectively, and the terminator uses [3] instead of [5]. This is a copy-paste error where the index was not incremented, causing the divider-6 and divider-8 entries to overwrite the already-written divider-4 entry. As a result, only three frequency steps (div 1, 2, and 4) are actually available to the cpufreq core, while the intended dividers 6 and 8 are silently lost. The struct us2e_freq_percpu_info::table[6] has room for 5 entries plus a terminator, matching the 5 hardware dividers. Fix the indices so all five frequency steps are correctly populated: table[0]=div1, table[1]=div2, table[2]=div4, table[3]=div6, table[4]=div8, table[5]=TABLE_END. Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") Signed-off-by: Xueqin Luo Signed-off-by: Viresh Kumar --- drivers/cpufreq/sparc-us2e-cpufreq.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/drivers/cpufreq/sparc-us2e-cpufreq.c b/drivers/cpufreq/sparc-us2e-cpufreq.c index a68706406b88..5cda391ad03b 100644 --- a/drivers/cpufreq/sparc-us2e-cpufreq.c +++ b/drivers/cpufreq/sparc-us2e-cpufreq.c @@ -282,12 +282,11 @@ static int us2e_freq_cpu_init(struct cpufreq_policy *policy) table[1].frequency = clock_tick / 2; table[2].driver_data = 2; table[2].frequency = clock_tick / 4; - table[2].driver_data = 3; - table[2].frequency = clock_tick / 6; - table[2].driver_data = 4; - table[2].frequency = clock_tick / 8; - table[2].driver_data = 5; - table[3].frequency = CPUFREQ_TABLE_END; + table[3].driver_data = 3; + table[3].frequency = clock_tick / 6; + table[4].driver_data = 4; + table[4].frequency = clock_tick / 8; + table[5].frequency = CPUFREQ_TABLE_END; policy->cpuinfo.transition_latency = 0; policy->cur = clock_tick; From f0a750dad7da1c653e85f0698063235453b43831 Mon Sep 17 00:00:00 2001 From: Xueqin Luo Date: Mon, 31 Aug 2026 15:30:08 +0800 Subject: [PATCH 72/89] cpufreq: tegra194: fix double-pointer error in get_cpu_ndiv ndiv is already a u64 pointer, passing &ndiv to smp_call_function_single() results in a u64** being written to instead of the caller's u64 variable, so the caller always reads back an uninitialized ndiv. Drop the spurious '&'. Fixes: 0839ed1fd7ac ("cpufreq: tegra194: add soc data to support multiple soc") Signed-off-by: Xueqin Luo Reviewed-by: Sumit Gupta Signed-off-by: Viresh Kumar --- drivers/cpufreq/tegra194-cpufreq.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/cpufreq/tegra194-cpufreq.c b/drivers/cpufreq/tegra194-cpufreq.c index c6375e14d445..1eb96e9f3310 100644 --- a/drivers/cpufreq/tegra194-cpufreq.c +++ b/drivers/cpufreq/tegra194-cpufreq.c @@ -367,7 +367,7 @@ static void tegra194_get_cpu_ndiv_sysreg(void *ndiv) static int tegra194_get_cpu_ndiv(u32 cpu, u32 cpuid, u32 clusterid, u64 *ndiv) { - return smp_call_function_single(cpu, tegra194_get_cpu_ndiv_sysreg, &ndiv, true); + return smp_call_function_single(cpu, tegra194_get_cpu_ndiv_sysreg, ndiv, true); } static void tegra194_set_cpu_ndiv_sysreg(void *data) From fdbe4cae71c8196cd7681ad47254b5b0560d3c1e Mon Sep 17 00:00:00 2001 From: Xueqin Luo Date: Mon, 31 Aug 2026 17:11:59 +0800 Subject: [PATCH 73/89] cpufreq: sti: avoid NULL dereference in dev_err() ddata.cpu is NULL-checked, then immediately passed to dev_err(). Replace with pr_err() to avoid dereferencing NULL. Fixes: ab0ea257fc58 ("cpufreq: st: Provide runtime initialised driver for ST's platforms") Signed-off-by: Xueqin Luo Signed-off-by: Viresh Kumar --- drivers/cpufreq/sti-cpufreq.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/cpufreq/sti-cpufreq.c b/drivers/cpufreq/sti-cpufreq.c index b15b3142b5fe..144ac0f1d85d 100644 --- a/drivers/cpufreq/sti-cpufreq.c +++ b/drivers/cpufreq/sti-cpufreq.c @@ -263,7 +263,7 @@ static int __init sti_cpufreq_init(void) ddata.cpu = get_cpu_device(0); if (!ddata.cpu) { - dev_err(ddata.cpu, "Failed to get device for CPU0\n"); + pr_err("Failed to get device for CPU0\n"); goto skip_voltage_scaling; } @@ -281,7 +281,7 @@ static int __init sti_cpufreq_init(void) goto register_cpufreq_dt; skip_voltage_scaling: - dev_err(ddata.cpu, "Not doing voltage scaling\n"); + pr_err("Not doing voltage scaling\n"); register_cpufreq_dt: platform_device_register_simple("cpufreq-dt", -1, NULL, 0); From 53732733c7af437b0ed710a1df91a67a0d3dfc07 Mon Sep 17 00:00:00 2001 From: Frank Zhang Date: Fri, 4 Sep 2026 15:56:35 +0800 Subject: [PATCH 74/89] rust: rcpufreq_dt: add module alias Add the missing `platform:cpufreq-dt` module alias so that the driver can be automatically loaded when the platform device is registered. Fixes: 06149d8f2216 ("cpufreq: Add Rust-based cpufreq-dt driver") Cc: stable@vger.kernel.org Signed-off-by: Frank Zhang Signed-off-by: Viresh Kumar --- drivers/cpufreq/rcpufreq_dt.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/cpufreq/rcpufreq_dt.rs b/drivers/cpufreq/rcpufreq_dt.rs index d7ead60bf8c2..2b1f18b7a1e7 100644 --- a/drivers/cpufreq/rcpufreq_dt.rs +++ b/drivers/cpufreq/rcpufreq_dt.rs @@ -225,4 +225,5 @@ fn probe<'bound>( authors: ["Viresh Kumar "], description: "Generic CPUFreq DT driver", license: "GPL v2", + alias: ["platform:cpufreq-dt"], } From 3b585d7fa3a1256c8bc3b9bb3b1dcfc3b2ff5d91 Mon Sep 17 00:00:00 2001 From: Sumeet Pawnikar Date: Sun, 13 Sep 2026 00:41:53 +0530 Subject: [PATCH 75/89] cpufreq: Use %pe to print error pointers symbolically Replace PTR_ERR() and %ld with %pe and pass the original pointer directly to pr_err() and pr_warn(). The %pe format specifier prints a symbolic error name (e.g. -ENOMEM) when CONFIG_SYMBOLIC_ERRNAME is enabled, otherwise it falls back gracefully and prints the raw integer value. This makes messages more readable without any functional change. Signed-off-by: Sumeet Pawnikar Signed-off-by: Viresh Kumar --- drivers/cpufreq/bmips-cpufreq.c | 4 ++-- drivers/cpufreq/cppc_cpufreq.c | 4 ++-- drivers/cpufreq/qoriq-cpufreq.c | 3 +-- drivers/cpufreq/s3c64xx-cpufreq.c | 5 ++--- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/drivers/cpufreq/bmips-cpufreq.c b/drivers/cpufreq/bmips-cpufreq.c index a8e35bc75fb2..389fff5e6f65 100644 --- a/drivers/cpufreq/bmips-cpufreq.c +++ b/drivers/cpufreq/bmips-cpufreq.c @@ -132,8 +132,8 @@ static int bmips_cpufreq_init(struct cpufreq_policy *policy) freq_table = bmips_cpufreq_get_freq_table(policy); if (IS_ERR(freq_table)) { - pr_err("%s: couldn't determine frequency table (%ld).\n", - BMIPS_CPUFREQ_NAME, PTR_ERR(freq_table)); + pr_err("%s: couldn't determine frequency table (%pe).\n", + BMIPS_CPUFREQ_NAME, freq_table); return PTR_ERR(freq_table); } diff --git a/drivers/cpufreq/cppc_cpufreq.c b/drivers/cpufreq/cppc_cpufreq.c index 80893844353c..f767898ebfb5 100644 --- a/drivers/cpufreq/cppc_cpufreq.c +++ b/drivers/cpufreq/cppc_cpufreq.c @@ -230,8 +230,8 @@ static void cppc_fie_kworker_init(void) kworker_fie = kthread_run_worker(0, "cppc_fie"); if (IS_ERR(kworker_fie)) { - pr_warn("%s: failed to create kworker_fie: %ld\n", __func__, - PTR_ERR(kworker_fie)); + pr_warn("%s: failed to create kworker_fie: %pe\n", __func__, + kworker_fie); fie_disabled = FIE_DISABLED; kworker_fie = NULL; return; diff --git a/drivers/cpufreq/qoriq-cpufreq.c b/drivers/cpufreq/qoriq-cpufreq.c index 42edb41ad459..a21b6f01218a 100644 --- a/drivers/cpufreq/qoriq-cpufreq.c +++ b/drivers/cpufreq/qoriq-cpufreq.c @@ -57,8 +57,7 @@ static u32 get_bus_freq(void) /* get platform freq by its clock name */ pltclk = clk_get(NULL, "cg-pll0-div1"); if (IS_ERR(pltclk)) { - pr_err("%s: can't get bus frequency %ld\n", - __func__, PTR_ERR(pltclk)); + pr_err("%s: can't get bus frequency %pe\n", __func__, pltclk); return PTR_ERR(pltclk); } diff --git a/drivers/cpufreq/s3c64xx-cpufreq.c b/drivers/cpufreq/s3c64xx-cpufreq.c index 9cef71528076..9a01592425ee 100644 --- a/drivers/cpufreq/s3c64xx-cpufreq.c +++ b/drivers/cpufreq/s3c64xx-cpufreq.c @@ -152,15 +152,14 @@ static int s3c64xx_cpufreq_driver_init(struct cpufreq_policy *policy) policy->clk = clk_get(NULL, "armclk"); if (IS_ERR(policy->clk)) { - pr_err("Unable to obtain ARMCLK: %ld\n", - PTR_ERR(policy->clk)); + pr_err("Unable to obtain ARMCLK: %pe\n", policy->clk); return PTR_ERR(policy->clk); } #ifdef CONFIG_REGULATOR vddarm = regulator_get(NULL, "vddarm"); if (IS_ERR(vddarm)) { - pr_err("Failed to obtain VDDARM: %ld\n", PTR_ERR(vddarm)); + pr_err("Failed to obtain VDDARM: %pe\n", vddarm); pr_err("Only frequency scaling available\n"); vddarm = NULL; } else { From cce713d008bdcca5e46541de86eab79b80d394bc Mon Sep 17 00:00:00 2001 From: Alexander Koskovich Date: Mon, 5 Oct 2026 17:47:27 +0000 Subject: [PATCH 76/89] cpufreq: Add Milos to cpufreq-dt-platdev blocklist The Qualcomm Milos platform uses the qcom-cpufreq-hw driver so add it to the cpufreq-dt-platdev driver's blocklist. Signed-off-by: Alexander Koskovich Signed-off-by: Viresh Kumar --- drivers/cpufreq/cpufreq-dt-platdev.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/cpufreq/cpufreq-dt-platdev.c b/drivers/cpufreq/cpufreq-dt-platdev.c index 371ca1fe9020..574c5bec4831 100644 --- a/drivers/cpufreq/cpufreq-dt-platdev.c +++ b/drivers/cpufreq/cpufreq-dt-platdev.c @@ -155,6 +155,7 @@ static const struct of_device_id blocklist[] __initconst = { { .compatible = "nvidia,tegra234", }, { .compatible = "qcom,apq8096", }, + { .compatible = "qcom,milos", }, { .compatible = "qcom,msm8909", }, { .compatible = "qcom,msm8996", }, { .compatible = "qcom,msm8998", }, From 95cc3927ce26cadc325833a16b13c2a3c08353de Mon Sep 17 00:00:00 2001 From: Kathiravan Thirumoorthy Date: Wed, 7 Oct 2026 12:31:56 +0530 Subject: [PATCH 77/89] cpufreq: qcom-nvmem: Add IPQ9650 support Add IPQ9650 support to the Qualcomm NVMEM cpufreq driver so the supported OPP can be selected at runtime using the eFuse value and the opp-supported-hw OPP property. Also block the generic cpufreq-dt platform device for IPQ9650 so the NVMEM-based driver is used. Signed-off-by: Kathiravan Thirumoorthy Reviewed-by: Dmitry Baryshkov Signed-off-by: Viresh Kumar --- drivers/cpufreq/cpufreq-dt-platdev.c | 1 + drivers/cpufreq/qcom-cpufreq-nvmem.c | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/drivers/cpufreq/cpufreq-dt-platdev.c b/drivers/cpufreq/cpufreq-dt-platdev.c index 574c5bec4831..bce4c87573a1 100644 --- a/drivers/cpufreq/cpufreq-dt-platdev.c +++ b/drivers/cpufreq/cpufreq-dt-platdev.c @@ -210,6 +210,7 @@ static const struct of_device_id blocklist[] __initconst = { { .compatible = "qcom,ipq8064", }, { .compatible = "qcom,ipq8074", }, { .compatible = "qcom,ipq9574", }, + { .compatible = "qcom,ipq9650", }, { .compatible = "qcom,apq8064", }, { .compatible = "qcom,msm8974", }, { .compatible = "qcom,msm8960", }, diff --git a/drivers/cpufreq/qcom-cpufreq-nvmem.c b/drivers/cpufreq/qcom-cpufreq-nvmem.c index efa766e98d86..375f12d5d11a 100644 --- a/drivers/cpufreq/qcom-cpufreq-nvmem.c +++ b/drivers/cpufreq/qcom-cpufreq-nvmem.c @@ -211,6 +211,14 @@ static int qcom_cpufreq_kryo_name_version(struct device *cpu_dev, case QCOM_ID_IPQ5404: drv->versions = (*speedbin == 0x3b) ? BIT(1) : BIT(0); break; + case QCOM_ID_IPQ9610: + case QCOM_ID_IPQ9620: + case QCOM_ID_IPQ9630: + case QCOM_ID_IPQ9640: + case QCOM_ID_IPQ9650: + case QCOM_ID_IPQ9670: + drv->versions = (*speedbin == 0x4b) ? BIT(3) : BIT(2); + break; case QCOM_ID_MSM8996SG: case QCOM_ID_APQ8096SG: drv->versions = 1 << ((unsigned int)(*speedbin) + 4); @@ -633,6 +641,7 @@ static const struct of_device_id qcom_cpufreq_match_list[] __initconst __maybe_u { .compatible = "qcom,ipq8074", .data = &match_data_ipq8074 }, { .compatible = "qcom,apq8064", .data = &match_data_krait }, { .compatible = "qcom,ipq9574", .data = &match_data_kryo }, + { .compatible = "qcom,ipq9650", .data = &match_data_kryo }, { .compatible = "qcom,msm8974", .data = &match_data_krait }, { .compatible = "qcom,msm8960", .data = &match_data_krait }, {}, From 6fa50b1988b8dfd2e92a9cecba073fe4347a5e23 Mon Sep 17 00:00:00 2001 From: Haotian Zhang Date: Fri, 9 Oct 2026 01:24:42 +0800 Subject: [PATCH 78/89] cpufreq: qcom-cpufreq-hw: fix device node refcount leak in phandle parsing of_parse_phandle_with_args() transfers a reference to the provider node in out_args->np, which the caller is responsible for releasing with of_node_put(). Neither qcom_get_related_cpus() nor qcom_cpufreq_hw_cpu_init() ever puts args.np: they only put cpu_np, which is a different node obtained from of_cpu_device_node_get(). The reference is therefore leaked on every successful call, and qcom_get_related_cpus() leaks one per present CPU. Release args.np after the parsed index has been read in both call sites. Fixes: 2849dd8bc72b ("cpufreq: qcom-hw: Add support for QCOM cpufreq HW driver") Assisted-by: DeepSeek-V4.1-Flash Signed-off-by: Haotian Zhang Signed-off-by: Viresh Kumar --- drivers/cpufreq/qcom-cpufreq-hw.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/cpufreq/qcom-cpufreq-hw.c b/drivers/cpufreq/qcom-cpufreq-hw.c index 874ff3fb9d97..060a460716f7 100644 --- a/drivers/cpufreq/qcom-cpufreq-hw.c +++ b/drivers/cpufreq/qcom-cpufreq-hw.c @@ -322,6 +322,8 @@ static void qcom_get_related_cpus(int index, struct cpumask *m) if (index == args.args[0]) cpumask_set_cpu(cpu, m); + + of_node_put(args.np); } } @@ -554,6 +556,7 @@ static int qcom_cpufreq_hw_cpu_init(struct cpufreq_policy *policy) return ret; index = args.args[0]; + of_node_put(args.np); data = &qcom_cpufreq.data[index]; /* HW should be in enabled state to proceed */ From a4b90518dd01f85e1e4bf7e9361396b2eded1f7e Mon Sep 17 00:00:00 2001 From: Ryan Brue Date: Fri, 9 Oct 2026 14:44:43 -0500 Subject: [PATCH 79/89] cpufreq: mediatek: Enter system sleep at the suspend OPP cpufreq_suspend() stops the governors at the start of system suspend, and this driver has no .suspend callback, so each cluster sleeps at whatever OPP it was last left at. On an MT8173 tablet (Amazon Fire HD 10, 2017), entering suspend-to-RAM with both clusters at their highest OPP measured 63.5 mA. With this change and the lowest OPP marked, the same entry measured 52.0 mA. Use the generic suspend-frequency support, as cpufreq-dt does. Tables without an opp-suspend entry leave suspend_freq at 0, which makes cpufreq_generic_suspend() a no-op, so they are unaffected. Assisted-by: LLM Signed-off-by: Ryan Brue Signed-off-by: Viresh Kumar --- drivers/cpufreq/mediatek-cpufreq.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/cpufreq/mediatek-cpufreq.c b/drivers/cpufreq/mediatek-cpufreq.c index 052ca7cd2f4f..f9b1eca176eb 100644 --- a/drivers/cpufreq/mediatek-cpufreq.c +++ b/drivers/cpufreq/mediatek-cpufreq.c @@ -603,6 +603,7 @@ static int mtk_cpufreq_init(struct cpufreq_policy *policy) policy->freq_table = freq_table; policy->driver_data = info; policy->clk = info->cpu_clk; + policy->suspend_freq = dev_pm_opp_get_suspend_opp_freq(info->cpu_dev) / 1000; return 0; } @@ -624,6 +625,7 @@ static struct cpufreq_driver mtk_cpufreq_driver = { .init = mtk_cpufreq_init, .exit = mtk_cpufreq_exit, .register_em = cpufreq_register_em_with_opp, + .suspend = cpufreq_generic_suspend, .name = "mtk-cpufreq", }; From 71245acb8c937382fc31806572314d36667b17f6 Mon Sep 17 00:00:00 2001 From: Ryan Brue Date: Fri, 9 Oct 2026 14:44:44 -0500 Subject: [PATCH 80/89] arm64: dts: mediatek: mt8173: Mark the lowest CPU OPPs for suspend Both clusters otherwise enter system sleep at whatever OPP the governor last chose. On the Amazon Fire HD 10 (2017), entering suspend-to-RAM with both clusters at their highest OPP measured 63.5 mA, against 52.0 mA when they suspend at 507 MHz. Mark the 507 MHz OPP of each cluster, the lowest in both tables, as the one to suspend at. mediatek-cpufreq uses it for system sleep, and a kernel without that support ignores it. Assisted-by: LLM Signed-off-by: Ryan Brue Signed-off-by: Viresh Kumar --- arch/arm64/boot/dts/mediatek/mt8173.dtsi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/mediatek/mt8173.dtsi b/arch/arm64/boot/dts/mediatek/mt8173.dtsi index 78c2ccd5be13..7f1af15cb934 100644 --- a/arch/arm64/boot/dts/mediatek/mt8173.dtsi +++ b/arch/arm64/boot/dts/mediatek/mt8173.dtsi @@ -56,6 +56,7 @@ cluster0_opp: opp-table-0 { opp-507000000 { opp-hz = /bits/ 64 <507000000>; opp-microvolt = <859000>; + opp-suspend; }; opp-702000000 { opp-hz = /bits/ 64 <702000000>; @@ -93,6 +94,7 @@ cluster1_opp: opp-table-1 { opp-507000000 { opp-hz = /bits/ 64 <507000000>; opp-microvolt = <828000>; + opp-suspend; }; opp-702000000 { opp-hz = /bits/ 64 <702000000>; From 087d1581f03ec4f924c22620c4970602f0ca734e Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Thu, 8 Oct 2026 22:28:31 +0100 Subject: [PATCH 81/89] MAINTAINERS: cpufreq: Replace CPPC reviewer Take over CPPC cpufreq review duties from Pierre Gondois. Thanks to Pierre for his reviews and contributions to the driver. Signed-off-by: Christian Loehle Reviewed-by: Pierre Gondois Reviewed-by: Jie Zhan Link: https://patch.msgid.link/20261008212832.354382-2-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- MAINTAINERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index c2414447892c..e69c888f1ec8 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -6763,7 +6763,7 @@ M: "Rafael J. Wysocki" M: Viresh Kumar R: Jie Zhan R: Lifeng Zheng -R: Pierre Gondois +R: Christian Loehle R: Sumit Gupta L: linux-pm@vger.kernel.org S: Maintained From ff8944361a7b28b80ca3a77a36877b4a45f377b4 Mon Sep 17 00:00:00 2001 From: Christian Loehle Date: Thu, 8 Oct 2026 22:28:32 +0100 Subject: [PATCH 82/89] MAINTAINERS: ACPI: Add a separate CPPC entry CPPC is a distinct part of ACPI, with its own CPU performance control semantics and register access mechanisms. Add a dedicated MAINTAINERS entry to help direct changes to maintainers familiar with those details and the various platforms implementing CPPC. List Rafael and myself as maintainers for the implementation, public header and sysfs documentation. Signed-off-by: Christian Loehle Acked-by: Sudeep Holla Reviewed-by: K Prateek Nayak Link: https://patch.msgid.link/20261008212832.354382-3-christian.loehle@arm.com Signed-off-by: Rafael J. Wysocki --- MAINTAINERS | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/MAINTAINERS b/MAINTAINERS index e69c888f1ec8..68be35f710de 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -327,6 +327,17 @@ F: drivers/acpi/acpica/ F: include/acpi/ F: tools/power/acpi/ +ACPI CPPC +M: "Rafael J. Wysocki" +M: Christian Loehle +L: linux-acpi@vger.kernel.org +L: linux-pm@vger.kernel.org +S: Maintained +T: git git://git.kernel.org/pub/scm/linux/kernel/git/rafael/linux-pm +F: Documentation/admin-guide/acpi/cppc_sysfs.rst +F: drivers/acpi/cppc_acpi.c +F: include/acpi/cppc_acpi.h + ACPI FOR ARM64 (ACPI/arm64) M: Lorenzo Pieralisi M: Hanjun Guo From ce390caf5541c493c9061e7a2d98a463e66b736a Mon Sep 17 00:00:00 2001 From: Hui Su Date: Tue, 13 Oct 2026 12:42:35 +0900 Subject: [PATCH 83/89] cpufreq: schedutil: Serialize sugov_start() against rate limit updates The current value of sg_policy->tunables->rate_limit_us is used in sugov_start() without locking to update sg_policy->freq_update_delay_ns, so if rate_limit_us_store() runs in parallel with it, the rate limit update may go unnoticed and the new freq_update_delay_ns value may be stale until the rate limit is updated again. To avoid that, acquire the update_lock mutex of the rate limit sysfs attribute, that is held by rate_limit_us_store() during rate limit updates, around the freq_update_delay_ns in sugov_start(). Fixes: 9bdcb44e391d ("cpufreq: schedutil: New governor based on scheduler utilization data") Reviewed-by: Zhongqiu Han Signed-off-by: Hui Su Cc: All applicable [ rjw: Subject adjustment, new changelog, edits of the new comment ] [ rjw: Add empty code line after guard()() ] Link: https://patch.msgid.link/20261013034235.2678177-1-sh_def@163.com Signed-off-by: Rafael J. Wysocki --- kernel/sched/cpufreq_schedutil.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/kernel/sched/cpufreq_schedutil.c b/kernel/sched/cpufreq_schedutil.c index 49ccd6f1c185..14405a614991 100644 --- a/kernel/sched/cpufreq_schedutil.c +++ b/kernel/sched/cpufreq_schedutil.c @@ -73,6 +73,18 @@ static void sugov_update_rate_limit_us(struct sugov_policy *sg_policy) (s64)sg_policy->tunables->rate_limit_us * NSEC_PER_USEC; } +static void sugov_update_rate_limit_us_sync(struct sugov_policy *sg_policy) +{ + /* + * Serialize with rate_limit_us_store() to prevent this path from using + * a stale rate limit tunable value to set the limit for the given + * policy. + */ + guard(mutex)(&sg_policy->tunables->attr_set.update_lock); + + sugov_update_rate_limit_us(sg_policy); +} + static bool sugov_should_update_freq(struct sugov_policy *sg_policy, u64 time) { s64 delta_ns; @@ -861,7 +873,7 @@ static int sugov_start(struct cpufreq_policy *policy) void (*uu)(struct update_util_data *data, u64 time, unsigned int flags); unsigned int cpu; - sugov_update_rate_limit_us(sg_policy); + sugov_update_rate_limit_us_sync(sg_policy); sg_policy->last_freq_update_time = 0; sg_policy->next_freq = 0; sg_policy->work_in_progress = false; From 15ef5c343b250c7ffe3c3e8311101089c0a4e001 Mon Sep 17 00:00:00 2001 From: Brian Norris Date: Thu, 1 Oct 2026 08:50:08 -0700 Subject: [PATCH 84/89] PM: runtime: Expand introduction with core concepts and structure I commonly see people have difficulty learning how runtime PM works because of the following key points [*]: 1) there are several boolean concepts in runtime PM, with somewhat similar meanings: enabled / disabled active / suspended allowed / forbidden 2) if these concepts are documented at all, they're scattered across the kerneldoc or Documentation/ 3) the runtime_pm.rst docs don't make any attempt to ease a reader into understanding the concepts, and instead jump straight into how it's implemented (queues, 'struct device' fields, helpers). Let's try to remedy this a bit by discussing the core concepts and highlights at the top of the introduction, and introduce a few sub-headings, so it's easier to navigate different aspects of the introduction. While shuffling the intro around, I also see that the existing text largely mirrors the layout of the following sections (2, 3, and 4), but does so out of order. Reorder those, and point to section numbers. [*] In addition to API complexity. I count 61 pm_*() helpers, 7 of which are variations of put() and 8 of which are variations of get(). Signed-off-by: Brian Norris Link: https://patch.msgid.link/20261001085009.v4.1.I45a794e4452e3df22419acdd8f11e2b788b3413f@changeid Signed-off-by: Rafael J. Wysocki --- Documentation/power/runtime_pm.rst | 110 ++++++++++++++++++++++++----- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/Documentation/power/runtime_pm.rst b/Documentation/power/runtime_pm.rst index 68903d93a3e7..e4407b687f8b 100644 --- a/Documentation/power/runtime_pm.rst +++ b/Documentation/power/runtime_pm.rst @@ -13,31 +13,107 @@ Runtime Power Management Framework for I/O Devices 1. Introduction =============== -Support for runtime power management (runtime PM) of I/O devices is provided -at the power management core (PM core) level by means of: +Runtime power management (or runtime PM, sometimes also referred to as RPM) +allows individual I/O devices that are not in active use to be put into +low-power states, in which they may not be operational or even accessible, and +go back to the fully operational state as needed. Power can be reduced this +way without waiting for a transition into a system-wide sleep state. -* The power management workqueue pm_wq in which bus types and device drivers can - put their PM-related work items. It is strongly recommended that pm_wq be - used for queuing all work items related to runtime PM, because this allows - them to be synchronized with system-wide power transitions (suspend to RAM, - hibernation and resume from system sleep states). pm_wq is declared in - include/linux/pm_runtime.h and defined in kernel/power/main.c. +Core Concepts +------------- -* A number of runtime PM fields in the 'power' member of 'struct device' (which - is of the type 'struct dev_pm_info', defined in include/linux/pm.h) that can - be used for synchronizing runtime PM operations with one another. +Runtime PM operates around a few device-level concepts — whether a device is +**active** or **suspended**; whether runtime PM is **enabled** or **disabled**; +whether runtime PM is **allowed** or **forbidden**; and whether a device is +**in use**. + +* **Active**: Runtime PM tracks whether or not devices are in active use, which + generally means that they are accessible and operational, and may be depended + on by something (for example, other devices or user space). This is + represented by the **active** meta-state. By contrast, the **suspended** + meta-state represents a promise that the given device will not be accessed. + This covers devices in low-power states, but it also may include devices that + are not fully operational. There are also two transient meta-states, + **suspending** and **resuming**, representing transitions between **active** + and **suspended** often referred to as *runtime suspend* and *runtime + resume*, or just *suspend* and *resume*, respectively. Every device handled + by runtime PM is in one of these four meta-states at any time, and its + meta-state is expected to reflect its actual physical configuration (that is, + for instance, if the device is not accessible, it must not be **active**). + The term "runtime PM status" used in what follows refers to the meta-states + described above. + +* **Enabled**: In order to track the runtime PM status and carry out + transitions between **active** and **suspended**, runtime PM needs to be + **enabled** for the given device. If it is **disabled**, runtime PM + generally leaves the device alone. This means in particular that the runtime + PM status of a device and its actual physical configuration may get out of + sync after disabling runtime PM for it. Additionally, the runtime PM status + of a device may need to be explicitly adjusted to reflect its current + physical configuration before enabling runtime PM for it (e.g., during device + probe). Once runtime PM is **enabled**, it begins managing PM status — + performing *runtime suspend* and *runtime resume* transitions based on its + understanding of whether a device is in use. + + As a rule, all devices are initialized with runtime PM **disabled**, though + some bus types (such as PCI) manage some RPM initialization automatically, + and therefore probe devices in an **enabled** state. For other devices, + drivers must enable runtime PM on their own to opt in. + +* **Allowed**: Runtime PM may be **forbidden** even though it has been enabled. + Doing so causes the given device to transition into the **active** meta-state + and prevents it from being suspended. In other words, a device with + forbidden runtime PM remains **active** at least until runtime PM is + **allowed** for it again. This mechanism can be managed by in-kernel APIs, + but more importantly, it is also available to user space via the + ``/sys/devices/.../power/control`` sysfs attribute of the given device. + Namely, writing ``on`` to that attribute forbids runtime PM, and writing + ``auto`` allows it. User space may modify that attribute at any time, so the + ability to transition a device into the **suspended** meta-state via runtime + PM must not be relied on for correctness. + +* **In use**: Runtime PM determines whether a device should be transitioned to + an **active** state by its understanding of whether a device is in use. The + first way a device is considered **in use** is by its own usage count — a + reference counter driven by a variety of get()/put() APIs. Additionally, a + device may be considered **in use** by having active dependents — either + child devices, or device-linked consumers. When a device is no longer in + use, runtime PM may choose to transition it to the **suspended** state. + + Thus, when a device moves from **suspended** to **active**, it may also cause + its dependencies to resume, unless they are **active** already. Conversely, + suspending a device may also cause its otherwise-unused dependencies to + suspend. + +* **Autosuspend**: Runtime PM also supports a feature called "autosuspend." + This is different than the ``control`` notion of "auto" (i.e., **allowed**). + Autosuspend is covered in `Section 9`_. + +Implementation Structure +------------------------ + +Support for runtime power management is provided at the power management core +(PM core) level by means of: * Three device runtime PM callbacks in 'struct dev_pm_ops' (defined in - include/linux/pm.h). + include/linux/pm.h). See `Section 2`_. + +* A number of runtime PM fields in the 'power' member of 'struct device' that + can be used for synchronizing runtime PM operations with one another. These + are covered in `Section 3`_. * A set of helper functions defined in drivers/base/power/runtime.c that can be used for carrying out runtime PM operations in such a way that the - synchronization between them is taken care of by the PM core. Bus types and - device drivers are encouraged to use these functions. + synchronization between them is taken care of by the PM core. Bus types and + device drivers are encouraged to use these functions. They are covered in + `Section 4`_. -The runtime PM callbacks present in 'struct dev_pm_ops', the device runtime PM -fields of 'struct dev_pm_info' and the core helper functions provided for -runtime PM are described below. +* The power management workqueue pm_wq in which bus types and device drivers can + put their PM-related work items. It is strongly recommended that pm_wq be + used for queuing all work items related to runtime PM, because this allows + them to be synchronized with system-wide power transitions (suspend to RAM, + hibernation and resume from system sleep states). pm_wq is declared in + include/linux/pm_runtime.h and defined in kernel/power/main.c. .. _Section 2: From 48a82a3f44576a1b227f964fdb150e1cae7455bf Mon Sep 17 00:00:00 2001 From: Huacai Chen Date: Fri, 28 Aug 2026 11:11:57 +0800 Subject: [PATCH 85/89] cpufreq: loongson3: Make this driver depend on MACH_LOONGSON64 32BIT Loongson machines don't have SMC and FreqCtrl registers, so make this driver depend on MACH_LOONGSON64 (only for LoongArch based 64bit machines currently). Cc: stable@vger.kernel.org Fixes: ccf51454145b ("cpufreq: Add Loongson-3 CPUFreq driver support") Signed-off-by: Huacai Chen Reviewed-by: Zhongqiu Han Signed-off-by: Viresh Kumar --- drivers/cpufreq/Kconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/cpufreq/Kconfig b/drivers/cpufreq/Kconfig index db83f3365698..edc1299098d4 100644 --- a/drivers/cpufreq/Kconfig +++ b/drivers/cpufreq/Kconfig @@ -288,6 +288,7 @@ endif if LOONGARCH config LOONGSON3_CPUFREQ tristate "Loongson3 CPUFreq Driver" + depends on MACH_LOONGSON64 help This option adds a CPUFreq driver for Loongson processors which support software configurable cpu frequency. From 290814d3c22b300eb36b74dc76ad36bd7fdc1220 Mon Sep 17 00:00:00 2001 From: Huacai Chen Date: Fri, 28 Aug 2026 11:11:58 +0800 Subject: [PATCH 86/89] cpufreq: loongson3: Adjust the width of id and val in smc_message The id field of smc_message is usually stand for the CPU ID. In the past this driver was only tested for desktop and laptop productions so 4-bits are enough. But now we have servers that can have as many as 256 cores, so we need 8-bits CPU ID. On the other hand, the val field is usually stand for CPU frequency so 12-bits are enough to represent 4GHz, and for higher frequencies there is an extra bit to extend (enough for 8GHz). In theory, this is a incompatible change, but fortunately the old SMC firmwares are not widely shipped and can be updated on the air, thus we can safely adjust the widths. Cc: stable@vger.kernel.org Fixes: ccf51454145b ("cpufreq: Add Loongson-3 CPUFreq driver support") Signed-off-by: Hongliang Wang Signed-off-by: Huacai Chen Reviewed-by: Zhongqiu Han Signed-off-by: Viresh Kumar --- drivers/cpufreq/loongson3_cpufreq.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/cpufreq/loongson3_cpufreq.c b/drivers/cpufreq/loongson3_cpufreq.c index 1e8715ea1b77..630f679aa739 100644 --- a/drivers/cpufreq/loongson3_cpufreq.c +++ b/drivers/cpufreq/loongson3_cpufreq.c @@ -21,9 +21,9 @@ union smc_message { u32 value; struct { - u32 id : 4; + u32 id : 8; u32 info : 4; - u32 val : 16; + u32 val : 12; u32 cmd : 6; u32 extra : 1; u32 complete : 1; From 5d7802ada30b3101f408706b7399b0e170d2febd Mon Sep 17 00:00:00 2001 From: Huacai Chen Date: Fri, 28 Aug 2026 11:11:59 +0800 Subject: [PATCH 87/89] cpufreq: loongson3: Replace per-package mutex with per-node Our server productions (e.g. Loongson-3D6000/3E6000) can have multiple nodes in one package and SMC mailboxes are also per-node. So replace the per-package mutex with per-node one. Cc: stable@vger.kernel.org Fixes: ccf51454145b ("cpufreq: Add Loongson-3 CPUFreq driver support") Signed-off-by: Hongliang Wang Signed-off-by: Huacai Chen Reviewed-by: Zhongqiu Han Signed-off-by: Viresh Kumar --- drivers/cpufreq/loongson3_cpufreq.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/drivers/cpufreq/loongson3_cpufreq.c b/drivers/cpufreq/loongson3_cpufreq.c index 630f679aa739..e3cd78a5ab18 100644 --- a/drivers/cpufreq/loongson3_cpufreq.c +++ b/drivers/cpufreq/loongson3_cpufreq.c @@ -169,7 +169,7 @@ struct loongson3_freq_data { struct cpufreq_frequency_table table[]; }; -static struct mutex cpufreq_mutex[MAX_PACKAGES]; +static struct mutex cpufreq_mutex[MAX_NUMNODES]; static struct cpufreq_driver loongson3_cpufreq_driver; static DEFINE_PER_CPU(struct loongson3_freq_data *, freq_data); @@ -177,14 +177,14 @@ static inline int do_service_request(u32 id, u32 info, u32 cmd, u32 val, u32 ext { int retries; unsigned int cpu = raw_smp_processor_id(); - unsigned int package = cpu_data[cpu].package; + unsigned int nid = cpu_to_node(cpu); union smc_message msg, last; - mutex_lock(&cpufreq_mutex[package]); + mutex_lock(&cpufreq_mutex[nid]); last.value = iocsr_read32(LOONGARCH_IOCSR_SMCMBX); if (!last.complete) { - mutex_unlock(&cpufreq_mutex[package]); + mutex_unlock(&cpufreq_mutex[nid]); return -EPERM; } @@ -208,11 +208,11 @@ static inline int do_service_request(u32 id, u32 info, u32 cmd, u32 val, u32 ext } if (!msg.complete || msg.cmd != CMD_OK) { - mutex_unlock(&cpufreq_mutex[package]); + mutex_unlock(&cpufreq_mutex[nid]); return -EPERM; } - mutex_unlock(&cpufreq_mutex[package]); + mutex_unlock(&cpufreq_mutex[nid]); return msg.val; } @@ -337,7 +337,7 @@ static int loongson3_cpufreq_probe(struct platform_device *pdev) { int i, ret; - for (i = 0; i < MAX_PACKAGES; i++) { + for (i = 0; i < MAX_NUMNODES; i++) { ret = devm_mutex_init(&pdev->dev, &cpufreq_mutex[i]); if (ret) return ret; From e38360d7c62c8fa530b58905f99a4557d4d6b352 Mon Sep 17 00:00:00 2001 From: Huacai Chen Date: Fri, 28 Aug 2026 11:12:00 +0800 Subject: [PATCH 88/89] cpufreq: loongson3: Use global physical CPU ID in get/target callbacks Our server productions (e.g. Loongson-3D6000/3E6000) can have discrete global physical CPU IDs while the core ID inside the packages are always continuous. In these cases we should use global physical CPU IDs to get and set frequencies. Cc: stable@vger.kernel.org Fixes: ccf51454145b ("cpufreq: Add Loongson-3 CPUFreq driver support") Signed-off-by: Hongliang Wang Signed-off-by: Huacai Chen Reviewed-by: Zhongqiu Han Signed-off-by: Viresh Kumar --- drivers/cpufreq/loongson3_cpufreq.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/drivers/cpufreq/loongson3_cpufreq.c b/drivers/cpufreq/loongson3_cpufreq.c index e3cd78a5ab18..1ae821e265fb 100644 --- a/drivers/cpufreq/loongson3_cpufreq.c +++ b/drivers/cpufreq/loongson3_cpufreq.c @@ -16,6 +16,7 @@ #include #include #include +#include /* Message */ union smc_message { @@ -219,38 +220,40 @@ static inline int do_service_request(u32 id, u32 info, u32 cmd, u32 val, u32 ext static unsigned int loongson3_cpufreq_get(unsigned int cpu) { - int ret; + int ret, core = cpu_logical_map(cpu); - ret = do_service_request(cpu, FREQ_INFO_TYPE_FREQ, CMD_GET_FREQ_INFO, 0, 0); + ret = do_service_request(core, FREQ_INFO_TYPE_FREQ, CMD_GET_FREQ_INFO, 0, 0); - return ret * KILO; + return (ret < 0) ? 0 : ret * KILO; } static int loongson3_cpufreq_target(struct cpufreq_policy *policy, unsigned int index) { - int ret; + int ret, core = cpu_logical_map(policy->cpu); - ret = do_service_request(cpu_data[policy->cpu].core, - FREQ_INFO_TYPE_LEVEL, CMD_SET_FREQ_INFO, index, 0); + ret = do_service_request(core, FREQ_INFO_TYPE_LEVEL, CMD_SET_FREQ_INFO, + index, 0); return (ret >= 0) ? 0 : ret; } static int configure_freq_table(int cpu) { - int i, ret, boost_level, max_level, freq_level; + int i, ret, core, boost_level, max_level, freq_level; struct platform_device *pdev = cpufreq_get_driver_data(); struct loongson3_freq_data *data; if (per_cpu(freq_data, cpu)) return 0; - ret = do_service_request(cpu, 0, CMD_GET_FREQ_LEVEL_NUM, 0, 0); + core = cpu_logical_map(cpu); + + ret = do_service_request(core, 0, CMD_GET_FREQ_LEVEL_NUM, 0, 0); if (ret < 0) return ret; max_level = ret; - ret = do_service_request(cpu, 0, CMD_GET_FREQ_BOOST_LEVEL, 0, 0); + ret = do_service_request(core, 0, CMD_GET_FREQ_BOOST_LEVEL, 0, 0); if (ret < 0) return ret; boost_level = ret; @@ -263,7 +266,7 @@ static int configure_freq_table(int cpu) data->def_freq_level = boost_level - 1; for (i = 0; i < freq_level; i++) { - ret = do_service_request(cpu, FREQ_INFO_TYPE_FREQ, CMD_GET_FREQ_LEVEL_INFO, i, 0); + ret = do_service_request(core, FREQ_INFO_TYPE_FREQ, CMD_GET_FREQ_LEVEL_INFO, i, 0); if (ret < 0) { devm_kfree(&pdev->dev, data); return ret; From 81792daacfb41ca07b04bd3956ea9e672080cc98 Mon Sep 17 00:00:00 2001 From: Huacai Chen Date: Fri, 28 Aug 2026 11:12:01 +0800 Subject: [PATCH 89/89] cpufreq: loongson3: Replace IOCSR read/write with MMIO ones Our server productions (e.g. Loongson-3D6000/3E6000) can have multiple nodes in one package and SMC mailboxes are also per-node. However, IOCSR read/write can only perform on the current node, while sometimes we want to perform on other nodes (e.g. when switch governor, the get and target callbacks are not run on target core). So replace IOCSR read/write with MMIO ones. Cc: stable@vger.kernel.org Fixes: ccf51454145b ("cpufreq: Add Loongson-3 CPUFreq driver support") Tested-by: Xi Ruoyao Signed-off-by: Huacai Chen Reviewed-by: Zhongqiu Han Signed-off-by: Viresh Kumar --- drivers/cpufreq/loongson3_cpufreq.c | 34 +++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/drivers/cpufreq/loongson3_cpufreq.c b/drivers/cpufreq/loongson3_cpufreq.c index 1ae821e265fb..aea7c31e155a 100644 --- a/drivers/cpufreq/loongson3_cpufreq.c +++ b/drivers/cpufreq/loongson3_cpufreq.c @@ -17,6 +17,7 @@ #include #include #include +#include /* Message */ union smc_message { @@ -165,6 +166,12 @@ union smc_message { #define FREQ_MAX_LEVEL 16 +#define MMIO_SMCMBX(node) \ + ((void __iomem *)(nid_to_addrbase(node) | IO_BASE | LOONGSON_REG_BASE | LOONGARCH_IOCSR_SMCMBX)) + +#define MMIO_MISC_FUNC(node) \ + ((void __iomem *)(nid_to_addrbase(node) | IO_BASE | LOONGSON_REG_BASE | LOONGARCH_IOCSR_MISC_FUNC)) + struct loongson3_freq_data { unsigned int def_freq_level; struct cpufreq_frequency_table table[]; @@ -177,13 +184,27 @@ static DEFINE_PER_CPU(struct loongson3_freq_data *, freq_data); static inline int do_service_request(u32 id, u32 info, u32 cmd, u32 val, u32 extra) { int retries; - unsigned int cpu = raw_smp_processor_id(); - unsigned int nid = cpu_to_node(cpu); + unsigned int cpu = 0, nid; union smc_message msg, last; +#ifdef CONFIG_SMP + switch (cmd) { + case CMD_GET_FREQ_INFO: + case CMD_SET_FREQ_INFO: + case CMD_GET_FREQ_LEVEL_NUM: + case CMD_GET_FREQ_LEVEL_INFO: + case CMD_GET_FREQ_BOOST_LEVEL: + cpu = cpu_number_map(id); + break; + default: + cpu = raw_smp_processor_id(); + } +#endif + nid = cpu_to_node(cpu); + mutex_lock(&cpufreq_mutex[nid]); - last.value = iocsr_read32(LOONGARCH_IOCSR_SMCMBX); + last.value = readl(MMIO_SMCMBX(nid)); if (!last.complete) { mutex_unlock(&cpufreq_mutex[nid]); return -EPERM; @@ -196,12 +217,11 @@ static inline int do_service_request(u32 id, u32 info, u32 cmd, u32 val, u32 ext msg.extra = extra; msg.complete = 0; - iocsr_write32(msg.value, LOONGARCH_IOCSR_SMCMBX); - iocsr_write32(iocsr_read32(LOONGARCH_IOCSR_MISC_FUNC) | IOCSR_MISC_FUNC_SOFT_INT, - LOONGARCH_IOCSR_MISC_FUNC); + writel(msg.value, MMIO_SMCMBX(nid)); + writel(readl(MMIO_MISC_FUNC(nid)) | IOCSR_MISC_FUNC_SOFT_INT, MMIO_MISC_FUNC(nid)); for (retries = 0; retries < 10000; retries++) { - msg.value = iocsr_read32(LOONGARCH_IOCSR_SMCMBX); + msg.value = readl(MMIO_SMCMBX(nid)); if (msg.complete) break;