mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs
@ 2026-09-16 16:27 Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 01/20] ACPI: CPPC: Validate the _CPC package header Christian Loehle
                   ` (20 more replies)
  0 siblings, 21 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle

This series fixes malformed _CPC handling, control-write error propagation,
PCC ownership and cleanup, CPC sysfs lifetime, register-field access,
cross-processor aliases, and Performance Limited clearing.

Series structure
================

Patches 1-7 cover parsing and control semantics, error propagation,
PCC update serialization, and descriptor/PCC cleanup. Patches 8-15
validate the register layouts and aliases which the existing accessors and
locking can safely support, and correct Performance Limited clearing.
Patch 16 bounds x86 and arm64 FFH fields before accessing hardware, checks
x86 MSR numbers, and dispatches FFH before generic Access Size decoding.
Patch 17 propagates failed cross-CPU FFH calls on arm64 and RISC-V before
using callback output, including counter reads for an offline CPU.
Patch 18 accepts a request to retain immutable Autonomous Selection as a
no-op, rejects unsupported amd-pstate transitions to passive mode before
changing state or removing the driver, and restores cppc-cpufreq's previous
mode if a later bounds update fails. The immutable-capability query is
serialized with descriptor publication and removal.
Patch 19 selects the frequency-invariance callback per CPU, avoiding
uninitialized deferred work in policies mixing PCC and direct counters.
Patch 20 creates the FIE worker when policy initialization encounters PCC
counters, before publishing callbacks. This covers offline and later-added
PCC members without making direct-counter FIE depend on worker setup.

The layout fixes are more substantial: a physical access unit can contain
several fields or be shared across processors and _PSD domains. Probe-time
interval registries enforce the assumptions of the existing
descriptor-local locking model. They add no lookup to the scheduler path;
standalone full-width SystemMemory writes remain lockless. Disjoint fields
within one descriptor retain their shared lock when their access units
overlap, including a full-width byte beside a wider partial-field access.

Behavior description
==================

The parser validates the package before indexing its entries and the
Generic Register descriptor consumed by the driver. It tolerates trailing
package/ResourceTemplate data and legacy Integer-zero absent controls.
Buffer-backed capabilities and bound readbacks are checked before narrowing
to u32. Minimum and Maximum Performance must form a usable pair, and
an explicit minimum-valid flag preserves a zero Minimum Performance
readback across cpufreq initialization and exit. Maximum Performance and
amd-pstate's unsaved firmware-minimum restore requests retain their existing
zero-means-omitted convention.

Lowest Performance must remain nonzero, as before the series. Accepting zero
needs separate changes to consumers which assume a zero frequency origin
or use a zero capability as an absence sentinel. This does not change the
explicit zero Minimum Performance update used to remove a lower bound.
Invalid firmware-provided Lowest/Nominal Frequency values remain an error.

PCC ownership is acquired before staging payload updates. Direct-only,
batched PCC, and mixed-address-space performance updates retain their
separate ordering requirements. PCC lifecycle locking and payload-copy
serialization protect different state.

Frequency-invariance policy teardown remembers which PCC work was
initialized, so it can drain that work after processor removal unpublishes
the CPC descriptor. Callback selection uses that same per-CPU initialization
state rather than looking up the potentially removed descriptor again.
Worker creation is serialized and occurs only when initializing PCC
counters, before that policy publishes callbacks. Setup failure leaves
that policy without FIE, but does not disable direct-counter or already
active policies. Direct-only systems create no worker or deadline
reservation. Direct-counter CPUs continue updating in the tick.
This does not solve the separate runtime-accessor lifetime races with
processor removal (which I will post in a later series).

SystemMemory permits read-only overlaps, supported exact writable aliases,
and safe descriptor-local partial writes. A full-width writer may share a
larger read access unit across descriptors when their logical fields are
disjoint. Only controls written by the CPPC library count as competing
writers (i.e. OSPM Nominal Performance is not written). Its write-only
readback protections still apply. The validation rejects cross-descriptor
partial writers which its locks cannot serialize and accesses which replay
a write-only neighbour's undefined readback. SystemIO permits read-only
aliases with write-only controls, but not with readable controls whose
readback would select the wrong register. Unsupported optional controls are
disabled only where doing so cannot silently change the operating mode.
Present but inaccessible Enable or Autonomous Selection controls fail
probe, including sub-byte PCC forms which the existing writer cannot
program. This may reject firmware-enabled configurations that previously
worked until the driver attempted an unsupported control access.
Optional-writer fallback only changes unpublished descriptors. A conflict
with an already-published writer can still fail probe; making recovery
independent of CPU discovery order requires runtime quiescence support.

Performance Limited is sticky, but ACPI does not define the effect of
writing one. A selective clear can either replay stale status or set a bit
on a plain read/write implementation. Write 0x3 to the perf_limited
attribute to clear both bits with one literal-zero register write;
selective clears return -EOPNOTSUPP. Partial SystemMemory/SystemIO status
fields remain readable but cannot be cleared generically. This is a status
acknowledgement, not a lossless event log.

Inaccessible status ranges remain visible to overlap validation without
being accessed. Sumit Gupta's NVIDIA T41 quirk widens only the verified
two-bit, offset-zero DWord descriptor whose remaining bits are unimplemented,
allowing that platform to use the safe clear-all operation.

The general SystemMemory/SystemIO alignment checks retain the x86 exception
for unaligned accesses. Performance Limited currently inherits that policy.
This is not a guarantee of an indivisible device transaction at an unaligned
address; the stricter status-atomicity question remains separate work. No
torn access or affected platform has been demonstrated by the reviews.

Testing
=======

An earlier iteration was built and booted on Orion O6-01, with CPPC sysfs,
frequency changes, advancing counters and module reloads checked. The
subsequent AMD transition guard and FIE fixes have passed source/model
checks and checkpatch, but this tip has not yet been built or booted.
Those checks are not AMD hardware, mixed-PCC hotplug or concurrency tests.

Changes since v6
================

Patch 1: Validate the _CPC package header
  Unchanged from v6.

Patch 2: Validate _CPC entry and control semantics
  Allow the full 64-bit Integer Counter Wraparound Time and emit FW_BUG
  once for tolerated legacy Integer-zero Buffer placeholders. (Rafael)
  Drop v6's zero Lowest Performance support: nonzero frequency anchors do
  not make it safe for all consumers. Retain the pre-series nonzero check.
  Reject Minimum/Maximum readbacks above U32_MAX before narrowing them.
  Keep only min_perf_valid: an explicit zero minimum removes a lower bound,
  whereas zero Maximum Performance retains its legacy omitted-update
  meaning. Document the flag and leave it clear for amd-pstate's unsaved
  firmware-minimum restore requests.

Patch 3: Propagate performance-control write errors
  Adjust for patch 2's minimum-only validity flag; a zero maximum remains
  an omitted update.

Patch 4: Serialize PCC single-register payload updates (v6 patch 5)
  Reject unsupported PCC widths before taking the exclusive lock, so a
  malformed request cannot abort an older valid performance batch.
  Match both subspace and generation when completing failed PCC writes;
  equal generation numbers in another subspace must not receive the error.

Patch 5: Serialize PCC EPP payload updates (v6 patch 6)
  Preflight PCC widths and SystemIO writer geometry before changing any
  direct control. This keeps the patch independently safe before the later
  probe-time layout validation. Pass the subspace to error completion.
  Patches 10 and 11 remove these preflights once probe guarantees them.

Patch 6: Release CPC descriptors through kobject (v6 patch 7)
  Track initialized PCC frequency-invariance work independently of the CPC
  descriptor, so policy exit still drains it after descriptor unpublication.
  Make the counter-transport query tolerate a missing descriptor. Broader
  runtime-accessor lifetime protection remains deferred.

Patch 7: Release PCC data after probe failures (v6 patch 8)
  No functional changes from v6. Only clarify the channel-reuse comment.

Patch 8: Reject unsafe cross-CPU SystemMemory RMW (v6 patch 9)
  Mark successful relaxed MMIO writes pending for lock-handoff ordering.
  Use the interval walk to lock both same-descriptor overlapping accesses,
  including mixed-width full/partial fields, without another overlap pass.
  Permit a full-width writer beside a disjoint logical read-only field,
  including across descriptors when the reader uses a larger access unit.
  Do not count OSPM Nominal Performance as a competing writer: Linux does
  not write it. Its write-only readback protections remain in patch 9.
  Exact-alias coalescing and representative promotion were already in v6
  and remain present; they are not additions in this version.

Patch 9: Reject direct reads of write-only controls (v6 patch 10)
  Also reject readable SystemMemory fields whose logical bits overlap a
  write-only control, including retained inaccessible fields. Treat a
  retained zero-width field conservatively before the no-writer shortcut.
  Reuse the logical-field overlap check instead of a separate full-width
  ownership helper. Remove the duplicate Desired Performance getter check;
  the common getter already enforces write-only semantics.

Patch 10: Validate and access PCC register layouts (v6 patch 11)
  Fail probe for a present inaccessible Autonomous Selection control,
  instead of hiding it without establishing the hardware's operating mode.
  Separate the final bound-pair check from mandatory-control validation.
  Remove setter PCC-width preflights once probe validates every published
  writer, and remove unreachable PCC branches in the memory accessors.
  Keep actual access-error handling and ownership serialization.

Patch 11: Validate SystemIO register layouts (v6 patch 12)
  Allow a read-only port alias with a write-only control, but reject aliases
  with readable controls whose readback would select the wrong register.
  Remove the EPP setter's duplicate SystemIO geometry preflight once probe
  enforces it. Read-only partial-field support and the x86 exception to
  natural alignment were already in v6 and remain unchanged.

Patch 12: Validate PCC overlaps across processors (v6 patch 13)
  Replace the separate same-descriptor overlap pass with registry checks.
  Drop v6's expansion to additional byte-multiple PCC writer widths; retain
  the existing 8/16/32/64-bit widths, subject to control-specific limits.
  Keep one interval record per descriptor entry; PCC alias coalescing is
  deferred. Correct Fixes to 80b8286aeec0 (CPPC request batching).

Patch 13: Validate SystemIO overlaps across processors (v6 patch 14)
  Apply patch 11's direction-aware alias policy across processors and
  remove the now-redundant same-descriptor overlap pass. Retain per-entry
  interval records; SystemIO alias coalescing remains deferred.

Patch 14: Clear Performance Limited without a stale read (v6 patch 15)
  Stop assuming write-zero-to-clear semantics for written ones. Reject
  selective clears and implement clear-all (sysfs 0x3) with one literal-zero
  register write, avoiding both stale readback and fabricated status. (Sumit)
  Extend the readable-but-not-clearable fallback to partial SystemIO fields.
  Extend inaccessible-range retention to PCC and SystemIO, including status
  crossing the PCC payload end or port 0xffff. PCC claims retain their own
  subspace without selecting or retaining an unnecessary access channel.
  Permit disabling only unpublished, disjoint optional SystemMemory writers
  which share a status access unit; never hide a required or Autonomous
  Selection control. Published writers are not changed during another probe.
  Compare bit geometry as well as byte ranges for exact PCC/SystemIO aliases,
  and count only accessible controls as writers in those registries.
  Make status fallback warnings once-only and document the clear-all and
  unsupported-access behavior in the sysfs ABI.

Patch 15: Keep Performance Limited clearable on NVIDIA T41 (new)
  Add Sumit's quirk for the verified two-bit, offset-zero DWord layout.
  Use a cached DSDT match and quirk flags, applying the fixup to the private
  GAS copy before validation and mapping. Distinguish lookup failures from
  non-matches and leave failures retryable. Keep all quirk plumbing here,
  so patch 14 remains independently buildable without unused declarations.

Patch 16: Validate FFH register fields before hardware access (new)
  Reject invalid x86 field geometry and GAS addresses that would truncate
  to another 32-bit MSR number. Bound arm64 AMU fields in both single and
  paired readers. Dispatch FFH before generic Access Size decoding can
  shift by an invalid amount using an architecture-specific field.

Patch 17: Propagate errors from cross-CPU FFH calls (new)
  Return failed SMP-call errors on arm64 and RISC-V before consuming
  uninitialized callback output. Also check RISC-V callback errors before
  copying a read value. Keep this as a separate backportable error-path fix.

Patch 18: Accept requests to retain immutable autonomous selection (new)
  Treat setting immutable Autonomous Selection Integer 1 to one as a
  successful no-op; disabling it still fails. Distinguish immutable-one
  descriptors from writable controls currently reading one. Reject an
  unsupported amd-pstate transition to passive before changing the mode or
  unregistering the working driver, checking known offline CPUs too.
  Serialize the immutable query with descriptor publication/removal,
  including probe failure, since CPU-hotplug locking does not pin it.
  On a later bounds-update failure, restore cppc-cpufreq's previous mode
  rather than always disabling selection.

Patch 19: Select the frequency-invariance callback per CPU (new)
  Register the PCC callback only on CPUs with PCC counters. A shared policy
  can also contain direct-counter CPUs, whose irq_work is not initialized.
  Select from the recorded work-initialization state, so descriptor removal
  between initialization and publication cannot change the callback choice.
  Keep callback registration after the complete counter-initialization
  pass, so an online CPU's failed initial read publishes no CPPC callbacks.

Patch 20: Create the FIE worker before enabling PCC callbacks (new)
  Create the worker when initializing PCC counters, rather than relying
  on an online-only startup scan. Serialize creation and reuse it until
  driver teardown. Offline members and later PCC policies are covered.
  On setup failure, skip FIE for that policy without changing the global
  setting or affecting existing policies. Direct-only systems do not
  allocate an unused worker or consume deadline admission bandwidth.

Dropped v6 patch 4: Use 64-bit masks for register fields
  All supported CPPC configurations are already 64-bit, so this is only
  a cleanup I'll submit later on.

Separate lifetime work
======================

The PCC mailbox teardown fixes are posted separately: [1] is their cover,
[2] frees the channel before unmapping its shared memory, and [3] serializes
channel updates with shared-memory teardown. They are not included here;
this series alone does not close the mailbox IRQ-teardown races.

[1] https://lore.kernel.org/all/20260903111328.805352-1-christian.loehle@arm.com/
[2] https://lore.kernel.org/all/20260903111328.805352-2-christian.loehle@arm.com/
[3] https://lore.kernel.org/all/20260903111328.805352-3-christian.loehle@arm.com/

Christian Loehle (19):
  ACPI: CPPC: Validate the _CPC package header
  ACPI: CPPC: Validate _CPC entry and control semantics
  ACPI: CPPC: Propagate performance-control write errors
  ACPI: CPPC: Serialize PCC single-register payload updates
  ACPI: CPPC: Serialize PCC EPP payload updates
  ACPI: CPPC: Release CPC descriptors through kobject
  ACPI: CPPC: Release PCC data after probe failures
  ACPI: CPPC: Reject unsafe cross-CPU SystemMemory RMW
  ACPI: CPPC: Reject direct reads of write-only controls
  ACPI: CPPC: Validate and access PCC register layouts
  ACPI: CPPC: Validate SystemIO register layouts
  ACPI: CPPC: Validate PCC overlaps across processors
  ACPI: CPPC: Validate SystemIO overlaps across processors
  ACPI: CPPC: Clear Performance Limited without a stale read
  ACPI: CPPC: Validate FFH register fields before hardware access
  ACPI: CPPC: Propagate errors from cross-CPU FFH calls
  ACPI: CPPC: Accept requests to retain immutable autonomous selection
  cpufreq: CPPC: Select the frequency-invariance callback per CPU
  cpufreq: CPPC: Create the FIE worker before enabling PCC callbacks

Sumit Gupta (1):
  ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41

 Documentation/ABI/testing/sysfs-devices-system-cpu |   16 +-
 arch/arm64/kernel/topology.c                       |   14 +-
 arch/x86/kernel/acpi/cppc.c                        |   14 +
 drivers/acpi/cppc_acpi.c                           | 2209 +++++++++++++++++---
 drivers/acpi/riscv/cppc.c                          |   26 +-
 drivers/acpi/utils.c                               |   13 +-
 drivers/cpufreq/amd-pstate.c                       |   31 +-
 drivers/cpufreq/cppc_cpufreq.c                     |   73 +-
 include/acpi/cppc_acpi.h                           |   18 +-
 9 files changed, 2024 insertions(+), 390 deletions(-)

base-commit: fd73f4a6659897191fa0d40695fe370925dd3780
--
2.34.1

^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 01/20] ACPI: CPPC: Validate the _CPC package header
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 02/20] ACPI: CPPC: Validate _CPC entry and control semantics Christian Loehle
                   ` (19 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
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 <christian.loehle@arm.com>
---
 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",
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 02/20] ACPI: CPPC: Validate _CPC entry and control semantics
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 01/20] ACPI: CPPC: Validate the _CPC package header Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-17 10:26   ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 03/20] ACPI: CPPC: Propagate performance-control write errors Christian Loehle
                   ` (18 subsequent siblings)
  20 siblings, 1 reply; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com
Suggested-by: Rafael J. Wysocki <rafael@kernel.org>
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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 = &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 = &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(&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(&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 {
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 03/20] ACPI: CPPC: Propagate performance-control write errors
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 01/20] ACPI: CPPC: Validate the _CPC package header Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 02/20] ACPI: CPPC: Validate _CPC entry and control semantics Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 04/20] ACPI: CPPC: Serialize PCC single-register payload updates Christian Loehle
                   ` (17 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian.loehle%40arm.com
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 drivers/acpi/cppc_acpi.c | 263 ++++++++++++++++++++++++++++++---------
 1 file changed, 203 insertions(+), 60 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(&reg->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;
 
-	/*
-	 * 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
-	 */
-	if (regs_in_pcc) {
+	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];
-		down_read(&pcc_ss_data->pcc_lock); /* BEGIN Phase-I */
+		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) {
-				up_read(&pcc_ss_data->pcc_lock);
+			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;
-			}
 		}
