* perf/uncore: activeboxes count TOCTOU lets PCI probe/remove run perf_pmu_register() and perf_pmu_unregister() concurrently on the same struct pmu
@ 2026-10-22 13:58 徐晗翔
2026-10-27 17:56 ` Chen, Zide
0 siblings, 1 reply; 2+ messages in thread
From: 徐晗翔 @ 2026-10-22 13:58 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung; +Cc: linux-perf-users, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 7379 bytes --]
The uncore PCI driver decides "first active box registers the PMU" and
"last active box unregisters the PMU" with nothing but the
pmu->activeboxes counter -- the decision and the perf_pmu_register()/
perf_pmu_unregister() execution are not serialized -- so on a multi-die
platform whose same-typed boxes live on different dies, concurrently
removing the last box of one die and probing a box of another die runs
register and unregister on the SAME struct pmu at the same time; the
unregister side's unlocked tail (two grace periods, then idr_remove()
re-reading pmu->type and perf_pmu_free() reading pmu->dev and
pmu->cpu_pmu_context) then consumes whatever the concurrent register
just published, and the kernel oopses or leaves a PMU in the list
holding freed per-CPU contexts. The perf-core half of this is
confirmed dynamically on mainline 7.3 (first concurrency round:
page fault oops inside perf_pmu_unregister); the uncore-side trigger
is source-proven on 7.3's current (recently refactored) code, which
added box refcounting and a PMU_BROKEN state but still no
serialization of this decision path.
Both decision sites are counter-only on 7.3 (arch/x86/events/intel/
uncore.c): uncore_box_setup() at :1153 skips registration when
atomic_inc_return(&pmu->activeboxes) > 1 (:1166), otherwise calls
uncore_pmu_register() (:926); uncore_pci_pmu_unregister() at :1283
calls uncore_pmu_unregister() (:960) when atomic_dec_return(...) == 0
(:1291). Meanwhile the perf core is asymmetric: perf_pmu_register()
(kernel/events/core.c:12836) holds pmus_lock for its whole body
(guard at :12841), while perf_pmu_unregister() (:13023) holds it only
for the idr_cmpxchg/list_del section (:13025-13029) and for the final
idr_remove (:13056-13057) -- between and after them the two
synchronize_srcu()/synchronize_rcu() grace periods (:13042-13043),
the idr_remove() that re-reads pmu->type, and perf_pmu_free()
(:13068, reading pmu->dev/pmu->cpu_pmu_context and freeing them) all
run unlocked for tens of milliseconds. Triggering needs a multi-die
host with id-table uncore PCI drivers (snbep/skx families) and
concurrent sysfs unbind/rescan (or a hotplug/DPC event racing an
admin operation) -- root actions, but each entirely legitimate.
The detailed description:
* Test environment / affected version
Linux version 7.3.0 (gcc 13.3.0, GNU ld 2.42) #1 SMP PREEMPT_DYNAMIC
-- vanilla mainline 7.3 release tarball (kernel.org, unmodified),
rebuilt with CONFIG_PERF_EVENTS=y, CONFIG_PERF_EVENTS_INTEL_UNCORE=y,
KASAN enabled, QEMU/KVM x86_64 guest. The perf-core functions
cited are byte-identical in 7.1.5 and 7.3; the uncore functions were
refactored during 7.3 (box refcounting, PMU_BROKEN stickiness,
WARN_ON_ONCE against double probe at :1206) -- the refactor did NOT
add any lock (the file's lock inventory is unchanged), and the
counter-only decision shape survives it. Line numbers below are
from 7.3.
* Mechanism
1. Counter-only decisions, no lock between decision and execution:
uncore_box_setup() /* uncore.c:1153 */
...
if (atomic_inc_return(&pmu->activeboxes) > 1) /* :1166 */
return 0;
ret = uncore_pmu_register(pmu); /* first
box */
uncore_pci_pmu_unregister() /* uncore.c:1283 */
pmu->boxes[die] = NULL;
if (atomic_dec_return(&pmu->activeboxes) == 0) /* :1291 */
uncore_pmu_unregister(pmu); /* last
box */
Two different PCI devices' probe and remove callbacks (per-device
locks only; sysfs bind/unbind/rescan are not mutually exclusive)
can hold both decisions simultaneously: remove drops die0's last
box to 0 and decides "unregister" while probe brings die1's box
from 0 to 1 and decides "register" -- same struct intel_uncore_pmu
(type->pmus[] is shared across dies for id-table types).
2. The register side rewrites the shared struct pmu from scratch:
uncore_pmu_register() (:926) assigns the whole template
(clearing ->dev, ->type, ->cpu_pmu_context) before calling
perf_pmu_register(), which then creates a NEW device, allocates a
NEW type id and a NEW percpu context array, and publishes them.
3. The unregister side's unlocked tail reads what register just
published. With A = remove(die0) and B = probe(die1):
A: perf_pmu_unregister() locks pmus_lock, idr_cmpxchg + list_del
(:13025-13029), UNLOCKS, enters two grace periods
(:13042-13043) -- tens of ms
B: uncore_pmu_register() resets the struct, perf_pmu_register()
(holding pmus_lock the whole time) creates device D2, type
T2, contexts P2; INIT event list; publishes; returns 0
A: resumes: idr_remove(&pmu_idr, pmu->type) /* :13056-13057,
re-reads pmu->type == T2 -> deletes B's fresh entry */
perf_pmu_free(pmu) /* :13068: reads
pmu->dev == D2 -> device_del/put; reads pmu->cpu_pmu_context
== P2 -> kfrees B's fresh contexts + free_percpu */
End states: a PMU still on the pmus list whose IDR entry is gone
and whose contexts are freed (later perf_event_open() reaching
find_get_pmu_context() walks freed memory), or the mirror
interleaving where A's cmpxchg consumes B's registration wholesale
(the active PMU vanishes; event creation fails with ENODEV until
rebind). uncore_pmu_unregister() (:960-967) also ignores
perf_pmu_unregister()'s return value, so the -EINVAL variant
leaves a half-torn state machine in place.
* Evidence (7.3)
- Source-level, uncore side: the decision paths above contain no
serialization on current 7.3 (verified function-by-function; the
7.3 refactor touched exactly these functions without adding a
lock). Real-hardware trigger: concurrent
echo <bdf0> > /sys/bus/pci/drivers/skx_uncore/unbind
echo 1 > /sys/bus/pci/rescan
on a multi-die host.
- Dynamic, perf-core half (reproducer attached, pmu_race.c): its
"race" action starts two kthreads faithfully mirroring both sides
-- an unregister loop and a register loop that performs the same
whole-struct reset uncore_pmu_register() does before each
perf_pmu_register(), on the same struct pmu. On the 4-vCPU KASAN
VM the FIRST concurrency round (seconds into the run) ends in:
CPU: 0 ... Comm: pmu_race_unreg
RIP: 0010:perf_pmu_unregister+0x1cb/0x620
#PF: supervisor read access ... address fffffffffffffe08
reading a pointer derived from the concurrently rewritten pmu
fields; the unregister thread dies and the race never converges.
(Same signature on 7.1.5 at +0x1d3; the code between is
identical.) Usage:
insmod pmu_race.ko
echo "race 2000" > /sys/module/pmu_race/parameters/action
echo status > /sys/module/pmu_race/parameters/action
QEMU provides no uncore PCI devices, which is why the uncore-side
trigger is presented source-level and the perf-core concurrency
(the half both sides funnel into) is what the reproducer drives.
Thanks for taking a look.
Regards,
Hanxiang Xu
xuhx@hust.edu.cn
[-- Attachment #2: pmu_race.c --]
[-- Type: text/plain, Size: 13648 bytes --]
// SPDX-License-Identifier: GPL-2.0
/*
* pmu_race.c - verification driver for perf/PMU race-condition audit cases
* (R4/R6/R8/R9) against the KASAN verification kernel.
*
* Exposes an "action" module parameter that drives perf_pmu_register /
* perf_pmu_unregister / perf_pmu_migrate_context / kernel-counter creation
* on a fake PMU named "racepmu":
*
* echo register > action : one perf_pmu_register
* echo unregister > action : one perf_pmu_unregister
* echo regloop N > action : N sequential register+unregister rounds
* (natural-window hammer, R4-A / baseline)
* echo race N > action : 2 kthreads: A=unregister loop,
* B=register loop on the same struct pmu
* (R8: activeboxes TOCTOU core, uncore-style
* template reset before each register)
* echo churn N > action : 3 kthreads: reg/unreg churn + migrate loop
* + open/close churn (R9 perf-core overlap)
* echo open N > action : N kernel counter create/release attempts
* (poisoned-state consumption)
* echo status > action : log registered state and stats
*
* The kernel source is NOT modified: everything is driven through exported
* perf core APIs (perf_pmu_register/perf_pmu_unregister are EXPORT_SYMBOL_GPL,
* perf_pmu_migrate_context and perf_event_create_kernel_counter likewise).
*/
#define pr_fmt(fmt) "PMURACE: " fmt
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/workqueue.h>
#include <linux/perf_event.h>
#include <linux/err.h>
#include <linux/string.h>
static struct pmu test_pmu;
static int my_registered;
static int test_type = -1;
/* statistics */
static unsigned long stat_reg_ok, stat_reg_fail;
static unsigned long stat_unreg_ok, stat_unreg_fail;
static unsigned long stat_open_ok, stat_open_fail;
static int open_err0, open_err1;
static int race_event_init(struct perf_event *event);
static void race_event_read(struct perf_event *event);
static int race_event_add(struct perf_event *event, int flags);
static void race_event_del(struct perf_event *event, int flags);
static void race_event_start(struct perf_event *event, int flags);
static void race_event_stop(struct perf_event *event, int flags);
static int open_thread(void *data);
/*
* Template reset mirrors uncore_pmu_register()'s
* pmu->pmu = (struct pmu){ ... };
* which clears ->dev / ->cpu_pmu_context / ->type etc. before re-registering.
*/
static void pmu_reset(void)
{
static const struct pmu tmpl = {
.task_ctx_nr = perf_invalid_context,
.module = THIS_MODULE,
.capabilities = PERF_PMU_CAP_NO_INTERRUPT |
PERF_PMU_CAP_NO_EXCLUDE,
.event_init = race_event_init,
.add = race_event_add,
.del = race_event_del,
.start = race_event_start,
.stop = race_event_stop,
.read = race_event_read,
};
test_pmu = tmpl;
}
static int race_event_init(struct perf_event *event)
{
if (event->attr.type != test_type)
return -ENOENT;
return 0;
}
static void race_event_read(struct perf_event *event) { }
static int race_event_add(struct perf_event *event, int flags) { return 0; }
static void race_event_del(struct perf_event *event, int flags) { }
static void race_event_start(struct perf_event *event, int flags) { }
static void race_event_stop(struct perf_event *event, int flags) { }
static int do_register(void)
{
int ret;
pmu_reset();
ret = perf_pmu_register(&test_pmu, "racepmu", -1);
if (ret) {
stat_reg_fail++;
my_registered = 0;
} else {
stat_reg_ok++;
my_registered = 1;
test_type = test_pmu.type;
}
return ret;
}
static int do_unregister(void)
{
int ret;
if (!my_registered) {
stat_unreg_fail++;
return -EINVAL;
}
ret = perf_pmu_unregister(&test_pmu);
if (ret) {
stat_unreg_fail++;
} else {
stat_unreg_ok++;
my_registered = 0;
}
return ret;
}
static int do_open_one(int cpu)
{
struct perf_event_attr attr = {};
struct perf_event *ev;
if (!my_registered || test_type < 0)
return -ENODEV;
attr.size = sizeof(attr);
attr.type = test_type;
attr.config = 0;
attr.disabled = 1;
ev = perf_event_create_kernel_counter(&attr, cpu % nr_cpu_ids, NULL,
NULL, NULL);
if (IS_ERR(ev)) {
stat_open_fail++;
if (open_err0 == 0) open_err0 = (int)PTR_ERR(ev);
else if (open_err1 == 0 && (int)PTR_ERR(ev) != open_err0) open_err1 = (int)PTR_ERR(ev);
return PTR_ERR(ev);
}
stat_open_ok++;
perf_event_release_kernel(ev);
return 0;
}
/*
* Ungated open attempt: like a real in-flight perf_event_open() that
* already passed perf_init_event() under SRCU while the PMU is being
* torn down. No my_registered gate: failures are expected and ignored;
* the goal is to reach find_get_pmu_context() while/after
* pmu->cpu_pmu_context is freed.
*/
static void do_open_raw(int cpu)
{
struct perf_event_attr attr = {};
struct perf_event *ev;
if (test_type < 0)
return;
attr.size = sizeof(attr);
attr.type = test_type;
attr.config = 0;
attr.disabled = 1;
ev = perf_event_create_kernel_counter(&attr, cpu % nr_cpu_ids, NULL,
NULL, NULL);
if (!IS_ERR(ev))
perf_event_release_kernel(ev);
}
struct loop_ctx {
long n;
struct completion done;
};
/* R8: thread A - unregister hammer (mirrors uncore_pci_pmu_unregister) */
static int race_unreg_thread(void *data)
{
struct loop_ctx *c = data;
long i;
for (i = 0; i < c->n && !kthread_should_stop(); i++) {
if (my_registered)
do_unregister();
usleep_range(0, 300);
}
complete(&c->done);
return 0;
}
/* R8: thread B - register hammer with template reset (mirrors uncore_pmu_register) */
static int race_reg_thread(void *data)
{
struct loop_ctx *c = data;
long i;
for (i = 0; i < c->n && !kthread_should_stop(); i++) {
do_register();
usleep_range(0, 300);
}
complete(&c->done);
return 0;
}
/* R9 core: migrate hammer (mirrors uncore_change_type_ctx) */
static int migrate_thread(void *data)
{
struct loop_ctx *c = data;
long i;
for (i = 0; i < c->n && !kthread_should_stop(); i++) {
if (my_registered)
perf_pmu_migrate_context(&test_pmu, 0, 1 % nr_cpu_ids);
usleep_range(0, 200);
}
complete(&c->done);
return 0;
}
/*
* R9-specific: migrate hammer kthread racing a SEQUENTIAL
* unregister/register loop (no concurrent register||unregister, so the
* R8 tear at perf_pmu_unregister+0x1d3 cannot fire first). The migrate
* thread keeps calling perf_pmu_migrate_context() while the loop frees
* and re-creates pmu->cpu_pmu_context.
*/
static int mig_hammer(void *unused)
{
while (!kthread_should_stop()) {
perf_pmu_migrate_context(&test_pmu, 0, 1 % nr_cpu_ids);
cond_resched();
}
return 0;
}
static void run_migunreg(long n)
{
struct task_struct *mig;
long i;
if (!my_registered)
do_register();
mig = kthread_run(mig_hammer, NULL, "pmu_mig_hammer");
if (IS_ERR(mig)) {
pr_err("kthread_run failed\n");
return;
}
for (i = 0; i < n; i++) {
if (my_registered)
do_unregister();
do_register();
cond_resched();
}
kthread_stop(mig);
/* consume any poisoned end state */
do_open_one(0);
do_open_one(1 % nr_cpu_ids);
pr_info("MIGUNREG_DONE n=%ld reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu open_ok=%lu open_fail=%lu\n",
n, stat_reg_ok, stat_reg_fail, stat_unreg_ok, stat_unreg_fail,
stat_open_ok, stat_open_fail);
}
/*
* R9 with live events: a migrate hammer and an open/close hammer run
* continuously while the main thread does sequential unregister/register
* rounds. Opened events live long enough for perf_pmu_migrate_context()
* to take the __perf_pmu_install_event() path, which dereferences
* pmu->cpu_pmu_context through find_get_pmu_context().
*/
static void run_migopen(long n)
{
struct loop_ctx o = { .n = 4 * n + 64 };
struct task_struct *mig, *op;
long i;
init_completion(&o.done);
if (!my_registered)
do_register();
mig = kthread_run(mig_hammer, NULL, "pmu_mig_hammer");
op = kthread_run(open_thread, &o, "pmu_open_hammer");
if (IS_ERR(mig) || IS_ERR(op)) {
pr_err("kthread_run failed\n");
return;
}
for (i = 0; i < n; i++) {
if (my_registered)
do_unregister();
do_register();
/* give the open/migrate hammers a stable registered window */
usleep_range(4000, 8000);
cond_resched();
}
kthread_stop(mig);
if (!IS_ERR(op)) {
/* let the open hammer drain, then stop */
wait_for_completion(&o.done);
}
pr_info("MIGOPEN_DONE n=%ld reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu open_ok=%lu open_fail=%lu err0=%d err1=%d\n",
n, stat_reg_ok, stat_reg_fail, stat_unreg_ok, stat_unreg_fail,
stat_open_ok, stat_open_fail, open_err0, open_err1);
}
/* R9 core: open/close churn (exercises find_get_pmu_context) */
static int open_thread(void *data)
{
struct loop_ctx *c = data;
long i;
for (i = 0; i < c->n && !kthread_should_stop(); i++) {
do_open_raw((int)(i % nr_cpu_ids));
usleep_range(0, 200);
}
complete(&c->done);
return 0;
}
static void run_race(long n)
{
struct loop_ctx a = { .n = n }, b = { .n = n };
struct task_struct *ta, *tb;
init_completion(&a.done);
init_completion(&b.done);
/* start from registered state like the real scenario */
if (!my_registered)
do_register();
ta = kthread_run(race_unreg_thread, &a, "pmu_race_unreg");
tb = kthread_run(race_reg_thread, &b, "pmu_race_reg");
if (!IS_ERR(ta) && !IS_ERR(tb)) {
wait_for_completion(&a.done);
wait_for_completion(&b.done);
} else
pr_err("kthread_run failed\n");
if (!IS_ERR(ta))
kthread_stop(ta);
if (!IS_ERR(tb))
kthread_stop(tb);
pr_info("RACE_DONE n=%ld reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu\n",
n, stat_reg_ok, stat_reg_fail, stat_unreg_ok, stat_unreg_fail);
}
static void run_churn(long n)
{
struct loop_ctx a = { .n = n }, b = { .n = n }, c = { .n = n };
struct loop_ctx d = { .n = n };
struct task_struct *ta, *tb, *tc, *td;
init_completion(&a.done);
init_completion(&b.done);
init_completion(&c.done);
init_completion(&d.done);
if (!my_registered)
do_register();
ta = kthread_run(race_unreg_thread, &a, "pmu_churn_unreg");
tb = kthread_run(race_reg_thread, &b, "pmu_churn_reg");
tc = kthread_run(migrate_thread, &c, "pmu_churn_mig");
td = kthread_run(open_thread, &d, "pmu_churn_open");
if (!IS_ERR(ta) && !IS_ERR(tb) && !IS_ERR(tc) && !IS_ERR(td)) {
wait_for_completion(&a.done);
wait_for_completion(&b.done);
wait_for_completion(&c.done);
/* consume any poisoned end state */
do_open_one(0);
do_open_one(1 % nr_cpu_ids);
} else
pr_err("kthread_run failed\n");
pr_info("CHURN_DONE n=%ld reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu open_ok=%lu open_fail=%lu\n",
n, stat_reg_ok, stat_reg_fail, stat_unreg_ok, stat_unreg_fail,
stat_open_ok, stat_open_fail);
}
static void run_regloop(long n)
{
long i;
for (i = 0; i < n; i++) {
do_register();
do_unregister();
usleep_range(0, 50);
}
pr_info("REGLOOP_DONE n=%ld reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu\n",
n, stat_reg_ok, stat_reg_fail, stat_unreg_ok, stat_unreg_fail);
}
static void run_open(long n)
{
long i;
for (i = 0; i < n; i++)
do_open_raw((int)(i % nr_cpu_ids));
pr_info("OPEN_DONE open_ok=%lu open_fail=%lu\n",
stat_open_ok, stat_open_fail);
}
static char action_cmd[64];
static void action_workfn(struct work_struct *w)
{
char cmd[64];
long n;
char *p, *cp;
strscpy(cmd, strstrip(action_cmd), sizeof(cmd));
cp = cmd;
p = strsep(&cp, " ");
n = 0;
if (cp && cp[0])
if (kstrtol(cp, 0, &n))
n = 0;
if (!strcmp(p, "register"))
pr_info("register rc=%d type=%d\n", do_register(), test_type);
else if (!strcmp(p, "unregister"))
pr_info("unregister rc=%d\n", do_unregister());
else if (!strcmp(p, "regloop"))
run_regloop(n);
else if (!strcmp(p, "race"))
run_race(n);
else if (!strcmp(p, "churn"))
run_churn(n);
else if (!strcmp(p, "migunreg"))
run_migunreg(n);
else if (!strcmp(p, "migopen"))
run_migopen(n);
else if (!strcmp(p, "open"))
run_open(n);
else if (!strcmp(p, "status"))
pr_info("status registered=%d type=%d reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu open_ok=%lu open_fail=%lu\n",
my_registered, test_type, stat_reg_ok, stat_reg_fail,
stat_unreg_ok, stat_unreg_fail, stat_open_ok,
stat_open_fail);
else
pr_info("unknown action '%s'\n", p);
pr_info("ACTION_COMPLETE\n");
}
static DECLARE_WORK(action_work, action_workfn);
static char action_cmd[64];
static int action_set(const char *val, const struct kernel_param *kp)
{
int ret = param_set_copystring(val, kp);
if (ret == 0)
schedule_work(&action_work);
return ret;
}
static const struct kernel_param_ops action_ops = {
.set = action_set,
.get = param_get_string,
};
static struct kparam_string action_param = {
.maxlen = sizeof(action_cmd),
.string = action_cmd,
};
module_param_cb(action, &action_ops, &action_param, 0200);
MODULE_PARM_DESC(action, "register|unregister|regloop N|race N|churn N|open N|status");
static int __init pmu_race_init(void)
{
pmu_reset();
pr_info("loaded, nr_cpu_ids=%d\n", nr_cpu_ids);
return 0;
}
static void __exit pmu_race_exit(void)
{
cancel_work_sync(&action_work);
if (my_registered)
do_unregister();
pr_info("unloaded reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu\n",
stat_reg_ok, stat_reg_fail, stat_unreg_ok, stat_unreg_fail);
}
module_init(pmu_race_init);
module_exit(pmu_race_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("perf/PMU race-condition verification driver (R4/R6/R8/R9)");
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: perf/uncore: activeboxes count TOCTOU lets PCI probe/remove run perf_pmu_register() and perf_pmu_unregister() concurrently on the same struct pmu
2026-10-22 13:58 perf/uncore: activeboxes count TOCTOU lets PCI probe/remove run perf_pmu_register() and perf_pmu_unregister() concurrently on the same struct pmu 徐晗翔
@ 2026-10-27 17:56 ` Chen, Zide
0 siblings, 0 replies; 2+ messages in thread
From: Chen, Zide @ 2026-10-27 17:56 UTC (permalink / raw)
To: 徐晗翔, peterz, mingo, acme, namhyung
Cc: linux-perf-users, linux-kernel, Dapeng Mi
Hi Hanxiang,
Thanks for bringing up this long-standing uncore issue.
Yes, I agree that a lock is needed to serialize uncore PMU registration
and teardown.
It seems perf_pmu_{register,unregister}() expects individual PMU drivers
to manage the lifecycle of struct pmu and won't call
perf_pmu_{register,unregister}() concurrently on the same object.
During unregister's unlocked grace window, a concurrent
perf_pmu_register() reinitializes the PMU context, which the resuming
unregister tail then frees, corrupting the core state.
Intel Uncore is unique in implementing an N-to-1 model where multiple
PMON boxes across different dies share a single struct pmu instance.
Without a lock, concurrent probe and remove races across dies can happen.
I have been working on a series of uncore bug fixes, and this patch will
be included in the patch series that I will post to LKML shortly.
Thanks,
Zide
On 10/22/2026 8:58 AM, 徐晗翔 wrote:
> The uncore PCI driver decides "first active box registers the PMU" and
> "last active box unregisters the PMU" with nothing but the
> pmu->activeboxes counter -- the decision and the perf_pmu_register()/
> perf_pmu_unregister() execution are not serialized -- so on a multi-die
> platform whose same-typed boxes live on different dies, concurrently
> removing the last box of one die and probing a box of another die runs
> register and unregister on the SAME struct pmu at the same time; the
> unregister side's unlocked tail (two grace periods, then idr_remove()
> re-reading pmu->type and perf_pmu_free() reading pmu->dev and
> pmu->cpu_pmu_context) then consumes whatever the concurrent register
> just published, and the kernel oopses or leaves a PMU in the list
> holding freed per-CPU contexts. The perf-core half of this is
> confirmed dynamically on mainline 7.3 (first concurrency round:
> page fault oops inside perf_pmu_unregister); the uncore-side trigger
> is source-proven on 7.3's current (recently refactored) code, which
> added box refcounting and a PMU_BROKEN state but still no
> serialization of this decision path.
>
> Both decision sites are counter-only on 7.3 (arch/x86/events/intel/
> uncore.c): uncore_box_setup() at :1153 skips registration when
> atomic_inc_return(&pmu->activeboxes) > 1 (:1166), otherwise calls
> uncore_pmu_register() (:926); uncore_pci_pmu_unregister() at :1283
> calls uncore_pmu_unregister() (:960) when atomic_dec_return(...) == 0
> (:1291). Meanwhile the perf core is asymmetric: perf_pmu_register()
> (kernel/events/core.c:12836) holds pmus_lock for its whole body
> (guard at :12841), while perf_pmu_unregister() (:13023) holds it only
> for the idr_cmpxchg/list_del section (:13025-13029) and for the final
> idr_remove (:13056-13057) -- between and after them the two
> synchronize_srcu()/synchronize_rcu() grace periods (:13042-13043),
> the idr_remove() that re-reads pmu->type, and perf_pmu_free()
> (:13068, reading pmu->dev/pmu->cpu_pmu_context and freeing them) all
> run unlocked for tens of milliseconds. Triggering needs a multi-die
> host with id-table uncore PCI drivers (snbep/skx families) and
> concurrent sysfs unbind/rescan (or a hotplug/DPC event racing an
> admin operation) -- root actions, but each entirely legitimate.
>
> The detailed description:
>
> * Test environment / affected version
>
> Linux version 7.3.0 (gcc 13.3.0, GNU ld 2.42) #1 SMP PREEMPT_DYNAMIC
> -- vanilla mainline 7.3 release tarball (kernel.org, unmodified),
> rebuilt with CONFIG_PERF_EVENTS=y, CONFIG_PERF_EVENTS_INTEL_UNCORE=y,
> KASAN enabled, QEMU/KVM x86_64 guest. The perf-core functions
> cited are byte-identical in 7.1.5 and 7.3; the uncore functions were
> refactored during 7.3 (box refcounting, PMU_BROKEN stickiness,
> WARN_ON_ONCE against double probe at :1206) -- the refactor did NOT
> add any lock (the file's lock inventory is unchanged), and the
> counter-only decision shape survives it. Line numbers below are
> from 7.3.
>
> * Mechanism
>
> 1. Counter-only decisions, no lock between decision and execution:
>
> uncore_box_setup() /* uncore.c:1153 */
> ...
> if (atomic_inc_return(&pmu->activeboxes) > 1) /* :1166 */
> return 0;
> ret = uncore_pmu_register(pmu); /* first
> box */
>
> uncore_pci_pmu_unregister() /* uncore.c:1283 */
> pmu->boxes[die] = NULL;
> if (atomic_dec_return(&pmu->activeboxes) == 0) /* :1291 */
> uncore_pmu_unregister(pmu); /* last
> box */
>
> Two different PCI devices' probe and remove callbacks (per-device
> locks only; sysfs bind/unbind/rescan are not mutually exclusive)
> can hold both decisions simultaneously: remove drops die0's last
> box to 0 and decides "unregister" while probe brings die1's box
> from 0 to 1 and decides "register" -- same struct intel_uncore_pmu
> (type->pmus[] is shared across dies for id-table types).
>
> 2. The register side rewrites the shared struct pmu from scratch:
> uncore_pmu_register() (:926) assigns the whole template
> (clearing ->dev, ->type, ->cpu_pmu_context) before calling
> perf_pmu_register(), which then creates a NEW device, allocates a
> NEW type id and a NEW percpu context array, and publishes them.
>
> 3. The unregister side's unlocked tail reads what register just
> published. With A = remove(die0) and B = probe(die1):
>
> A: perf_pmu_unregister() locks pmus_lock, idr_cmpxchg + list_del
> (:13025-13029), UNLOCKS, enters two grace periods
> (:13042-13043) -- tens of ms
> B: uncore_pmu_register() resets the struct, perf_pmu_register()
> (holding pmus_lock the whole time) creates device D2, type
> T2, contexts P2; INIT event list; publishes; returns 0
> A: resumes: idr_remove(&pmu_idr, pmu->type) /* :13056-13057,
> re-reads pmu->type == T2 -> deletes B's fresh entry */
> perf_pmu_free(pmu) /* :13068: reads
> pmu->dev == D2 -> device_del/put; reads pmu->cpu_pmu_context
> == P2 -> kfrees B's fresh contexts + free_percpu */
>
> End states: a PMU still on the pmus list whose IDR entry is gone
> and whose contexts are freed (later perf_event_open() reaching
> find_get_pmu_context() walks freed memory), or the mirror
> interleaving where A's cmpxchg consumes B's registration wholesale
> (the active PMU vanishes; event creation fails with ENODEV until
> rebind). uncore_pmu_unregister() (:960-967) also ignores
> perf_pmu_unregister()'s return value, so the -EINVAL variant
> leaves a half-torn state machine in place.
>
> * Evidence (7.3)
>
> - Source-level, uncore side: the decision paths above contain no
> serialization on current 7.3 (verified function-by-function; the
> 7.3 refactor touched exactly these functions without adding a
> lock). Real-hardware trigger: concurrent
> echo <bdf0> > /sys/bus/pci/drivers/skx_uncore/unbind
> echo 1 > /sys/bus/pci/rescan
> on a multi-die host.
> - Dynamic, perf-core half (reproducer attached, pmu_race.c): its
> "race" action starts two kthreads faithfully mirroring both sides
> -- an unregister loop and a register loop that performs the same
> whole-struct reset uncore_pmu_register() does before each
> perf_pmu_register(), on the same struct pmu. On the 4-vCPU KASAN
> VM the FIRST concurrency round (seconds into the run) ends in:
>
> CPU: 0 ... Comm: pmu_race_unreg
> RIP: 0010:perf_pmu_unregister+0x1cb/0x620
> #PF: supervisor read access ... address fffffffffffffe08
>
> reading a pointer derived from the concurrently rewritten pmu
> fields; the unregister thread dies and the race never converges.
> (Same signature on 7.1.5 at +0x1d3; the code between is
> identical.) Usage:
>
> insmod pmu_race.ko
> echo "race 2000" > /sys/module/pmu_race/parameters/action
> echo status > /sys/module/pmu_race/parameters/action
>
> QEMU provides no uncore PCI devices, which is why the uncore-side
> trigger is presented source-level and the perf-core concurrency
> (the half both sides funnel into) is what the reproducer drives.
>
>
> Thanks for taking a look.
>
>
> Regards,
> Hanxiang Xu
> xuhx@hust.edu.cn
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-27 17:56 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-22 13:58 perf/uncore: activeboxes count TOCTOU lets PCI probe/remove run perf_pmu_register() and perf_pmu_unregister() concurrently on the same struct pmu 徐晗翔
2026-10-27 17:56 ` Chen, Zide
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®