Re: [PATCH] perf_events: fix array-index-out-of-bounds in x86_pmu_del

From: Peter Zijlstra

Date: Mon Mar 09 2026 - 08:21:15 EST


On Sun, Mar 08, 2026 at 06:41:30PM -0700, Oliver Rosenberg wrote:
> Vulnerability Description:
> There exists a KASAN:wild-memory-access and an array-index-out-of-bounds
> (-1 index) in x86_pmu_del(). When a cross-PMU event group is created
> where the group leader is a PMU type that does not have txn implemented
> then the cpuc->txn_flags for the siblings are not set because only the
> group leader's pmu->start_txn() is called and in the case of not being
> implemented it is set to perf_pmu_nop_txn(). The events are still attempted
> to be processed as a group, when one of the x86 PMUs errors out,
> cpuc->txn_flags is not set and event->hw.idx is not set. The x86_pmu_del()
> function expects event->hw.idx not to be set when events are batched and
> not yet enabled during a transaction so the function checks the
> cpuc->txn_flags to skip the array-index-out-of-bounds. However, when an
> event errors out and the group events are not enable here, cpuc->txn_flags
> is not set and event->hw.idx is -1 so we do not skip the uses of
> event->hw.idx which causes an array-index-out-of-bounds.

I can confirm your POC works, and that the above is somehow what
happens. But it is not what should be happening.

Specifically, by adding an x86 even to a 'software' event, the software
event should be moved to the x86 pmu_ctx (move_group case in
perf_event_open) and then group_event->pmu_ctx->pmu should end up being
the x86 pmu in group_sched_in().

Tracing shows the move_group path is indeed taken and pmu_ctx is
adjusted.

> POC Explanation:
> The POC first fills the CPUs PMU events with x86 raw PMU events (type 4) to
> set up the group scheduling to fail and trigger the bug, it then creates a
> cross-PMU group with BREAKPOINT (type 5) as the group leader and 2 x86 raw
> PMU siblings. The first sibling is added but batched and not enabled, the
> second sibling fails on add because we exceed the max number of PMU events
> for the cpu. This triggers the cleanup of the first sibling for which
> cpu->txn_flags is not set and event->hw.idx=-1 since sibling 1 was not
> enabled. This triggers the bug when event->hw.idx is used as an
> array-index-out-of-bounds in x86_pmu_del().

You're forgetting part of what the POC does:

if (fork() == 0) _exit(0);
wait(NULL);


Without that, it doesn't reproduce. Suggesting there's something wrong
with inherit.

And indeed, the below makes it go away.

Now, let me go audit the code to see if this same problem exists in more
shapes...

diff --git a/arch/x86/events/core.c b/arch/x86/events/core.c
index 03ce1bc7ef2e..75cd651a7f55 100644
--- a/arch/x86/events/core.c
+++ b/arch/x86/events/core.c
@@ -1655,6 +1655,8 @@ static void x86_pmu_del(struct perf_event *event, int flags)
if (cpuc->txn_flags & PERF_PMU_TXN_ADD)
goto do_del;

+ WARN_ON(event->hw.idx < 0);
+
__set_bit(event->hw.idx, cpuc->dirty);

/*
diff --git a/kernel/events/core.c b/kernel/events/core.c
index 03ced7aad309..ab968203b735 100644
--- a/kernel/events/core.c
+++ b/kernel/events/core.c
@@ -14743,7 +14749,7 @@ inherit_event(struct perf_event *parent_event,
get_ctx(child_ctx);
child_event->ctx = child_ctx;

- pmu_ctx = find_get_pmu_context(child_event->pmu, child_ctx, child_event);
+ pmu_ctx = find_get_pmu_context(parent_event->pmu_ctx->pmu, child_ctx, child_event);
if (IS_ERR(pmu_ctx)) {
free_event(child_event);
return ERR_CAST(pmu_ctx);