-		/*
-		 * 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 (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;
 	}
 
-	if (CPC_SUPPORTED(desired_reg))
-		cpc_write(cpu, desired_reg, perf_ctrls->desired_perf);
+	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;
+	}
 
-	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);
+	/*
+	 * 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.
+	 */
+	if (desired_pcc) {
+		ret = cpc_write(cpu, desired_reg, perf_ctrls->desired_perf);
+		if (ret)
+			goto out_pcc_read_unlock;
+	}
 
-	if (regs_in_pcc)
-		up_read(&pcc_ss_data->pcc_lock);	/* END Phase-I */
+	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;
+	}
+
+	/* 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);
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 04/20] ACPI: CPPC: Serialize PCC single-register payload updates
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (2 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 03/20] ACPI: CPPC: Propagate performance-control write errors Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 05/20] ACPI: CPPC: Serialize PCC EPP " Christian Loehle
                   ` (16 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
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 <christian.loehle@arm.com>
---
 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;
 	}
-
-	ret = cpc_write(cpu, reg, val);
-	if (ret)
-		return ret;
+	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)
+		goto out;
+
 	/* 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;
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 05/20] ACPI: CPPC: Serialize PCC EPP payload updates
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (3 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 04/20] ACPI: CPPC: Serialize PCC single-register payload updates Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 06/20] ACPI: CPPC: Release CPC descriptors through kobject Christian Loehle
                   ` (15 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
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 <christian.loehle@arm.com>
---
 drivers/acpi/cppc_acpi.c | 100 +++++++++++++++++++++++++++------------
 1 file changed, 70 insertions(+), 30 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 = &reg->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 = &reg->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;
+
+	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 (CPC_IN_PCC(epp_set_reg) || CPC_IN_PCC(auto_sel_reg)) {
+	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)) {
+		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_unlock;
+
+		if (auto_sel_pcc) {
 			ret = cpc_write(cpu, auto_sel_reg, enable);
 			if (ret)
-				return ret;
+				goto out_unlock;
 		}
 
-		if (cpc_is_writable(epp_set_reg)) {
+		if (epp_pcc) {
 			ret = cpc_write(cpu, epp_set_reg, perf_ctrls->energy_perf);
 			if (ret)
-				return ret;
+				goto out_unlock;
 		}
 
-		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);
-		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 = cpc_write(cpu, auto_sel_reg, enable);
-			if (ret)
-				return ret;
-		}
 
-		if (epp_ffh_sysmem) {
-			ret = cpc_write(cpu, epp_set_reg,
-					perf_ctrls->energy_perf);
-			if (ret)
-				return ret;
-		}
+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;
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 06/20] ACPI: CPPC: Release CPC descriptors through kobject
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (4 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 05/20] ACPI: CPPC: Serialize PCC EPP " Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 07/20] ACPI: CPPC: Release PCC data after probe failures Christian Loehle
                   ` (14 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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;
 	}
 }
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 07/20] ACPI: CPPC: Release PCC data after probe failures
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (5 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 06/20] ACPI: CPPC: Release CPC descriptors through kobject Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 08/20] ACPI: CPPC: Reject unsafe cross-CPU SystemMemory RMW Christian Loehle
                   ` (13 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle

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 <christian.loehle@arm.com>
---
 drivers/acpi/cppc_acpi.c | 153 ++++++++++++++++++++++++++-------------
 1 file changed, 101 insertions(+), 52 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 <linux/delay.h>
 #include <linux/iopoll.h>
 #include <linux/ktime.h>
+#include <linux/mutex.h>
 #include <linux/rwsem.h>
 #include <linux/wait.h>
 #include <linux/topology.h>
@@ -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 (IS_ERR(pcc_chan)) {
-			pr_err("Failed to find PCC channel for subspace %d\n",
-			       pcc_ss_idx);
-			return -ENODEV;
-		}
+	if (pcc_ss_idx < 0 || pcc_ss_idx >= MAX_PCC_SUBSPACES)
+		return -EINVAL;
 
-		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;
+	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;
 
-		/* 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 (!cpc_ptr) {
+		per_cpu(cpu_pcc_subspace_idx, pr->id) = -1;
+		return;
 	}
 
-	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;
+	pcc_ss_id = per_cpu(cpu_pcc_subspace_idx, pr->id);
+	per_cpu(cpc_desc_ptr, pr->id) = NULL;
+	kobject_del(&cpc_ptr->kobj);
 
-	if (!cpc_ptr)
-		return;
+	pcc_data_put(pcc_ss_id);
+	per_cpu(cpu_pcc_subspace_idx, pr->id) = -1;
 
 	kobject_put(&cpc_ptr->kobj);
 }
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 08/20] ACPI: CPPC: Reject unsafe cross-CPU SystemMemory RMW
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (6 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 07/20] ACPI: CPPC: Release PCC data after probe failures Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 09/20] ACPI: CPPC: Reject direct reads of write-only controls Christian Loehle
                   ` (12 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
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 <christian.loehle@arm.com>
---
 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 <linux/delay.h>
+#include <linux/interval_tree_generic.h>
 #include <linux/iopoll.h>
 #include <linux/ktime.h>
+#include <linux/list.h>
 #include <linux/mutex.h>
+#include <linux/rbtree.h>
 #include <linux/rwsem.h>
 #include <linux/wait.h>
 #include <linux/topology.h>
@@ -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 = &reg->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(&reg->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 = &reg->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(reg) && CPC_IN_SYSTEM_MEMORY(reg))
+			reg->cpc_entry.use_rmw_lock =
+				cpc_sysmem_reg_needs_rmw(reg);
+	}
+}
 
-		if (!CPC_SUPPORTED(a) || !CPC_IN_SYSTEM_MEMORY(a))
+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;
 };
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 09/20] ACPI: CPPC: Reject direct reads of write-only controls
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (7 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 08/20] ACPI: CPPC: Reject unsafe cross-CPU SystemMemory RMW Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 10/20] ACPI: CPPC: Validate and access PCC register layouts Christian Loehle
                   ` (11 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
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 <christian.loehle@arm.com>
---
 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 = &reg->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 = &reg->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;
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 10/20] ACPI: CPPC: Validate and access PCC register layouts
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (8 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 09/20] ACPI: CPPC: Reject direct reads of write-only controls Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 11/20] ACPI: CPPC: Validate SystemIO " Christian Loehle
                   ` (10 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle

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 <christian.loehle@arm.com>
---
 drivers/acpi/cppc_acpi.c | 452 ++++++++++++++++++++++++++++-----------
 1 file changed, 327 insertions(+), 125 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(&reg->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 = &reg->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 = &reg->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;
-		}
-	}
+	ret = cpc_resolve_unsupported(cpc_ptr, unsupported_regs);
+	if (ret)
+		goto out_free;
+	unsupported_regs = 0;
 
-	/*
-	 * 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_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 = &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");
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 11/20] ACPI: CPPC: Validate SystemIO register layouts
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (9 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 10/20] ACPI: CPPC: Validate and access PCC register layouts Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 12/20] ACPI: CPPC: Validate PCC overlaps across processors Christian Loehle
                   ` (9 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle

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 <christian.loehle@arm.com>
---
 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 = &reg->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;
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 12/20] ACPI: CPPC: Validate PCC overlaps across processors
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (10 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 11/20] ACPI: CPPC: Validate SystemIO " Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 13/20] ACPI: CPPC: Validate SystemIO " Christian Loehle
                   ` (8 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle

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 <christian.loehle@arm.com>
---
 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;
 };
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 13/20] ACPI: CPPC: Validate SystemIO overlaps across processors
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (11 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 12/20] ACPI: CPPC: Validate PCC overlaps across processors Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:27 ` [PATCH v7 14/20] ACPI: CPPC: Clear Performance Limited without a stale read Christian Loehle
                   ` (7 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle

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 <christian.loehle@arm.com>
---
 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;
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 14/20] ACPI: CPPC: Clear Performance Limited without a stale read
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (12 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 13/20] ACPI: CPPC: Validate SystemIO " Christian Loehle
@ 2026-09-16 16:27 ` Christian Loehle
  2026-09-16 16:28 ` [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41 Christian Loehle
                   ` (6 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:27 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, Sashiko

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 <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260807111303.1062391-1-christian.loehle%40arm.com
Reported-by: Sumit Gupta <sumitg@nvidia.com>
Link: https://lore.kernel.org/lkml/d5f1ea9b-53b7-4db2-983a-b5be8e71a371@arm.com/
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 .../ABI/testing/sysfs-devices-system-cpu      |  16 +-
 drivers/acpi/cppc_acpi.c                      | 352 ++++++++++++++----
 2 files changed, 293 insertions(+), 75 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.
-
-		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
+		If firmware provides the register but Linux cannot access it
+		safely, reads return the literal string "<unsupported>".
+
+		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 = &reg->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 = &reg->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 = &reg->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 = &reg->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 = &reg->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 = &reg->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];
+
+		if (CPC_SUPPORTED(reg) && CPC_IN_PCC(reg) &&
+		    (cpc_is_readable(reg) || cpc_is_writable(reg)))
+			return true;
+	}
 
-	return cpc_reg_access_width(gas) / 8;
+	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 = &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 = &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(&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, &current_val);
-	if (ret)
-		return ret;
-
-	/* Clear the specified bits */
-	new_val = current_val & ~bits_to_clear;
+	/*
+	 * 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;
 
-	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);
 
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (13 preceding siblings ...)
  2026-09-16 16:27 ` [PATCH v7 14/20] ACPI: CPPC: Clear Performance Limited without a stale read Christian Loehle
@ 2026-09-16 16:28 ` Christian Loehle
  2026-09-17 12:59   ` Christian Loehle
  2026-09-16 16:28 ` [PATCH v7 16/20] ACPI: CPPC: Validate FFH register fields before hardware access Christian Loehle
                   ` (5 subsequent siblings)
  20 siblings, 1 reply; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:28 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle

From: Sumit Gupta <sumitg@nvidia.com>

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 <sumitg@nvidia.com>
[ Rework quirk matching and fixup placement; propagate lookup failures
  without caching them. ]
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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(&reg->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);
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 16/20] ACPI: CPPC: Validate FFH register fields before hardware access
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (14 preceding siblings ...)
  2026-09-16 16:28 ` [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41 Christian Loehle
@ 2026-09-16 16:28 ` Christian Loehle
  2026-09-16 16:28 ` [PATCH v7 17/20] ACPI: CPPC: Propagate errors from cross-CPU FFH calls Christian Loehle
                   ` (4 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:28 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, stable

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: stable@vger.kernel.org
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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 <linux/bitfield.h>
+#include <linux/limits.h>
 
 #include <acpi/cppc_acpi.h>
 #include <asm/msr.h>
@@ -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 = &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);
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 17/20] ACPI: CPPC: Propagate errors from cross-CPU FFH calls
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (15 preceding siblings ...)
  2026-09-16 16:28 ` [PATCH v7 16/20] ACPI: CPPC: Validate FFH register fields before hardware access Christian Loehle
@ 2026-09-16 16:28 ` Christian Loehle
  2026-09-16 16:28 ` [PATCH v7 18/20] ACPI: CPPC: Accept requests to retain immutable autonomous selection Christian Loehle
                   ` (3 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:28 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, stable

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: stable@vger.kernel.org
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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;
 	}
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 18/20] ACPI: CPPC: Accept requests to retain immutable autonomous selection
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (16 preceding siblings ...)
  2026-09-16 16:28 ` [PATCH v7 17/20] ACPI: CPPC: Propagate errors from cross-CPU FFH calls Christian Loehle
@ 2026-09-16 16:28 ` Christian Loehle
  2026-09-16 16:28 ` [PATCH v7 19/20] cpufreq: CPPC: Select the frequency-invariance callback per CPU Christian Loehle
                   ` (2 subsequent siblings)
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:28 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, stable

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: stable@vger.kernel.org
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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;
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 19/20] cpufreq: CPPC: Select the frequency-invariance callback per CPU
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (17 preceding siblings ...)
  2026-09-16 16:28 ` [PATCH v7 18/20] ACPI: CPPC: Accept requests to retain immutable autonomous selection Christian Loehle
@ 2026-09-16 16:28 ` Christian Loehle
  2026-09-16 16:28 ` [PATCH v7 20/20] cpufreq: CPPC: Create the FIE worker before enabling PCC callbacks Christian Loehle
  2026-09-17 18:30 ` [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Sumit Gupta
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:28 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, stable

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: stable@vger.kernel.org
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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));
+	}
 }
 
 /*
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* [PATCH v7 20/20] cpufreq: CPPC: Create the FIE worker before enabling PCC callbacks
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (18 preceding siblings ...)
  2026-09-16 16:28 ` [PATCH v7 19/20] cpufreq: CPPC: Select the frequency-invariance callback per CPU Christian Loehle
@ 2026-09-16 16:28 ` Christian Loehle
  2026-09-17 18:30 ` [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Sumit Gupta
  20 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-16 16:28 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Christian Loehle, stable

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: stable@vger.kernel.org
Signed-off-by: Christian Loehle <christian.loehle@arm.com>
---
 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 <linux/cpufreq.h>
 #include <linux/irq_work.h>
 #include <linux/kthread.h>
+#include <linux/mutex.h>
 #include <linux/time.h>
 #include <linux/vmalloc.h>
 #include <uapi/linux/sched/types.h>
@@ -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)
-- 
2.34.1


^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH v7 02/20] ACPI: CPPC: Validate _CPC entry and control semantics
  2026-09-16 16:27 ` [PATCH v7 02/20] ACPI: CPPC: Validate _CPC entry and control semantics Christian Loehle
@ 2026-09-17 10:26   ` Christian Loehle
  0 siblings, 0 replies; 26+ messages in thread
From: Christian Loehle @ 2026-09-17 10:26 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han, Sashiko

On 9/16/26 17:27, Christian Loehle wrote:
> 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 <sashiko-bot@kernel.org>
> Link: https://sashiko.dev/#/patchset/20260724134251.1632824-1-christian.loehle%40arm.com
> Suggested-by: Rafael J. Wysocki <rafael@kernel.org>
> Signed-off-by: Christian Loehle <christian.loehle@arm.com>
> ---
>  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;

Sashiko:
"Does this code accidentally omit GUARANTEED_PERF from the whitelist?
According to the ACPI specification (Section 8.4.6.1.1), the firmware can
provide Guaranteed Performance as a non-zero Integer. Because it is missing
from this switch statement, valid non-zero integer values for this register
will be rejected here, causing CPPC initialization to fail on compliant
platforms."
So first of all 8.4.6.1.1 doesn't describe the type at all.

We have
8.4.6.1 _CPC (Continuous Performance Control)
describing
GuaranteedPerformanceRegister, // Buffer (Resource Descriptor)
(i.e. no Integer)
and
Table 8.23: Continuous Performance Control Package Values
Guaranteed Performance Register Buffer
(i.e. Integer)

The 'finding' is deliberate behavior and correct.


> +	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);

Sashiko:
"Is this FW_BUG warning spurious?
The ACPI specification explicitly states that if a platform does not support
an optional register, the field must be an Integer with a value of 0 or a
Buffer containing a null register descriptor. Printing a FW_BUG warning here
incorrectly flags spec-compliant Integer 0 absent fields as firmware bugs."

This is wrong, the ACPI spec doesn't state that. No FW_BUG is printed if the platform
uses the null register descriptor (like the ACPI spec specifies in 8.4.6.1).
We tolerate Integer 0 for compatibility reasons and log it as an appropriate FW_BUG.
Also see v6 review:
https://lore.kernel.org/lkml/CAJZ5v0jp9q-ZzOKXsOVpvF0ABytHXU9qeSdGyHdhLUL-cLX4sA@mail.gmail.com/

> +			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 = &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 = &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(&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(&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;
>  	}

Sashiko:
"Can this check fail on platforms where Guaranteed Performance is provided as
an integer?
If probe succeeds, legacy code in cppc_get_perf_caps() forces the local
guaranteed value to 0 when the type is ACPI_TYPE_INTEGER. However,
CPC_SUPPORTED(guaranteed_reg) still evaluates to true, so the new bounds
check (guaranteed < low) will evaluate as (0 < low), returning -EFAULT."

(also see above)
We only accept 0 (as an absent buffer) and then CPC_SUPPORTED(guaranteed_reg)
evaluates to false.


> [snip]

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41
  2026-09-16 16:28 ` [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41 Christian Loehle
@ 2026-09-17 12:59   ` Christian Loehle
  2026-09-17 13:23     ` Rafael J. Wysocki (Intel)
  0 siblings, 1 reply; 26+ messages in thread
From: Christian Loehle @ 2026-09-17 12:59 UTC (permalink / raw)
  To: Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sumit Gupta, Sudeep Holla,
	Ionela Voinescu, zhongqiu.han

On 9/16/26 17:28, Christian Loehle wrote:
> From: Sumit Gupta <sumitg@nvidia.com>
> 
> 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 <sumitg@nvidia.com>
> [ Rework quirk matching and fixup placement; propagate lookup failures
>   without caching them. ]
> Signed-off-by: Christian Loehle <christian.loehle@arm.com>
> ---
>  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",

Sashiko:
"Will this quirk successfully match a standard ACPI table header?
The ACPI specification requires the OEM Table ID to be exactly 8 bytes long,
typically padded with trailing spaces by compliant firmware (e.g.,
"T41     ").
Looking at acpi_match_platform_list(), it compares the IDs using:
strncmp(plat->oem_table_id, hdr.oem_table_id, ACPI_OEM_TABLE_ID_SIZE)
Because "T41" is a null-terminated 3-character string, strncmp() will
compare the 4th character ('\0' from the quirk definition vs ' ' from the
ACPI table header) and immediately report a mismatch, causing the quirk to
silently fail on compliant firmware.
Should this be padded with spaces (e.g., "T41     ") to ensure it matches
the firmware's table correctly?"

non-padded "T41" matches exactly what Sumit proposed and was discussed in v6:
https://lore.kernel.org/lkml/55a5c9fa-cfd3-4000-b3cc-52c343841c9f@nvidia.com/
So once Sumit adds Tested-by: this should be fine.

>[snip]

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41
  2026-09-17 12:59   ` Christian Loehle
@ 2026-09-17 13:23     ` Rafael J. Wysocki (Intel)
  2026-09-17 18:20       ` Sumit Gupta
  0 siblings, 1 reply; 26+ messages in thread
From: Rafael J. Wysocki (Intel) @ 2026-09-17 13:23 UTC (permalink / raw)
  To: Christian Loehle, Sumit Gupta
  Cc: Rafael J . Wysocki, Viresh Kumar, linux-pm, linux-acpi,
	linux-kernel, Jie Zhan, Lifeng Zheng, Pierre Gondois,
	Sudeep Holla, Ionela Voinescu, zhongqiu.han

On Thu, Sep 17, 2026 at 2:59 PM Christian Loehle
<christian.loehle@arm.com> wrote:
>
> On 9/16/26 17:28, Christian Loehle wrote:
> > From: Sumit Gupta <sumitg@nvidia.com>
> >
> > 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 <sumitg@nvidia.com>
> > [ Rework quirk matching and fixup placement; propagate lookup failures
> >   without caching them. ]
> > Signed-off-by: Christian Loehle <christian.loehle@arm.com>
> > ---
> >  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",
>
> Sashiko:
> "Will this quirk successfully match a standard ACPI table header?
> The ACPI specification requires the OEM Table ID to be exactly 8 bytes long,
> typically padded with trailing spaces by compliant firmware (e.g.,
> "T41     ").
> Looking at acpi_match_platform_list(), it compares the IDs using:
> strncmp(plat->oem_table_id, hdr.oem_table_id, ACPI_OEM_TABLE_ID_SIZE)
> Because "T41" is a null-terminated 3-character string, strncmp() will
> compare the 4th character ('\0' from the quirk definition vs ' ' from the
> ACPI table header) and immediately report a mismatch, causing the quirk to
> silently fail on compliant firmware.
> Should this be padded with spaces (e.g., "T41     ") to ensure it matches
> the firmware's table correctly?"
>
> non-padded "T41" matches exactly what Sumit proposed and was discussed in v6:
> https://lore.kernel.org/lkml/55a5c9fa-cfd3-4000-b3cc-52c343841c9f@nvidia.com/
> So once Sumit adds Tested-by: this should be fine.

Sure, thanks!

Sumit, any concerns?

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41
  2026-09-17 13:23     ` Rafael J. Wysocki (Intel)
@ 2026-09-17 18:20       ` Sumit Gupta
  0 siblings, 0 replies; 26+ messages in thread
From: Sumit Gupta @ 2026-09-17 18:20 UTC (permalink / raw)
  To: Rafael J. Wysocki (Intel), Christian Loehle
  Cc: Viresh Kumar, linux-pm, linux-acpi, linux-kernel, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sudeep Holla, Ionela Voinescu,
	zhongqiu.han, linux-tegra



On 17/09/26 18:53, Rafael J. Wysocki (Intel) wrote:
> External email: Use caution opening links or attachments
> 
> 
> On Thu, Sep 17, 2026 at 2:59 PM Christian Loehle
> <christian.loehle@arm.com> wrote:
>>
>> On 9/16/26 17:28, Christian Loehle wrote:
>>> From: Sumit Gupta <sumitg@nvidia.com>
>>>
>>> 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 <sumitg@nvidia.com>
>>> [ Rework quirk matching and fixup placement; propagate lookup failures
>>>    without caching them. ]
>>> Signed-off-by: Christian Loehle <christian.loehle@arm.com>
>>> ---
>>>   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",
>>
>> Sashiko:
>> "Will this quirk successfully match a standard ACPI table header?
>> The ACPI specification requires the OEM Table ID to be exactly 8 bytes long,
>> typically padded with trailing spaces by compliant firmware (e.g.,
>> "T41     ").
>> Looking at acpi_match_platform_list(), it compares the IDs using:
>> strncmp(plat->oem_table_id, hdr.oem_table_id, ACPI_OEM_TABLE_ID_SIZE)
>> Because "T41" is a null-terminated 3-character string, strncmp() will
>> compare the 4th character ('\0' from the quirk definition vs ' ' from the
>> ACPI table header) and immediately report a mismatch, causing the quirk to
>> silently fail on compliant firmware.
>> Should this be padded with spaces (e.g., "T41     ") to ensure it matches
>> the firmware's table correctly?"
>>
>> non-padded "T41" matches exactly what Sumit proposed and was discussed in v6:
>> https://lore.kernel.org/lkml/55a5c9fa-cfd3-4000-b3cc-52c343841c9f@nvidia.com/
>> So once Sumit adds Tested-by: this should be fine.
> 
> Sure, thanks!
> 
> Sumit, any concerns?

Hi Rafael,

No concerns.
"T41" without space padding is correct because this firmware NUL pads
the eight-byte field:

  # od -An -tc -j 16 -N 8 /sys/firmware/acpi/tables/DSDT
    T   4   1  \0  \0  \0  \0  \0

The quirk matched, producing the following message during boot:

  ] ACPI CPPC: firmware quirk: Performance Limited owns its access unit, 
using Bit Width 32

Thanks,
Sumit


^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs
  2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
                   ` (19 preceding siblings ...)
  2026-09-16 16:28 ` [PATCH v7 20/20] cpufreq: CPPC: Create the FIE worker before enabling PCC callbacks Christian Loehle
@ 2026-09-17 18:30 ` Sumit Gupta
  20 siblings, 0 replies; 26+ messages in thread
From: Sumit Gupta @ 2026-09-17 18:30 UTC (permalink / raw)
  To: Christian Loehle, Rafael J . Wysocki, Viresh Kumar
  Cc: linux-pm, linux-acpi, linux-kernel, Len Brown, Jie Zhan,
	Lifeng Zheng, Pierre Gondois, Sudeep Holla, Ionela Voinescu,
	zhongqiu.han, linux-tegra

Hi Christian,


On 16/09/26 21:57, Christian Loehle wrote:
> External email: Use caution opening links or attachments
> 
> 
> This series fixes malformed _CPC handling, control-write error propagation,
> PCC ownership and cleanup, CPC sysfs lifetime, register-field access,
> cross-processor aliases, and Performance Limited clearing.
> 
> Series structure
> ================
> 
> Patches 1-7 cover parsing and control semantics, error propagation,
> PCC update serialization, and descriptor/PCC cleanup. Patches 8-15
> validate the register layouts and aliases which the existing accessors and
> locking can safely support, and correct Performance Limited clearing.
> Patch 16 bounds x86 and arm64 FFH fields before accessing hardware, checks
> x86 MSR numbers, and dispatches FFH before generic Access Size decoding.
> Patch 17 propagates failed cross-CPU FFH calls on arm64 and RISC-V before
> using callback output, including counter reads for an offline CPU.
> Patch 18 accepts a request to retain immutable Autonomous Selection as a
> no-op, rejects unsupported amd-pstate transitions to passive mode before
> changing state or removing the driver, and restores cppc-cpufreq's previous
> mode if a later bounds update fails. The immutable-capability query is
> serialized with descriptor publication and removal.
> Patch 19 selects the frequency-invariance callback per CPU, avoiding
> uninitialized deferred work in policies mixing PCC and direct counters.
> Patch 20 creates the FIE worker when policy initialization encounters PCC
> counters, before publishing callbacks. This covers offline and later-added
> PCC members without making direct-counter FIE depend on worker setup.
> 
> The layout fixes are more substantial: a physical access unit can contain
> several fields or be shared across processors and _PSD domains. Probe-time
> interval registries enforce the assumptions of the existing
> descriptor-local locking model. They add no lookup to the scheduler path;
> standalone full-width SystemMemory writes remain lockless. Disjoint fields
> within one descriptor retain their shared lock when their access units
> overlap, including a full-width byte beside a wider partial-field access.
> 
> Behavior description
> ==================
> 
> The parser validates the package before indexing its entries and the
> Generic Register descriptor consumed by the driver. It tolerates trailing
> package/ResourceTemplate data and legacy Integer-zero absent controls.
> Buffer-backed capabilities and bound readbacks are checked before narrowing
> to u32. Minimum and Maximum Performance must form a usable pair, and
> an explicit minimum-valid flag preserves a zero Minimum Performance
> readback across cpufreq initialization and exit. Maximum Performance and
> amd-pstate's unsaved firmware-minimum restore requests retain their existing
> zero-means-omitted convention.
> 
> Lowest Performance must remain nonzero, as before the series. Accepting zero
> needs separate changes to consumers which assume a zero frequency origin
> or use a zero capability as an absence sentinel. This does not change the
> explicit zero Minimum Performance update used to remove a lower bound.
> Invalid firmware-provided Lowest/Nominal Frequency values remain an error.
> 
> PCC ownership is acquired before staging payload updates. Direct-only,
> batched PCC, and mixed-address-space performance updates retain their
> separate ordering requirements. PCC lifecycle locking and payload-copy
> serialization protect different state.
> 
> Frequency-invariance policy teardown remembers which PCC work was
> initialized, so it can drain that work after processor removal unpublishes
> the CPC descriptor. Callback selection uses that same per-CPU initialization
> state rather than looking up the potentially removed descriptor again.
> Worker creation is serialized and occurs only when initializing PCC
> counters, before that policy publishes callbacks. Setup failure leaves
> that policy without FIE, but does not disable direct-counter or already
> active policies. Direct-only systems create no worker or deadline
> reservation. Direct-counter CPUs continue updating in the tick.
> This does not solve the separate runtime-accessor lifetime races with
> processor removal (which I will post in a later series).
> 
> SystemMemory permits read-only overlaps, supported exact writable aliases,
> and safe descriptor-local partial writes. A full-width writer may share a
> larger read access unit across descriptors when their logical fields are
> disjoint. Only controls written by the CPPC library count as competing
> writers (i.e. OSPM Nominal Performance is not written). Its write-only
> readback protections still apply. The validation rejects cross-descriptor
> partial writers which its locks cannot serialize and accesses which replay
> a write-only neighbour's undefined readback. SystemIO permits read-only
> aliases with write-only controls, but not with readable controls whose
> readback would select the wrong register. Unsupported optional controls are
> disabled only where doing so cannot silently change the operating mode.
> Present but inaccessible Enable or Autonomous Selection controls fail
> probe, including sub-byte PCC forms which the existing writer cannot
> program. This may reject firmware-enabled configurations that previously
> worked until the driver attempted an unsupported control access.
> Optional-writer fallback only changes unpublished descriptors. A conflict
> with an already-published writer can still fail probe; making recovery
> independent of CPU discovery order requires runtime quiescence support.
> 
> Performance Limited is sticky, but ACPI does not define the effect of
> writing one. A selective clear can either replay stale status or set a bit
> on a plain read/write implementation. Write 0x3 to the perf_limited
> attribute to clear both bits with one literal-zero register write;
> selective clears return -EOPNOTSUPP. Partial SystemMemory/SystemIO status
> fields remain readable but cannot be cleared generically. This is a status
> acknowledgement, not a lossless event log.
> 
> Inaccessible status ranges remain visible to overlap validation without
> being accessed. Sumit Gupta's NVIDIA T41 quirk widens only the verified
> two-bit, offset-zero DWord descriptor whose remaining bits are unimplemented,
> allowing that platform to use the safe clear-all operation.
> 
> The general SystemMemory/SystemIO alignment checks retain the x86 exception
> for unaligned accesses. Performance Limited currently inherits that policy.
> This is not a guarantee of an indivisible device transaction at an unaligned
> address; the stricter status-atomicity question remains separate work. No
> torn access or affected platform has been demonstrated by the reviews.
> 
> Testing
> =======
> 
> An earlier iteration was built and booted on Orion O6-01, with CPPC sysfs,
> frequency changes, advancing counters and module reloads checked. The
> subsequent AMD transition guard and FIE fixes have passed source/model
> checks and checkpatch, but this tip has not yet been built or booted.
> Those checks are not AMD hardware, mixed-PCC hotplug or concurrency tests.
> 
> Changes since v6
> ================
> 
> Patch 1: Validate the _CPC package header
>    Unchanged from v6.
> 
> Patch 2: Validate _CPC entry and control semantics
>    Allow the full 64-bit Integer Counter Wraparound Time and emit FW_BUG
>    once for tolerated legacy Integer-zero Buffer placeholders. (Rafael)
>    Drop v6's zero Lowest Performance support: nonzero frequency anchors do
>    not make it safe for all consumers. Retain the pre-series nonzero check.
>    Reject Minimum/Maximum readbacks above U32_MAX before narrowing them.
>    Keep only min_perf_valid: an explicit zero minimum removes a lower bound,
>    whereas zero Maximum Performance retains its legacy omitted-update
>    meaning. Document the flag and leave it clear for amd-pstate's unsaved
>    firmware-minimum restore requests.
> 
> Patch 3: Propagate performance-control write errors
>    Adjust for patch 2's minimum-only validity flag; a zero maximum remains
>    an omitted update.
> 
> Patch 4: Serialize PCC single-register payload updates (v6 patch 5)
>    Reject unsupported PCC widths before taking the exclusive lock, so a
>    malformed request cannot abort an older valid performance batch.
>    Match both subspace and generation when completing failed PCC writes;
>    equal generation numbers in another subspace must not receive the error.
> 
> Patch 5: Serialize PCC EPP payload updates (v6 patch 6)
>    Preflight PCC widths and SystemIO writer geometry before changing any
>    direct control. This keeps the patch independently safe before the later
>    probe-time layout validation. Pass the subspace to error completion.
>    Patches 10 and 11 remove these preflights once probe guarantees them.
> 
> Patch 6: Release CPC descriptors through kobject (v6 patch 7)
>    Track initialized PCC frequency-invariance work independently of the CPC
>    descriptor, so policy exit still drains it after descriptor unpublication.
>    Make the counter-transport query tolerate a missing descriptor. Broader
>    runtime-accessor lifetime protection remains deferred.
> 
> Patch 7: Release PCC data after probe failures (v6 patch 8)
>    No functional changes from v6. Only clarify the channel-reuse comment.
> 
> Patch 8: Reject unsafe cross-CPU SystemMemory RMW (v6 patch 9)
>    Mark successful relaxed MMIO writes pending for lock-handoff ordering.
>    Use the interval walk to lock both same-descriptor overlapping accesses,
>    including mixed-width full/partial fields, without another overlap pass.
>    Permit a full-width writer beside a disjoint logical read-only field,
>    including across descriptors when the reader uses a larger access unit.
>    Do not count OSPM Nominal Performance as a competing writer: Linux does
>    not write it. Its write-only readback protections remain in patch 9.
>    Exact-alias coalescing and representative promotion were already in v6
>    and remain present; they are not additions in this version.
> 
> Patch 9: Reject direct reads of write-only controls (v6 patch 10)
>    Also reject readable SystemMemory fields whose logical bits overlap a
>    write-only control, including retained inaccessible fields. Treat a
>    retained zero-width field conservatively before the no-writer shortcut.
>    Reuse the logical-field overlap check instead of a separate full-width
>    ownership helper. Remove the duplicate Desired Performance getter check;
>    the common getter already enforces write-only semantics.
> 
> Patch 10: Validate and access PCC register layouts (v6 patch 11)
>    Fail probe for a present inaccessible Autonomous Selection control,
>    instead of hiding it without establishing the hardware's operating mode.
>    Separate the final bound-pair check from mandatory-control validation.
>    Remove setter PCC-width preflights once probe validates every published
>    writer, and remove unreachable PCC branches in the memory accessors.
>    Keep actual access-error handling and ownership serialization.
> 
> Patch 11: Validate SystemIO register layouts (v6 patch 12)
>    Allow a read-only port alias with a write-only control, but reject aliases
>    with readable controls whose readback would select the wrong register.
>    Remove the EPP setter's duplicate SystemIO geometry preflight once probe
>    enforces it. Read-only partial-field support and the x86 exception to
>    natural alignment were already in v6 and remain unchanged.
> 
> Patch 12: Validate PCC overlaps across processors (v6 patch 13)
>    Replace the separate same-descriptor overlap pass with registry checks.
>    Drop v6's expansion to additional byte-multiple PCC writer widths; retain
>    the existing 8/16/32/64-bit widths, subject to control-specific limits.
>    Keep one interval record per descriptor entry; PCC alias coalescing is
>    deferred. Correct Fixes to 80b8286aeec0 (CPPC request batching).
> 
> Patch 13: Validate SystemIO overlaps across processors (v6 patch 14)
>    Apply patch 11's direction-aware alias policy across processors and
>    remove the now-redundant same-descriptor overlap pass. Retain per-entry
>    interval records; SystemIO alias coalescing remains deferred.
> 
> Patch 14: Clear Performance Limited without a stale read (v6 patch 15)
>    Stop assuming write-zero-to-clear semantics for written ones. Reject
>    selective clears and implement clear-all (sysfs 0x3) with one literal-zero
>    register write, avoiding both stale readback and fabricated status. (Sumit)
>    Extend the readable-but-not-clearable fallback to partial SystemIO fields.
>    Extend inaccessible-range retention to PCC and SystemIO, including status
>    crossing the PCC payload end or port 0xffff. PCC claims retain their own
>    subspace without selecting or retaining an unnecessary access channel.
>    Permit disabling only unpublished, disjoint optional SystemMemory writers
>    which share a status access unit; never hide a required or Autonomous
>    Selection control. Published writers are not changed during another probe.
>    Compare bit geometry as well as byte ranges for exact PCC/SystemIO aliases,
>    and count only accessible controls as writers in those registries.
>    Make status fallback warnings once-only and document the clear-all and
>    unsupported-access behavior in the sysfs ABI.
> 
> Patch 15: Keep Performance Limited clearable on NVIDIA T41 (new)
>    Add Sumit's quirk for the verified two-bit, offset-zero DWord layout.
>    Use a cached DSDT match and quirk flags, applying the fixup to the private
>    GAS copy before validation and mapping. Distinguish lookup failures from
>    non-matches and leave failures retryable. Keep all quirk plumbing here,
>    so patch 14 remains independently buildable without unused declarations.
> 
> Patch 16: Validate FFH register fields before hardware access (new)
>    Reject invalid x86 field geometry and GAS addresses that would truncate
>    to another 32-bit MSR number. Bound arm64 AMU fields in both single and
>    paired readers. Dispatch FFH before generic Access Size decoding can
>    shift by an invalid amount using an architecture-specific field.
> 
> Patch 17: Propagate errors from cross-CPU FFH calls (new)
>    Return failed SMP-call errors on arm64 and RISC-V before consuming
>    uninitialized callback output. Also check RISC-V callback errors before
>    copying a read value. Keep this as a separate backportable error-path fix.
> 
> Patch 18: Accept requests to retain immutable autonomous selection (new)
>    Treat setting immutable Autonomous Selection Integer 1 to one as a
>    successful no-op; disabling it still fails. Distinguish immutable-one
>    descriptors from writable controls currently reading one. Reject an
>    unsupported amd-pstate transition to passive before changing the mode or
>    unregistering the working driver, checking known offline CPUs too.
>    Serialize the immutable query with descriptor publication/removal,
>    including probe failure, since CPU-hotplug locking does not pin it.
>    On a later bounds-update failure, restore cppc-cpufreq's previous mode
>    rather than always disabling selection.
> 
> Patch 19: Select the frequency-invariance callback per CPU (new)
>    Register the PCC callback only on CPUs with PCC counters. A shared policy
>    can also contain direct-counter CPUs, whose irq_work is not initialized.
>    Select from the recorded work-initialization state, so descriptor removal
>    between initialization and publication cannot change the callback choice.
>    Keep callback registration after the complete counter-initialization
>    pass, so an online CPU's failed initial read publishes no CPPC callbacks.
> 
> Patch 20: Create the FIE worker before enabling PCC callbacks (new)
>    Create the worker when initializing PCC counters, rather than relying
>    on an online-only startup scan. Serialize creation and reuse it until
>    driver teardown. Offline members and later PCC policies are covered.
>    On setup failure, skip FIE for that policy without changing the global
>    setting or affecting existing policies. Direct-only systems do not
>    allocate an unused worker or consume deadline admission bandwidth.
> 
> Dropped v6 patch 4: Use 64-bit masks for register fields
>    All supported CPPC configurations are already 64-bit, so this is only
>    a cleanup I'll submit later on.
> 
> Separate lifetime work
> ======================
> 
> The PCC mailbox teardown fixes are posted separately: [1] is their cover,
> [2] frees the channel before unmapping its shared memory, and [3] serializes
> channel updates with shared-memory teardown. They are not included here;
> this series alone does not close the mailbox IRQ-teardown races.
> 
> [1] https://lore.kernel.org/all/20260903111328.805352-1-christian.loehle@arm.com/
> [2] https://lore.kernel.org/all/20260903111328.805352-2-christian.loehle@arm.com/
> [3] https://lore.kernel.org/all/20260903111328.805352-3-christian.loehle@arm.com/
> 
> Christian Loehle (19):
>    ACPI: CPPC: Validate the _CPC package header
>    ACPI: CPPC: Validate _CPC entry and control semantics
>    ACPI: CPPC: Propagate performance-control write errors
>    ACPI: CPPC: Serialize PCC single-register payload updates
>    ACPI: CPPC: Serialize PCC EPP payload updates
>    ACPI: CPPC: Release CPC descriptors through kobject
>    ACPI: CPPC: Release PCC data after probe failures
>    ACPI: CPPC: Reject unsafe cross-CPU SystemMemory RMW
>    ACPI: CPPC: Reject direct reads of write-only controls
>    ACPI: CPPC: Validate and access PCC register layouts
>    ACPI: CPPC: Validate SystemIO register layouts
>    ACPI: CPPC: Validate PCC overlaps across processors
>    ACPI: CPPC: Validate SystemIO overlaps across processors
>    ACPI: CPPC: Clear Performance Limited without a stale read
>    ACPI: CPPC: Validate FFH register fields before hardware access
>    ACPI: CPPC: Propagate errors from cross-CPU FFH calls
>    ACPI: CPPC: Accept requests to retain immutable autonomous selection
>    cpufreq: CPPC: Select the frequency-invariance callback per CPU
>    cpufreq: CPPC: Create the FIE worker before enabling PCC callbacks
> 
> Sumit Gupta (1):
>    ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41
> 
>   Documentation/ABI/testing/sysfs-devices-system-cpu |   16 +-
>   arch/arm64/kernel/topology.c                       |   14 +-
>   arch/x86/kernel/acpi/cppc.c                        |   14 +
>   drivers/acpi/cppc_acpi.c                           | 2209 +++++++++++++++++---
>   drivers/acpi/riscv/cppc.c                          |   26 +-
>   drivers/acpi/utils.c                               |   13 +-
>   drivers/cpufreq/amd-pstate.c                       |   31 +-
>   drivers/cpufreq/cppc_cpufreq.c                     |   73 +-
>   include/acpi/cppc_acpi.h                           |   18 +-
>   9 files changed, 2024 insertions(+), 390 deletions(-)
> 
> base-commit: fd73f4a6659897191fa0d40695fe370925dd3780
> --
> 2.34.1


Tested the v7 on v7.3-rc3, with both _CPC rev 3 and 4.

For the entire series:
Tested-by: Sumit Gupta <sumitg@nvidia.com>


Patch 20/20 had a conflict in drivers/cpufreq/cppc_cpufreq.c with
linux-next 20260916.

The series applied cleanly to the base commit specified in the cover
letter (fd73f4a66598, tag v7.3-rc3), so patch 20/20 may need to be
rebased for the current linux-next.

Thanks,
Sumit



^ permalink raw reply	[flat|nested] 26+ messages in thread

end of thread, other threads:[~2026-09-17 18:31 UTC | newest]

Thread overview: 26+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-16 16:27 [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Christian Loehle
2026-09-16 16:27 ` [PATCH v7 01/20] ACPI: CPPC: Validate the _CPC package header Christian Loehle
2026-09-16 16:27 ` [PATCH v7 02/20] ACPI: CPPC: Validate _CPC entry and control semantics Christian Loehle
2026-09-17 10:26   ` Christian Loehle
2026-09-16 16:27 ` [PATCH v7 03/20] ACPI: CPPC: Propagate performance-control write errors Christian Loehle
2026-09-16 16:27 ` [PATCH v7 04/20] ACPI: CPPC: Serialize PCC single-register payload updates Christian Loehle
2026-09-16 16:27 ` [PATCH v7 05/20] ACPI: CPPC: Serialize PCC EPP " Christian Loehle
2026-09-16 16:27 ` [PATCH v7 06/20] ACPI: CPPC: Release CPC descriptors through kobject Christian Loehle
2026-09-16 16:27 ` [PATCH v7 07/20] ACPI: CPPC: Release PCC data after probe failures Christian Loehle
2026-09-16 16:27 ` [PATCH v7 08/20] ACPI: CPPC: Reject unsafe cross-CPU SystemMemory RMW Christian Loehle
2026-09-16 16:27 ` [PATCH v7 09/20] ACPI: CPPC: Reject direct reads of write-only controls Christian Loehle
2026-09-16 16:27 ` [PATCH v7 10/20] ACPI: CPPC: Validate and access PCC register layouts Christian Loehle
2026-09-16 16:27 ` [PATCH v7 11/20] ACPI: CPPC: Validate SystemIO " Christian Loehle
2026-09-16 16:27 ` [PATCH v7 12/20] ACPI: CPPC: Validate PCC overlaps across processors Christian Loehle
2026-09-16 16:27 ` [PATCH v7 13/20] ACPI: CPPC: Validate SystemIO " Christian Loehle
2026-09-16 16:27 ` [PATCH v7 14/20] ACPI: CPPC: Clear Performance Limited without a stale read Christian Loehle
2026-09-16 16:28 ` [PATCH v7 15/20] ACPI: CPPC: Keep Performance Limited clearable on NVIDIA T41 Christian Loehle
2026-09-17 12:59   ` Christian Loehle
2026-09-17 13:23     ` Rafael J. Wysocki (Intel)
2026-09-17 18:20       ` Sumit Gupta
2026-09-16 16:28 ` [PATCH v7 16/20] ACPI: CPPC: Validate FFH register fields before hardware access Christian Loehle
2026-09-16 16:28 ` [PATCH v7 17/20] ACPI: CPPC: Propagate errors from cross-CPU FFH calls Christian Loehle
2026-09-16 16:28 ` [PATCH v7 18/20] ACPI: CPPC: Accept requests to retain immutable autonomous selection Christian Loehle
2026-09-16 16:28 ` [PATCH v7 19/20] cpufreq: CPPC: Select the frequency-invariance callback per CPU Christian Loehle
2026-09-16 16:28 ` [PATCH v7 20/20] cpufreq: CPPC: Create the FIE worker before enabling PCC callbacks Christian Loehle
2026-09-17 18:30 ` [PATCH v7 0/20] ACPI: CPPC: Fix register access and lifetime bugs Sumit Gupta

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®