mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* perf: perf_pmu_register() publishes sysfs attrs before per-CPU contexts exist -> NULL/poisoned cpc deref in perf_event_mux_interval_ms_store()
@ 2026-10-22  8:38 徐晗翔
  2026-10-27 10:14 ` Mi, Dapeng
  0 siblings, 1 reply; 2+ messages in thread
From: 徐晗翔 @ 2026-10-22  8:38 UTC (permalink / raw)
  To: peterz, mingo, acme, namhyung; +Cc: linux-perf-users, linux-kernel

perf_pmu_register() makes <pmu>/perf_event_mux_interval_ms writable
before the per-CPU PMU contexts are allocated and initialized, so a
concurrent write to that attribute dereferences a NULL or a published
-but-uninitialized cpc: a kernel oops writing to absolute address 0xf0
in task context, or a NULL+0xc deref on the target CPU in hardirq
context ending in "Fatal exception in interrupt" -- both verified
deterministically on mainline 7.3 and reached naturally by an ordinary
userspace write loop without any instrumentation.


The store side (perf_event_mux_interval_ms_store(),
kernel/events/core.c:12615) fetches cpc and writes through it with no
readiness/NULL check, while the register side publishes the sysfs
attributes first (pmu_dev_alloc() at core.c:12865) and installs the
per-CPU contexts afterwards (alloc_percpu at 12870; cpc pointer
published at 12877, its hrtimer initialized only at 12879 by
__perf_mux_hrtimer_init()). The registrant holds pmus_lock; sysfs
callbacks take no lock in common with it, so a root writer (default
attribute mode) racing a runtime PMU registration lands inside the
window. Verified on mainline 7.3 with KASAN: controlled-window
evidence pins both faulting instructions exactly, and a natural
write-hammer hits the window within ~0.6 s. The same publication
window also has a purely functional consequence (persistent
interval-configuration divergence) described at the end.


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_KASAN_GENERIC=y, CONFIG_KASAN_OUTLINE=y,
CONFIG_KASAN_VMALLOC=y, CONFIG_PERF_EVENTS=y, CONFIG_MODULES=y,
CONFIG_HZ_1000=y, booted in QEMU/KVM on x86_64 (4 and 64 vCPU
guests). The publication order and the store code are identical in
7.1.5 (function-level diff); line numbers below are from 7.3.


* Mechanism

perf_pmu_register() publishes first and initializes later:


if (pmu_bus_running && !pmu->dev) {
ret = pmu_dev_alloc(pmu); /* core.c:12865:
device_add() makes the
attribute group live */
...
}
pmu->cpu_pmu_context = alloc_percpu(...); /* core.c:12870 */
for_each_possible_cpu(cpu) {
cpc = kmalloc_node(...); /* per-CPU allocs */
*per_cpu_ptr(pmu->cpu_pmu_context, cpu) = cpc; /* :12877 */
__perf_init_event_pmu_context(&cpc->epc, pmu);
__perf_mux_hrtimer_init(cpc, cpu); /* :12879 */
}


and the store path has no readiness check:


for_each_online_cpu(cpu) {
struct perf_cpu_pmu_context *cpc;
cpc = *per_cpu_ptr(pmu->cpu_pmu_context, cpu); /* :12640 */
cpc->hrtimer_interval = ns_to_ktime(...); /* :12641 */
cpu_function_call(cpu, perf_mux_hrtimer_restart_ipi, cpc);
/* :12643 */
}


Two windows follow:


- Window A (array published, slots still NULL; register thread is in
the per-CPU kmalloc_node loop): the write faults at :12641 with a
NULL cpc. In the 7.3 build the store does
"mov %r14, 0xf0(%rsi)" with rsi==0 -- a write to absolute address
0xf0.

- Window B (cpc pointer already published at :12877, but
__perf_mux_hrtimer_init() at :12879 has not run yet, so the
embedded hrtimer is all-zero): the write itself succeeds and the
IPI at :12643 executes perf_mux_hrtimer_restart() ->
hrtimer_forward_now() -> hrtimer_cb_get_time() on the remote CPU,
which reads timer->base->clockid with timer->base == NULL (offset
0xc) in hardirq context.


pmu_dev_is_visible() (core.c:12709) does not mask the interval
attribute, and there is no lock common to the registrant (pmus_lock)
and the sysfs callback.


* Controlled-window evidence (deterministic, both fault forms)


Stalling the registering thread briefly (kprobe on the first per-CPU
cpc allocation for window A, on the first hrtimer_setup call for
window B; a 400 ms stall, writer runs during it) produces exactly
the predicted faults, nothing else:


Window A:
BUG: kernel NULL pointer dereference, address: 00000000000000f0
#PF: supervisor write access in kernel mode
RIP: 0010:perf_event_mux_interval_ms_store+0x18a/0x250
(Code: ... <4d> 89 be f0 00 00 00 ... /* mov %r14,0xf0(%rsi) */)


Window B:
BUG: kernel NULL pointer dereference, address: 00000000000000c
CPU: 0 ... Comm: swapper/0
RIP: 0010:hrtimer_cb_get_time+0x1e/0x70
__flush_smp_call_function_queue
remote_function
perf_mux_hrtimer_restart
hrtimer_cb_get_time
Kernel panic - not syncing: Fatal exception in interrupt


The kprobe module used for the stalls is available on request; it is
only a controlled-window amplifier and is NOT needed to hit the bug
naturally (next section).


* Natural reachability (no instrumentation)


A minimal PMU driver registered/unregistered in a loop through the
exported perf core API (same lifecycle as an unloadable PMU module
or an Intel uncore PCI rebind), while userspace runs a tight loop
writing the attribute as soon as it appears (alternating values, so
every write passes the same-value fast path). On a 64-vCPU KASAN
VM (7.3) the first slab-use-after-free appears ~0.6 s after the
hammer starts (94 reports within the run; the writes that land late
in the window successfully arm cpc->hrtimer and are then freed by
the unregister of the same round -- that use-after-free in
perf_pmu_free() is a separate defect I am reporting in its own
mail). A fault dump in the same run shows the writer's path
stopping exactly at perf_event_mux_interval_ms_store+0x18a, the
same instruction as the controlled window-A witness. On 7.1.5 the
same uninstrumented hammer also faulted naturally inside the store
(dangling percpu array read at store+0x149).


* Reproducer

Module source inlined at the end of this mail; usage:

# build against the kernel under test
make -C <kernel-build-dir> M=$PWD modules

# in the guest (root):
insmod r4_repro.ko
# terminal A: writer hammer
while :; do
F=/sys/bus/event_source/devices/r4pmu/perf_event_mux_interval_ms
[ -e $F ] && { echo 100 > $F 2>/dev/null; echo 200 > $F 2>/dev/null; }
done
# terminal B:
echo "regloop 400" > /sys/module/r4_repro/parameters/action


Triggering requires a runtime PMU registration (unloadable PMU
driver, uncore PCI rebind on matching hardware) plus a root writer
(attribute mode 0644); the window is the registration interval
(device_add done -> last cpc timer initialized), sub-millisecond to
milliseconds per round, trivially hit by a write loop.


* Additional symptom of the same window (functional, no memory fault)


If the store lands between __perf_mux_hrtimer_init()'s "is the
field unset" read and its default assignment (the default is
PERF_CPU_HRTIMER = 1000/HZ; the assignment writes
pmu->hrtimer_interval_ms), the PMU ends up with
pmu->hrtimer_interval_ms == default while every cpc actually runs
the user's value -- and because the store's same-value fast path
returns early on an equal value, writing the field value back is a
permanent no-op: the divergence cannot be repaired by re-writing
the same number. Same fix point (normalize defaults before
publication), so reported here rather than separately.


Thanks for taking a look.

Regards,
Hanxiang Xu
xuhx@hust.edu.cn

------------------------------------------------------------------------
reproducer: r4_repro.c (build with: make -C <kernel-build-dir> M=$PWD
modules; requires CONFIG_PERF_EVENTS=y and CONFIG_MODULES=y)
------------------------------------------------------------------------
// SPDX-License-Identifier: GPL-2.0
/*
* r4_repro.c - minimal reproducer for the premature-sysfs-publication
* NULL dereference in perf_event_mux_interval_ms_store()
* (kernel/events/core.c).
*
* perf_pmu_register() publishes the PMU device and its sysfs attributes
* (pmu_dev_alloc() -> device_add()) *before* allocating and installing the
* per-CPU perf_cpu_pmu_context array and initializing each cpc's hrtimer.
* A concurrent root write to perf_event_mux_interval_ms during that window
* reaches:
*
* cpc = *per_cpu_ptr(pmu->cpu_pmu_context, cpu);
* cpc->hrtimer_interval = ...; <- NULL cpc => write to 0xf0
*
* or, one window later (cpc published, timer not yet initialized), the
* restart IPI runs perf_mux_hrtimer_restart() on a zeroed hrtimer and
* dereferences timer->base == NULL in hrtimer_forward_now().
*
* Usage (from userspace):
*
* insmod r4_repro.ko
* # terminal A: writer hammer on the freshly (re)created sysfs node
* while :; do
* for F in /sys/bus/event_source/devices/r4pmu/perf_event_mux_interval_ms; do
* [ -e $F ] && { echo 100 > $F 2>/dev/null; echo 200 > $F 2>/dev/null; }
* done
* done
* # terminal B: registration churn through the exported perf core API
* echo "regloop 400" > /sys/module/r4_repro/parameters/action
*
* Alternating values bypass the same-value fast path so every open of the
* node performs a full store. On a KASAN kernel, a write landing inside
* the window produces an immediate NULL-pointer oops in
* perf_event_mux_interval_ms_store() (write fault at address 0xf0), or an
* IRQ-context panic via perf_mux_hrtimer_restart() -> hrtimer_cb_get_time()
* (NULL+0xc). Writes landing in a later part of the window succeed in
* arming cpc->hrtimer and will surface as use-after-free reports when the
* same registration round unregisters again (that is the related
* perf_pmu_free() defect; see the report text).
*
* No kernel source modification: everything goes through the
* EXPORT_SYMBOL_GPL perf core API, exactly like a real unloadable PMU
* provider driver (e.g. Intel uncore PCI drivers via bind/unbind).
*/
#define pr_fmt(fmt) "R4REPRO: " fmt
#include <linux/module.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;
static unsigned long stat_reg_ok, stat_reg_fail, stat_unreg_ok, stat_unreg_fail;
static int repro_event_init(struct perf_event *event)
{
if (event->attr.type != test_type)
return -ENOENT;
return 0;
}
static void repro_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 = repro_event_init,
};
test_pmu = tmpl;
}
static int do_register(void)
{
int ret;
repro_pmu_reset();
ret = perf_pmu_register(&test_pmu, "r4pmu", -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 = -EINVAL;
if (my_registered)
ret = perf_pmu_unregister(&test_pmu);
if (ret) {
stat_unreg_fail++;
} else {
stat_unreg_ok++;
my_registered = 0;
}
return ret;
}
static char action_cmd[64];
static void action_workfn(struct work_struct *w)
{
char cmd[64];
char *p, *cp;
long n = 0;
strscpy(cmd, strstrip(action_cmd), sizeof(cmd));
cp = cmd;
p = strsep(&cp, " ");
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") && n > 0) {
long i;
for (i = 0; i < n; i++) {
if (my_registered)
do_unregister();
do_register();
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);
} else if (!strcmp(p, "status")) {
pr_info("status registered=%d type=%d reg_ok=%lu reg_fail=%lu unreg_ok=%lu unreg_fail=%lu\n",
my_registered, test_type, stat_reg_ok, stat_reg_fail,
stat_unreg_ok, stat_unreg_fail);
} else {
pr_info("unknown action '%s'\n", p);
}
pr_info("ACTION_COMPLETE\n");
}
static DECLARE_WORK(action_work, action_workfn);
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|status");
static int __init r4_repro_init(void)
{
repro_pmu_reset();
pr_info("loaded; start the userspace writer hammer, then run regloop\n");
return 0;
}
static void __exit r4_repro_exit(void)
{
cancel_work_sync(&action_work);
if (my_registered)
do_unregister();
pr_info("unloaded\n");
}
module_init(r4_repro_init);
module_exit(r4_repro_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal reproducer: premature sysfs publication NULL deref in perf_event_mux_interval_ms_store()");

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

end of thread, other threads:[~2026-10-27 10:14 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-22  8:38 perf: perf_pmu_register() publishes sysfs attrs before per-CPU contexts exist -> NULL/poisoned cpc deref in perf_event_mux_interval_ms_store() 徐晗翔
2026-10-27 10:14 ` Mi, Dapeng

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®