[PATCH 27/28] perf kwork: Bounds check work->cpu before indexing cpus_runtime[]
From: Arnaldo Carvalho de Melo
Date: Sat May 09 2026 - 23:38:49 EST
From: Arnaldo Carvalho de Melo <acme@xxxxxxxxxx>
work->cpu comes from sample->cpu which is (u32)-1 when
PERF_SAMPLE_CPU is absent. Stored as int, this becomes -1
which passes the signed BUG_ON(work->cpu >= MAX_NR_CPUS) but
causes an out-of-bounds access on cpus_runtime[-1].
Replace the BUG_ON with an unsigned bounds check that skips
entries with invalid CPU values, and guard the idle and irq
runtime accumulators the same way.
Reported-by: sashiko-bot@xxxxxxxxxx # Running on a local machine
Cc: Yang Jihong <yangjihong@xxxxxxxxxxxxx>
Cc: Ian Rogers <irogers@xxxxxxxxxx>
Cc: Namhyung Kim <namhyung@xxxxxxxxxx>
Assisted-by: Claude Opus 4.6 (1M context) <noreply@xxxxxxxxxxxxx>
Signed-off-by: Arnaldo Carvalho de Melo <acme@xxxxxxxxxx>
---
tools/perf/builtin-kwork.c | 49 +++++++++++++++++++++++++++++++++-----
tools/perf/util/kwork.h | 1 +
2 files changed, 44 insertions(+), 6 deletions(-)
diff --git a/tools/perf/builtin-kwork.c b/tools/perf/builtin-kwork.c
index 9d3a4c779a41e383..6e124a0f277c8294 100644
--- a/tools/perf/builtin-kwork.c
+++ b/tools/perf/builtin-kwork.c
@@ -424,7 +424,9 @@ static bool profile_event_match(struct perf_kwork *kwork,
u64 time = sample->time;
struct perf_time_interval *ptime = &kwork->ptime;
- if ((kwork->cpu_list != NULL) && !test_bit(cpu, kwork->cpu_bitmap))
+ /* Guard test_bit: cpu == -1 (absent PERF_SAMPLE_CPU) would index past the bitmap */
+ if ((kwork->cpu_list != NULL) &&
+ ((unsigned int)cpu >= MAX_NR_CPUS || !test_bit(cpu, kwork->cpu_bitmap)))
return false;
if (((ptime->start != 0) && (ptime->start > time)) ||
@@ -2008,7 +2010,18 @@ static void top_calc_total_runtime(struct perf_kwork *kwork)
next = rb_first_cached(&class->work_root);
while (next) {
work = rb_entry(next, struct kwork_work, node);
- BUG_ON(work->cpu >= MAX_NR_CPUS);
+ /*
+ * work->cpu comes from sample->cpu which is -1 when
+ * PERF_SAMPLE_CPU is absent. As int that's -1, but as
+ * unsigned it exceeds MAX_NR_CPUS — skip to avoid OOB
+ * on cpus_runtime[].
+ */
+ /* Counted and reported in perf_kwork__top_report() */
+ if ((unsigned int)work->cpu >= MAX_NR_CPUS) {
+ stat->nr_skipped_cpu++;
+ next = rb_next(next);
+ continue;
+ }
stat->cpus_runtime[work->cpu].total += work->total_runtime;
stat->cpus_runtime[MAX_NR_CPUS].total += work->total_runtime;
next = rb_next(next);
@@ -2020,7 +2033,8 @@ static void top_calc_idle_time(struct perf_kwork *kwork,
{
struct kwork_top_stat *stat = &kwork->top_stat;
- if (work->id == 0) {
+ /* See comment in top_calc_total_runtime() */
+ if (work->id == 0 && (unsigned int)work->cpu < MAX_NR_CPUS) {
stat->cpus_runtime[work->cpu].idle += work->total_runtime;
stat->cpus_runtime[MAX_NR_CPUS].idle += work->total_runtime;
}
@@ -2032,6 +2046,12 @@ static void top_calc_irq_runtime(struct perf_kwork *kwork,
{
struct kwork_top_stat *stat = &kwork->top_stat;
+ /* See comment in top_calc_total_runtime() */
+ if ((unsigned int)work->cpu >= MAX_NR_CPUS) {
+ stat->nr_skipped_cpu++;
+ return;
+ }
+
if (type == KWORK_CLASS_IRQ) {
stat->cpus_runtime[work->cpu].irq += work->total_runtime;
stat->cpus_runtime[MAX_NR_CPUS].irq += work->total_runtime;
@@ -2084,12 +2104,21 @@ static void top_calc_cpu_usage(struct perf_kwork *kwork)
if (work->total_runtime == 0)
goto next;
+ /* See comment in top_calc_total_runtime() */
+ if ((unsigned int)work->cpu >= MAX_NR_CPUS) {
+ stat->nr_skipped_cpu++;
+ goto next;
+ }
+
__set_bit(work->cpu, stat->all_cpus_bitmap);
top_subtract_irq_runtime(kwork, work);
- work->cpu_usage = work->total_runtime * 10000 /
- stat->cpus_runtime[work->cpu].total;
+ /* Guard against division by zero if no runtime was accumulated */
+ if (stat->cpus_runtime[work->cpu].total) {
+ work->cpu_usage = work->total_runtime * 10000 /
+ stat->cpus_runtime[work->cpu].total;
+ }
top_calc_idle_time(kwork, work);
next:
@@ -2102,7 +2131,8 @@ static void top_calc_load_runtime(struct perf_kwork *kwork,
{
struct kwork_top_stat *stat = &kwork->top_stat;
- if (work->id != 0) {
+ /* See comment in top_calc_total_runtime() */
+ if (work->id != 0 && (unsigned int)work->cpu < MAX_NR_CPUS) {
stat->cpus_runtime[work->cpu].load += work->total_runtime;
stat->cpus_runtime[MAX_NR_CPUS].load += work->total_runtime;
}
@@ -2170,6 +2200,13 @@ static void perf_kwork__top_report(struct perf_kwork *kwork)
next = rb_next(next);
}
+ if (kwork->top_stat.nr_skipped_cpu) {
+ printf(" Warning: %u work entries with invalid CPU were excluded from totals.\n"
+ " Task runtimes may appear inflated (IRQ time not subtracted).\n"
+ " Consider re-recording with PERF_SAMPLE_CPU enabled.\n",
+ kwork->top_stat.nr_skipped_cpu);
+ }
+
printf("\n");
}
diff --git a/tools/perf/util/kwork.h b/tools/perf/util/kwork.h
index db00269b73f24c66..10290cd779402f9d 100644
--- a/tools/perf/util/kwork.h
+++ b/tools/perf/util/kwork.h
@@ -194,6 +194,7 @@ struct __top_cpus_runtime {
struct kwork_top_stat {
DECLARE_BITMAP(all_cpus_bitmap, MAX_NR_CPUS);
struct __top_cpus_runtime *cpus_runtime;
+ unsigned int nr_skipped_cpu;
};
struct perf_kwork {
--
2.54.0