From: "Chen, Zide" <zide.chen@intel.com>
To: 徐晗翔 <xuhx@hust.edu.cn>,
peterz@infradead.org, mingo@redhat.com, acme@kernel.org,
namhyung@kernel.org
Cc: linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org,
Dapeng Mi <dapeng1.mi@linux.intel.com>
Subject: Re: perf/uncore: activeboxes count TOCTOU lets PCI probe/remove run perf_pmu_register() and perf_pmu_unregister() concurrently on the same struct pmu
Date: Tue, 27 Oct 2026 12:56:11 -0500 [thread overview]
Message-ID: <a69eed6e-a812-43ad-bedc-3f0f68a47d0e@intel.com> (raw)
In-Reply-To: <78d9ba61.14c08.1a163e7b794.Coremail.xuhx@hust.edu.cn>
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
prev parent reply other threads:[~2026-10-27 17:56 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 13:58 徐晗翔
2026-10-27 17:56 ` Chen, Zide [this message]
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=a69eed6e-a812-43ad-bedc-3f0f68a47d0e@intel.com \
--to=zide.chen@intel.com \
--cc=acme@kernel.org \
--cc=dapeng1.mi@linux.intel.com \
--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 \
--cc=xuhx@hust.edu.cn \
/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®