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_free() kfree()s struct perf_cpu_pmu_context while its mux hrtimer is still queued (use-after-free)
Date: Thu, 22 Oct 2026 16:26:54 +0800 (GMT+08:00)	[thread overview]
Message-ID: <ca4f133.14714.1a162b8654c.Coremail.xuhx@hust.edu.cn> (raw)

perf_pmu_free() frees every non-embedded struct perf_cpu_pmu_context
with a plain kfree() without cancelling cpc->hrtimer first, so
unregistering a PMU whose multiplexing hrtimers are queued leaves live
hrtimers linked into the per-CPU timer rbtree pointing into freed
memory -- a use-after-free (confirmed by KASAN) that is reachable with
two sequential root operations and no scheduling race at all.
perf_pmu_free() in kernel/events/core.c releases the per-CPU PMU
contexts without any hrtimer quiescence, while several ordinary code
paths leave cpc->hrtimer queued on CPUs without any event attached.


Unregistering the PMU then frees the cpc objects under the queued
timers: every later hrtimer insertion/removal on the affected CPUs
reads and writes freed rbtree nodes, and at expiry __run_hrtimer()
calls perf_mux_hrtimer_handler() on the freed cpc, taking the freed
cpc->hrtimer_lock and writing cpc->hrtimer_active. Verified and
reproduced on mainline 7.3 with KASAN; the involved code is unchanged
since at least 7.1. Triggering requires only root sysfs writes plus a
runtime PMU unregister (unloadable PMU driver module or, on matching
hardware, Intel uncore PCI unbind).


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, CONFIG_HOTPLUG_CPU=y, booted in QEMU/KVM on x86_64
(4 vCPU). The same defect code is present and identical in 7.1.5
(verified by function-level diff); the 7.3 line numbers below are
from the 7.3 tarball.


* Mechanism

1. Writing /sys/bus/event_source/devices/<pmu>/perf_event_mux_interval_ms
takes the perf_event_mux_interval_ms_store() path
(kernel/events/core.c:12615). For every online CPU it stores the
new interval into cpc->hrtimer_interval and then calls
cpu_function_call(cpu, perf_mux_hrtimer_restart_ipi, cpc)
(core.c:12643). perf_mux_hrtimer_restart_ipi() ->
perf_mux_hrtimer_restart() arms cpc->hrtimer based solely on
hrtimer_active(), regardless of whether the PMU has any events:
an *idle* PMU ends up with a queued mux hrtimer on every online
CPU after one ordinary sysfs write.


2. Unregistering the PMU (perf_pmu_unregister() ->
perf_pmu_free(), core.c:12805) walks all possible CPUs and frees
the contexts:

cpc = *per_cpu_ptr(pmu->cpu_pmu_context, cpu);
if (!cpc)
continue;
if (cpc->epc.embedded) { /* refcount managed */
put_pmu_ctx(&cpc->epc);
continue;
}
kfree(cpc);

There is no hrtimer_cancel() (or any hrtimer call at all) in
perf_pmu_free(), perf_pmu_unregister(), or anywhere else on the
unregister path. The queued cpc->hrtimer stays linked in each
CPU's MONOTONIC rbtree after the kfree().


3. Consequences, both observed:
- Immediately after the free: any hrtimer queued on an affected
CPU walks/rotates the rbtree through the freed node; KASAN
reports slab-use-after-free in rb_erase/rb_insert_color/
timerqueue_add/__remove_hrtimer (reads *and writes of freed
memory, corrupting both the timer tree and the slab's new
owner).
- At expiry (interval after the free): __run_hrtimer() reads
timer->function from freed memory and runs
perf_mux_hrtimer_handler() (core.c:1278) on the freed cpc: it
calls perf_rotate_context(), takes the freed cpc->hrtimer_lock
and writes cpc->hrtimer_active. KASAN reports the UAF in hard
IRQ context (swapper/N).
The embedded variant (put_pmu_ctx() at core.c:5277 ->
free_cpc_rcu() at core.c:5262) defers the kfree by one RCU grace
period but equally never cancels the timer, so the window merely
shifts; the plain-kfree path above needs no events to have ever
been opened on the PMU.


* How to reproduce (deterministic, ~30 seconds)

A minimal PMU driver registered through the exported perf core API
(perf_pmu_register/perf_pmu_unregister, EXPORT_SYMBOL_GPL) -- the
same lifecycle an unloadable PMU provider module or an Intel uncore
PCI driver (arch/x86/events/intel/uncore.c, uncore_pci_pmu_
unregister() in 7.3) goes through. Module source is 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 r6_repro.ko
echo register > /sys/module/r6_repro/parameters/action
echo 10000 > /sys/bus/event_source/devices/r6pmu/perf_event_mux_interval_ms
echo unregister > /sys/module/r6_repro/parameters/action
# wait up to 10 s for expiry (or just keep the system busy);
# KASAN splats start immediately after 'unregister'


