From: 徐晗翔 <xuhx@hust.edu.cn>
To: rafael@kernel.org, lenb@kernel.org
Cc: linux-acpi@vger.kernel.org, viresh.kumar@linaro.org,
linux-pm@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: ACPI: CPPC: per-CPU cpc_desc is freed on processor unbind without clearing the published pointer, causing UAF in cpufreq accessors
Date: Tue, 10 Nov 2026 21:05:26 +0800 (GMT+08:00) [thread overview]
Message-ID: <610cc333.22ad9.1a1c5903a6b.Coremail.xuhx@hust.edu.cn> (raw)
In-Reply-To: <2026102727-data-sequel-ce41@gregkh>
[-- Attachment #1: Type: text/plain, Size: 7640 bytes --]
acpi_processor_stop() can be reached through the processor driver's
sysfs unbind attribute while the target CPU is still online. It calls
acpi_cppc_processor_exit(), which iounmap()s the _CPC system-memory
registers and kfree()s the per-CPU struct cpc_desc, but never clears
per_cpu(cpc_desc_ptr, pr->id). The cpufreq accessors (and the
acpi_cppc sysfs attributes) read that per-CPU pointer without a common
lock, refcount, or RCU grace period, so after the unbind they can
continue to use the freed descriptor. A KASAN kernel reproduced the
slab-use-after-free with a real "echo cpu0 > .../processor/unbind",
not with a synthetic direct call to the exit function.
The detailed description:
* Test environment / affected version
Linux 7.3.0 (mainline 7.3 source, unmodified), KASAN build from
verification/build-kasan-7.3, x86_64 QEMU/KVM guest, 2 vCPU,
CONFIG_KASAN=y, CONFIG_BUG=y, CONFIG_HOTPLUG_SMT=y. The guest used a
test SSDT (cpcint.aml) that adds a 21-entry, all-integer _CPC package
to \_SB.CPUS.C000/C001, so the normal acpi_cppc_processor_probe()
created a real cpc_desc and a real acpi_cppc sysfs directory.
The test kernel has CONFIG_ACPI_CPPC_LIB=y and
CONFIG_ACPI_PROCESSOR=y, but CONFIG_ACPI_CPPC_CPUFREQ is not enabled.
The concurrent accessor in the reproducer is therefore a module
calling the exported cppc_get_perf_caps(0), not a real
cppc_cpufreq tick. The release side is the real driver-core
unbind path; the accessor function and the released object are the
same as in the cpufreq/sysfs paths described below. Line numbers
are from Linux 7.3.
* Mechanism
The ACPI processor driver is bound to the cpu_subsys CPU device and
has .remove = acpi_processor_stop (drivers/acpi/processor_driver.c:
45-50). Writing a CPU name to
/sys/bus/cpu/drivers/processor/unbind runs
unbind_store() -> device_release_driver() -> acpi_processor_stop()
(drivers/base/bus.c:235-251, drivers/base/dd.c:1318-1375).
__device_release_driver() takes only the CPU device's device_lock;
it does not take a CPU-hotplug lock and does not require the CPU to
be offline. acpi_processor_stop() has no online check either and
calls acpi_cppc_processor_exit(pr) at processor_driver.c:204.
acpi_cppc_processor_exit() (drivers/acpi/cppc_acpi.c:1037-1068)
performs, in this order:
pcc_data refcount/free
-> iounmap(cpc_ptr->cpc_regs[i].sys_mem_vaddr)
-> kobject_put(&cpc_ptr->kobj)
-> kfree(cpc_ptr)
There is no corresponding
per_cpu(cpc_desc_ptr, pr->id) = NULL. The pointer was published by
acpi_cppc_processor_probe() at cppc_acpi.c:1002. cppc_ktype has no
.release callback (cppc_acpi.c:300-303), so kobject_put() only
removes the sysfs directory and drains its active references; it is
not a lifetime barrier for the descriptor.
The accessors take the pointer outside any lock:
cppc_get_reg_val(): cpc_desc = per_cpu(cpc_desc_ptr, cpu);
if (!cpc_desc) return -ENODEV;
reg = &cpc_desc->cpc_regs[reg_idx];
cppc_get_perf_caps(): same pattern (cppc_acpi.c:1488 onwards)
cppc_get_perf_ctrs(): same pattern (cppc_acpi.c:1678 onwards)
The NULL check does not help: after exit the per-CPU slot is still
non-NULL and points to freed memory.
The cpufreq policy is owned by the cpufreq core, not by the ACPI
processor driver. acpi_processor_stop() neither offlines the CPU nor
tears down the policy, so a registered CPPC frequency-invariance
source can keep calling cppc_get_perf_ctrs() after unbind
(drivers/cpufreq/cppc_cpufreq.c:69-104, 150-184). The
cpuinfo_cur_freq sysfs attribute reaches the driver's .get callback
through __cpufreq_get() (drivers/cpufreq/cpufreq.c:755-764), and the
acpi_cppc attributes call cppc_get_perf_caps() from show_cppc_data()
(cppc_acpi.c:160-186).
There is also a mapping-lifetime sub-case. For _CPC registers in
ACPI_ADR_SPACE_SYSTEM_MEMORY, cpc_read()/cpc_write() use
reg_res->sys_mem_vaddr directly (cppc_acpi.c:1162-1180 and
1235-1250), while exit iounmap()s those addresses before
kobject_put(). A sysfs show that is still running can therefore
reach readb/readw/readl/readq_relaxed() after the mapping was
revoked; an EPP store can do the same through cpc_write(). The
kernfs drain inside kobject_put() protects the later kfree(cpc_ptr),
but it starts only after iounmap(), so it does not protect the
mapping. This sub-case is source-level only in this report; the
tested guest had an all-integer _CPC and did not establish a
system-memory _CPC successfully.
A separate SMT path is worth fixing even though it was not the
reproduced one: cpuhp_smt_enable() calls _cpu_up(cpu, 0,
CPUHP_ONLINE) without taking the CPU device's device_lock
(kernel/cpu.c:2702-2725, called from the smt/control store at
2920-2940), while unbind_store() takes only that device_lock. For a
never-online SMT sibling, the first acpi_soft_cpu_online() probe can
therefore overlap the same CPU's unbind and hit the 1002-1004 window
(publish cpc_desc, then kobject_init_and_add()).
* Reproducer (attached: acpi_case1unbind.c) and results on 7.3
The attached module starts a kthread that repeatedly calls the
exported cppc_get_perf_caps(0, &caps), then the guest performs the
real unbind:
insmod /acpi_case1unbind.ko
sleep 1
echo cpu0 > /sys/bus/cpu/drivers/processor/unbind
It does not modify kernel source. It needs the test SSDT that
provides the _CPC package, and the module build uses the same
build-kasan-7.3 tree.
Actual KASAN output (trimmed):
CASE1UNBIND: unbinding cpu0
BUG: KASAN: slab-use-after-free in cppc_get_perf_caps+0x14a/0x880
Read of size 4 at addr ff110000019a6018 by task cppc_hammer_unb/73
Call Trace:
cppc_get_perf_caps+0x14a/0x880
hammer_fn+0xf5/0x110 [acpi_case1unbind]
kthread
...
Allocated by task 20:
acpi_cppc_processor_probe+0x174/0xe60
acpi_soft_cpu_online+0x90/0x190
...
Freed by task 1:
kfree+0x121/0x390
acpi_processor_stop+0x77/0x90
device_release_driver_internal+0x246/0x2e0
unbind_store+0xa9/0xb0
kernfs_fop_write_iter+0x1ef/0x290
...
CASE1UNBIND: unbind returned 0
The allocation stack is the normal probe path, and the free stack is
the real sysfs unbind path. This confirms that the remove callback
ran while CPU0 was online and that the published per-CPU pointer
survived the free. The module is an accessor stand-in: it does not
prove the scheduler-tick timing or the system-memory iounmap
sub-case, and the test kernel did not have cppc_cpufreq enabled.
The core "exit frees, published pointer remains, accessor reads"
lifetime is nevertheless demonstrated end to end.
* Suggested direction
Clear per_cpu(cpc_desc_ptr, pr->id) before releasing the object, and
give accessors a real lifetime rule: an RCU/refcount-protected
lookup, or a quiescence point that stops the cpufreq policy/FIE and
waits for in-flight accessors before iounmap()/kfree(). The remove
path should also refuse to run while the CPU is online (or offline
and stop cpufreq first). For the SMT path, make the first-online
probe and driver remove share a lock, or exclude remove during the
probe window. If sysfs mappings remain reachable during removal,
move iounmap() after the kernfs drain or protect the vaddr with the
same lifetime mechanism. Report-first, no patch attached; happy to
test any proposal.
Please keep me in Cc; I am not subscribed to the lists.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: acpi_case1unbind.c --]
[-- Type: text/plain, Size: 968 bytes --]
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/acpi.h>
#include <acpi/cppc_acpi.h>
static struct task_struct *hammer;
static int hammer_fn(void *data)
{
struct cppc_perf_caps caps;
unsigned long n = 0;
while (!kthread_should_stop()) {
memset(&caps, 0, sizeof(caps));
cppc_get_perf_caps(0, &caps);
n++;
if (!(n & 0x3ff))
cond_resched();
}
pr_info("ACPI_CASE1UNBIND: hammer iterations=%lu\n", n);
return 0;
}
static int __init case1unbind_init(void)
{
hammer = kthread_run(hammer_fn, NULL, "cppc_hammer_unbind");
if (IS_ERR(hammer)) {
pr_err("ACPI_CASE1UNBIND: kthread_run failed\n");
return PTR_ERR(hammer);
}
pr_info("ACPI_CASE1UNBIND: hammer started; unbind cpu0 now\n");
return 0;
}
static void __exit case1unbind_exit(void)
{
if (hammer)
kthread_stop(hammer);
}
module_init(case1unbind_init);
module_exit(case1unbind_exit);
MODULE_LICENSE("GPL");
next prev parent reply other threads:[~2026-11-10 13:05 UTC|newest]
Thread overview: 32+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 12:35 usb: devio: usbdev_release() has no barrier against an in-flight async_completed() callback (use-after-free window on struct usb_dev_state)gregkh@linuxfoundation.org 徐晗翔
2026-10-27 14:12 ` Greg KH
2026-11-03 11:21 ` mm/slub: has_pcs_used() reads remote per-CPU sheaf state from flush_all_cpus_locked() without synchronization (data race; KCSAN blind spot: slub.o is uninstrumented) 徐晗翔
2026-11-04 9:41 ` Qing Wang
2026-11-04 10:07 ` Qing Wang
2026-11-03 11:30 ` futex/hrtimer: hrtimer_sleeper::task is read unsynchronized in 16 places on v7.3 -- the accessor series that fixes this is not in 7.3; please make sure it reaches the affected trees (backport nudge + stock-kernel KCSAN evidence) 徐晗翔
2026-11-03 11:48 ` cgroup: fork vs cgroup.procs write race in favordynmods mode -- v7.3 is affected; please make sure bf6fbf46b5ff reaches the stable trees (repro incl. uninstrumented natural hit) 徐晗翔
2026-11-03 14:51 ` Sasha Levin
2026-11-03 11:57 ` net: rtnetlink: rtnl_link_unregister() vs in-flight RTM_GETLINK dump readers -- v7.3 lacks the synchronize_rcu() fix (Fixes: 6b57ff21a310 family); A/B reproduction on stock 7.3 徐晗翔
2026-11-03 15:49 ` Eric Dumazet
2026-11-04 15:03 ` Sasha Levin
2026-11-03 12:07 ` wifi: wilc1000: SDIO/SPI remove destroys the srcu_struct before wiphy_unregister() -- late cfg80211 callbacks write through dangling srcu state (unfixed upstream; controlled + natural Oops) 徐晗翔
2026-11-03 12:10 ` gpio: gpiolib: final put of the gpio_device happens inside the SRCU read critical section on the setup-failure path -- deterministic use-after-free read at guard release (unfixed upstream) 徐晗翔
2026-11-05 8:52 ` Linus Walleij
2026-11-04 12:54 ` soc: qcom: pmic_glink_altmode: fix use-after-free on unbind (no cancel_work_sync for port work and enable_work) 徐晗翔
2026-11-04 13:04 ` Greg KH
2026-11-04 12:57 ` usb: typec: anx7411: unregister partner/altmodes before stopping the IRQ work -- use-after-free, double unregister, orphan partner on remove 徐晗翔
2026-11-04 13:28 ` Greg KH
2026-11-04 12:58 ` usb: typec: ucsi: deadlock on cros_ec unbind -- connector lock held across cancel_delayed_work_sync vs the mode-selection work's unbounded trylock polling 徐晗翔
2026-11-04 13:00 ` usb: typec: stusb160x/rt1719/hd3ss3220: IRQ thread vs remove -- double unregister / use-after-free on the partner and port 徐晗翔
2026-11-04 13:01 ` usb: typec: ucsi: displayport: data race on dp->header/ vdo_data/vdo_size -- work reads them unlocked while writers hold con->lock (VDM mismatch) 徐晗翔
2026-11-04 13:02 ` usb: typec: class: three lifetime/locking gaps around the mux/switch handles and mode_selection (drvdata UAF after provider unbind; no concurrent protocol for set/put; unlocked state_update vs delete) 徐晗翔
2026-11-10 12:56 ` driver core: uevent: netns teardown frees uevent_sock while a tagged uevent is still being broadcast (UAF) 徐晗翔
2026-11-10 13:00 ` 徐晗翔
2026-11-10 13:05 ` 徐晗翔 [this message]
2026-11-10 13:07 ` ACPI: CPPC: shared PCC subspace data can be freed while another CPU probes, reads, or tears it down 徐晗翔
2026-11-10 13:11 ` ACPI: ACPICA: Reset can delete an event semaphore while another thread is blocked in Wait 徐晗翔
2026-11-10 13:14 ` power: supply: bq25890: pump-express work can call power_supply_changed after the psy is unregistered 徐晗翔
2026-11-10 13:17 ` power: supply: core: get/set property callbacks can run after unregister and read freed driver data 徐晗翔
2026-11-10 13:18 ` power: supply: core: system-battery enumeration can return an unregistered battery 徐晗翔
2026-11-10 13:21 ` power: supply: core: changed work can run after unregister and use freed LED triggers 徐晗翔
2026-11-10 13:23 ` power: supply: bq24190: IRQ handler can call power_supply_changed after the psy is unregistered 徐晗翔
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=610cc333.22ad9.1a1c5903a6b.Coremail.xuhx@hust.edu.cn \
--to=xuhx@hust.edu.cn \
--cc=lenb@kernel.org \
--cc=linux-acpi@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-pm@vger.kernel.org \
--cc=rafael@kernel.org \
--cc=viresh.kumar@linaro.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®