mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: 徐晗翔 <xuhx@hust.edu.cn>
To: peterz@infradead.org, mingo@redhat.com, acme@kernel.org,
	namhyung@kernel.org
Cc: linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: perf: perf_pmu_register() publishes sysfs attrs before per-CPU contexts exist -> NULL/poisoned cpc deref in perf_event_mux_interval_ms_store()
Date: Thu, 22 Oct 2026 16:38:48 +0800 (GMT+08:00)	[thread overview]
Message-ID: <de936c3.1476b.1a162c34cec.Coremail.xuhx@hust.edu.cn> (raw)

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()");

             reply	other threads:[~2026-10-22  8:39 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-22  8:38 徐晗翔 [this message]
2026-10-27 10:14 ` Mi, Dapeng

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=de936c3.1476b.1a162c34cec.Coremail.xuhx@hust.edu.cn \
    --to=xuhx@hust.edu.cn \
    --cc=acme@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-perf-users@vger.kernel.org \
    --cc=mingo@redhat.com \
    --cc=namhyung@kernel.org \
    --cc=peterz@infradead.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®