mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] drivers/perf: riscv: Fix racy hw.idx use in user access paths
@ 2026-10-28  3:11 Li Jinyue
  0 siblings, 0 replies; only message in thread
From: Li Jinyue @ 2026-10-28  3:11 UTC (permalink / raw)
  To: Atish Patra, Will Deacon, Mark Rutland
  Cc: Anup Patel, Paul Walmsley, Palmer Dabbelt, Albert Ou,
	Alexandre Ghiti, Xixin Liu, Andrew Jones, linux-riscv,
	linux-arm-kernel, linux-perf-users, linux-kernel, stable

riscv_pmu_event_idx() and arch_perf_update_userpage() are called from
perf_mmap() through perf_event_update_userpage(), and
pmu_sbi_set_scounteren()/pmu_sbi_reset_scounteren() run on every CPU in
mm_cpumask() through IPIs sent by pmu_sbi_event_mapped() and
pmu_sbi_event_unmapped(). None of them hold the event context lock, so
the event can be scheduled in or out on another CPU at the same time,
and riscv_pmu_add()/riscv_pmu_del() change hw.idx underneath them.

Taking the context lock is not an option: perf_event_update_userpage()
is also called from riscv_pmu_add() and riscv_pmu_del(), which already
hold it.

Commit 3fec323339a4 ("drivers: perf: Fix panic in riscv SBI mmap
support") added hw.idx != -1 checks to the scounteren helpers and to
riscv_perf_user_access(), but hw.idx is loaded again afterwards to
index pmu_ctr_list, so the check does not cover the access.
riscv_pmu_event_idx() has no check and relies on perf_event_index(),
which only looks at the software state (event->state and hw.state) and
not at hw.idx. event_sched_in() marks the event ACTIVE before calling
pmu::add(), and hw.state is still zero on the first schedule-in, so
perf_event_index() can call into the driver with hw.idx == -1.
event_sched_out() likewise calls pmu::del() before the state changes.

syzkaller hits this on QEMU with KASAN. In every report the faulting
address is pmu_ctr_list[-1]:

  BUG: KASAN: slab-out-of-bounds in pmu_sbi_csr_index+0x80/0x82
  Read of size 8 at addr ff60000009933bf8 by task syz.0.5217/27313
  Call Trace:
   pmu_sbi_csr_index+0x80/0x82
   riscv_pmu_event_idx+0x9c/0xd8
   perf_event_update_userpage+0x544/0x6ca
   riscv_pmu_event_mapped+0x6e/0x98
   perf_mmap+0xbcc/0x22ae
   __mmap_region+0x1128/0x2398
   mmap_region+0x3cc/0x466
   do_mmap+0xe7c/0x162c
   vm_mmap_pgoff+0x1e8/0x362
   ksys_mmap_pgoff+0x338/0x402
   __riscv_sys_mmap+0x11c/0x18c

The same read is also reported in pmu_sbi_ctr_get_width() from
arch_perf_update_userpage(), in pmu_sbi_set_scounteren() from the IPI
handler and in pmu_sbi_reset_scounteren() from perf_mmap_close().

Load hw.idx once with READ_ONCE() in these paths, check that value and
use it for the lookup. Change the csr_index() callback to take the
counter index so that the value checked is the value used; neither
backend needs anything else from the event. Use WRITE_ONCE() for the
stores in riscv_pmu_add() and riscv_pmu_del().

Fixes: cc4c07c89aad ("drivers: perf: Implement perf event mmap support in the SBI backend")
Cc: stable@vger.kernel.org
Signed-off-by: Li Jinyue <jinyue.li@linux.alibaba.com>
---
 drivers/perf/riscv_pmu.c        | 22 ++++++++++++++--------
 drivers/perf/riscv_pmu_legacy.c |  4 ++--
 drivers/perf/riscv_pmu_sbi.c    | 14 ++++++++------
 include/linux/perf/riscv_pmu.h  |  2 +-
 4 files changed, 25 insertions(+), 17 deletions(-)

diff --git a/drivers/perf/riscv_pmu.c b/drivers/perf/riscv_pmu.c
index 8e3cd0f35336..0ec4114595b0 100644
--- a/drivers/perf/riscv_pmu.c
+++ b/drivers/perf/riscv_pmu.c
@@ -18,18 +18,23 @@
 
 #include <asm/sbi.h>
 
-static bool riscv_perf_user_access(struct perf_event *event)
+static bool riscv_perf_user_access(struct perf_event *event, int idx)
 {
 	return ((event->attr.type == PERF_TYPE_HARDWARE) ||
 		(event->attr.type == PERF_TYPE_HW_CACHE) ||
 		(event->attr.type == PERF_TYPE_RAW)) &&
 		!!(event->hw.flags & PERF_EVENT_FLAG_USER_READ_CNT) &&
-		(event->hw.idx != -1);
+		(idx != -1);
 }
 
 void arch_perf_update_userpage(struct perf_event *event,
 			       struct perf_event_mmap_page *userpg, u64 now)
 {
+	/*
+	 * hw.idx is updated by pmu::add()/del() on another CPU, so take a
+	 * snapshot and use it for both the check and the lookup.
+	 */
+	int idx = READ_ONCE(event->hw.idx);
 	struct clock_read_data *rd;
 	unsigned int seq;
 	u64 ns;
@@ -37,7 +42,7 @@ void arch_perf_update_userpage(struct perf_event *event,
 	userpg->cap_user_time = 0;
 	userpg->cap_user_time_zero = 0;
 	userpg->cap_user_time_short = 0;
-	userpg->cap_user_rdpmc = riscv_perf_user_access(event);
+	userpg->cap_user_rdpmc = riscv_perf_user_access(event, idx);
 
 	/*
 	 * The counters are 64-bit but the priv spec doesn't mandate all the
@@ -45,7 +50,7 @@ void arch_perf_update_userpage(struct perf_event *event,
 	 * the cpu vendor.
 	 */
 	if (userpg->cap_user_rdpmc)
-		userpg->pmc_width = to_riscv_pmu(event->pmu)->ctr_get_width(event->hw.idx) + 1;
+		userpg->pmc_width = to_riscv_pmu(event->pmu)->ctr_get_width(idx) + 1;
 
 	do {
 		rd = sched_clock_read_begin(&seq);
@@ -265,7 +270,7 @@ static int riscv_pmu_add(struct perf_event *event, int flags)
 	if (idx < 0)
 		return idx;
 
-	hwc->idx = idx;
+	WRITE_ONCE(hwc->idx, idx);
 	cpuc->events[idx] = event;
 	cpuc->n_events++;
 	hwc->state = PERF_HES_UPTODATE | PERF_HES_STOPPED;
@@ -293,7 +298,7 @@ static void riscv_pmu_del(struct perf_event *event, int flags)
 	if (rvpmu->ctr_clear_idx)
 		rvpmu->ctr_clear_idx(event);
 	perf_event_update_userpage(event);
-	hwc->idx = -1;
+	WRITE_ONCE(hwc->idx, -1);
 }
 
 static void riscv_pmu_read(struct perf_event *event)
@@ -353,12 +358,13 @@ static int riscv_pmu_event_init(struct perf_event *event)
 static int riscv_pmu_event_idx(struct perf_event *event)
 {
 	struct riscv_pmu *rvpmu = to_riscv_pmu(event->pmu);
+	int idx = READ_ONCE(event->hw.idx);
 
-	if (!(event->hw.flags & PERF_EVENT_FLAG_USER_READ_CNT))
+	if (!(event->hw.flags & PERF_EVENT_FLAG_USER_READ_CNT) || idx == -1)
 		return 0;
 
 	if (rvpmu->csr_index)
-		return rvpmu->csr_index(event) + 1;
+		return rvpmu->csr_index(idx) + 1;
 
 	return 0;
 }
diff --git a/drivers/perf/riscv_pmu_legacy.c b/drivers/perf/riscv_pmu_legacy.c
index 1b8e4789cb3c..e540bb798fb6 100644
--- a/drivers/perf/riscv_pmu_legacy.c
+++ b/drivers/perf/riscv_pmu_legacy.c
@@ -76,9 +76,9 @@ static void pmu_legacy_ctr_start(struct perf_event *event, u64 ival)
 	local64_set(&hwc->prev_count, initial_val);
 }
 
-static uint8_t pmu_legacy_csr_index(struct perf_event *event)
+static uint8_t pmu_legacy_csr_index(int idx)
 {
-	return event->hw.idx;
+	return idx;
 }
 
 static void pmu_legacy_event_mapped(struct perf_event *event, struct mm_struct *mm)
diff --git a/drivers/perf/riscv_pmu_sbi.c b/drivers/perf/riscv_pmu_sbi.c
index 2991dd92def2..2360ee5d8843 100644
--- a/drivers/perf/riscv_pmu_sbi.c
+++ b/drivers/perf/riscv_pmu_sbi.c
@@ -530,9 +530,9 @@ int riscv_pmu_get_hpm_info(u32 *hw_ctr_width, u32 *num_hw_ctr)
 }
 EXPORT_SYMBOL_GPL(riscv_pmu_get_hpm_info);
 
-static uint8_t pmu_sbi_csr_index(struct perf_event *event)
+static uint8_t pmu_sbi_csr_index(int idx)
 {
-	return pmu_ctr_list[event->hw.idx].csr - CSR_CYCLE;
+	return pmu_ctr_list[idx].csr - CSR_CYCLE;
 }
 
 static unsigned long pmu_sbi_get_filter_flags(struct perf_event *event)
@@ -809,19 +809,21 @@ static u64 pmu_sbi_ctr_read(struct perf_event *event)
 static void pmu_sbi_set_scounteren(void *arg)
 {
 	struct perf_event *event = (struct perf_event *)arg;
+	int idx = READ_ONCE(event->hw.idx);
 
-	if (event->hw.idx != -1)
+	if (idx != -1)
 		csr_write(CSR_SCOUNTEREN,
-			  csr_read(CSR_SCOUNTEREN) | BIT(pmu_sbi_csr_index(event)));
+			  csr_read(CSR_SCOUNTEREN) | BIT(pmu_sbi_csr_index(idx)));
 }
 
 static void pmu_sbi_reset_scounteren(void *arg)
 {
 	struct perf_event *event = (struct perf_event *)arg;
+	int idx = READ_ONCE(event->hw.idx);
 
-	if (event->hw.idx != -1)
+	if (idx != -1)
 		csr_write(CSR_SCOUNTEREN,
-			  csr_read(CSR_SCOUNTEREN) & ~BIT(pmu_sbi_csr_index(event)));
+			  csr_read(CSR_SCOUNTEREN) & ~BIT(pmu_sbi_csr_index(idx)));
 }
 
 static void pmu_sbi_ctr_start(struct perf_event *event, u64 ival)
diff --git a/include/linux/perf/riscv_pmu.h b/include/linux/perf/riscv_pmu.h
index ecaa40370830..4690a5646ae9 100644
--- a/include/linux/perf/riscv_pmu.h
+++ b/include/linux/perf/riscv_pmu.h
@@ -66,7 +66,7 @@ struct riscv_pmu {
 	void		(*event_init)(struct perf_event *event);
 	void		(*event_mapped)(struct perf_event *event, struct mm_struct *mm);
 	void		(*event_unmapped)(struct perf_event *event, struct mm_struct *mm);
-	uint8_t		(*csr_index)(struct perf_event *event);
+	uint8_t		(*csr_index)(int idx);
 
 	struct cpu_hw_events	__percpu *hw_events;
 	struct hlist_node	node;
-- 
2.25.1


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2026-10-28  3:11 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-28  3:11 [PATCH] drivers/perf: riscv: Fix racy hw.idx use in user access paths Li Jinyue

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®