There is no interleaving window: register -> sysfs write ->
unregister is purely sequential. On the 4-vCPU KASAN VM this
produced 72 KASAN slab-use-after-free reports within ~10 seconds of
the unregister (histogram: rb_insert_color 19, rb_erase 18,
perf_mux_hrtimer_handler 12, timerqueue_linked_add 5,
rb_erase_linked 5, __hrtimer_run_queues 4, _raw_spin_lock/unlock 6,
__remove_hrtimer 2, hrtimer_start_range_ns 1).


* KASAN report (expiry-path witness, one of 72)

BUG: KASAN: slab-use-after-free in perf_mux_hrtimer_handler+0x8b/0x5e0
Read of size 8 at addr ff11000001d09600 by task swapper/2/0
CPU: 2 UID: 0 PID: 0 Comm: swapper/2 ... 7.3.0 #1 PREEMPT(lazy)
<IRQ>
perf_mux_hrtimer_handler+0x8b/0x5e0
__hrtimer_run_queues+0x166/0x340
hrtimer_interrupt+0x1d2/0x320
__sysvec_apic_timer_interrupt+0x62/0x1a0
sysvec_apic_timer_interrupt+0x66/0x80
</IRQ>


Allocated by task 29:
__kmalloc_cache_node_noprof+0x17c/0x3a0
perf_pmu_register+0x196/0x5a0
action_workfn+0x17b/0x230 [r6_repro]


Freed by task 48:
kfree+0x121/0x390
perf_pmu_free+0xc2/0x190
perf_pmu_unregister+0x497/0x620
action_workfn+0x217/0x230 [r6_repro]


The object is a struct perf_cpu_pmu_context (kmalloc-256, KASAN
tracks the allocation inside the per-CPU loop of
perf_pmu_register()). The full serial log (72 reports, all three
stacks for each) is available on request.
Thanks for taking a look.


Regards,
Hanxiang Xu
xuhx@hust.edu.cn
------------------------------------------------------------------------
reproducer: r6_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
/*
* r6_repro.c - minimal reproducer for the perf mux hrtimer
* use-after-free in perf_pmu_free() (kernel/events/core.c).
*
* insmod r6_repro.ko
* echo register > /sys/module/r6_repro/parameters/action
* echo 10000 > /sys/bus/event_source/devices/r6pmu/perf_event_mux_interval_ms
* echo unregister > /sys/module/r6_repro/parameters/action
*
* No kernel source modification: everything goes through the
* EXPORT_SYMBOL_GPL perf core API, exactly like a real unloadable PMU
* provider driver.
*/
#define pr_fmt(fmt) "R6REPRO: " fmt
#include <linux/module.h>
#include <linux/kernel.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 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 char action_cmd[64];
static void action_workfn(struct work_struct *w)
{
char cmd[64];
strscpy(cmd, strstrip(action_cmd), sizeof(cmd));
if (!strcmp(cmd, "register")) {
int ret;
repro_pmu_reset();
ret = perf_pmu_register(&test_pmu, "r6pmu", -1);
if (ret) {
my_registered = 0;
} else {
my_registered = 1;
test_type = test_pmu.type;
}
pr_info("register rc=%d type=%d\n", ret, test_type);
} else if (!strcmp(cmd, "unregister")) {
int ret = -EINVAL;
if (my_registered)
ret = perf_pmu_unregister(&test_pmu);
if (!ret)
my_registered = 0;
pr_info("unregister rc=%d\n", ret);
} else if (!strcmp(cmd, "status")) {
pr_info("status registered=%d type=%d\n",
my_registered, test_type);
} else {
pr_info("unknown action '%s'\n", cmd);
}
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|status");
static int __init r6_repro_init(void)
{
repro_pmu_reset();
pr_info("loaded; register me, write perf_event_mux_interval_ms, then unregister\n");
return 0;
}
static void __exit r6_repro_exit(void)
{
cancel_work_sync(&action_work);
if (my_registered)
perf_pmu_unregister(&test_pmu);
pr_info("unloaded\n");
}
module_init(r6_repro_init);
module_exit(r6_repro_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Minimal reproducer: perf mux hrtimer UAF in perf_pmu_free()");

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

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

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=ca4f133.14714.1a162b8654c.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®