[PATCH 0/6] perf: fix six memory-safety vulnerabilities in sched/header/subcmd

From: Wang Haoran

Date: Fri May 29 2026 - 02:56:04 EST


>From 9e71ffe9400fd54c4fc958b16229e5628271e4ad Mon Sep 17 00:00:00 2001
From: Wang Haoran <haoranwangsec@xxxxxxxxx>
Date: Thu, 28 May 2026 15:21:08 +0800
Subject: [PATCH 0/6] perf: fix six memory-safety vulnerabilities in sched/header/subcmd

Hi, I found several vulnerabilities in perf module.

This series fixes six memory-safety bugs found in perf version 7.0.6,
in the perf sched stats subsystem and related infrastructure. All six
were confirmed via AddressSanitizer + LeakSanitizer with crafted
perf.data files; the reproducer files are attached to the individual
patch emails.


Privilege note
==============
"perf sched stats record" requires root to capture
scheduler events from the kernel. However, "perf sched stats report"
processes a perf.data file from disk and requires NO special privileges.
This means an unprivileged attacker can hand a crafted perf.data to any
user who runs "perf sched stats report" -- the attack surface is fully
reachable without administrator rights.

Crash-to-fix mapping
====================

crash_err234_iter50.data -> patch 1 (memory leaks in schedstat)
crash_sig6_iter840.data -> patch 2 (heap overflow via bitmap_zalloc)
crash_sig7_iter210.data -> patch 3 (SIGBUS via out-of-bounds mmap)
crash_sig11_iter2.data -> patch 4 (array OOB in domain index)
(no poc) -> patch 5 (list_first_entry on empty list)
crash_err255_iter46.data -> patch 6 (astrcat leak in parse_options)

Vulnerability summary
=====================

1. Memory leaks in perf_sched__process_schedstat() / free_schedstat()
(tools/perf/builtin-sched.c)

Three distinct leak paths:
a) When zalloc() of the inner data pointer (cpu_data or domain_data)
fails, the outer struct is returned without freeing the parent.
b) In the after_workload_flag=true branch the temporary struct and
its embedded data pointer are used then discarded without being
freed.
c) free_schedstat() frees each cpu/domain node but not the
cpu_data/domain_data pointers allocated inside each node.

ASAN/LSan reports 72-144 bytes leaked per crafted event processed.

2. Heap buffer overflow via u64->int truncation in do_read_bitmap()
(tools/perf/util/header.c)

bitmap_zalloc() takes an int but do_read_bitmap() passes a raw u64
read from the file. If size > INT_MAX the int wraps to a small
value, a tiny buffer is allocated, and the subsequent loop that
reads BITS_TO_U64(size) u64 words from the file writes arbitrarily
far past the end of the allocation (heap buffer overflow).

3. SIGBUS from data.offset beyond file size
(tools/perf/util/header.c)

A crafted perf.data can set perf_file_header.data.offset to a value
larger than the actual file size. mmap() succeeds because the
kernel accepts out-of-file offsets, but any access to the mapped
region triggers SIGBUS. Confirmed with data.offset=0xff68=65384
against a 4760-byte file.

4. Array out-of-bounds write via unchecked domain index
(tools/perf/util/header.c)

process_cpu_domain_info() reads a domain index from the file and
uses it directly to index cd_map[cpu]->domains[], an array of
max_sched_domains entries. A domain value >= max_sched_domains
causes an out-of-bounds write into adjacent heap memory.

5. list_first_entry() misuse on potentially-empty lists
(tools/perf/builtin-sched.c)

get_all_cpu_stats() and show_schedstat_data() call
list_first_entry() which, unlike list_first_entry_or_null(),
never returns NULL -- it computes container_of() on the list head
itself when the list is empty, producing a garbage pointer. The
NULL checks that follow are therefore dead code. A crafted
perf.data that causes an empty list makes these functions dereference
the garbage pointer. Replaced with list_first_entry_or_null() plus
proper NULL guards.

6. Memory leak in parse_options_subcommand()
(tools/lib/subcmd/parse-options.c)

When subcommands are present and no usage string has been supplied,
parse_options_subcommand() builds a usage string with astrcat() and
stores the pointer in usagestr[0]. The pointer is never freed,
causing a 73-byte leak on every invocation.

Testing
=======

Affected version: perf 7.0.6

Each patch was verified with:

$ make -C tools/perf EXTRA_CFLAGS="-fsanitize=address,leak \
-fno-omit-frame-pointer -Wno-error=stringop-truncation" \
-j$(nproc)

$ ./perf sched stats report -i <crash-file>

Before the patch the corresponding crash file produced the ASAN/LSan
report listed above. After the patch the same file is either rejected
cleanly (patches 2-4) or processed without any error report (patches
1, 5, 6).

The reproducer files (crash_*.data) are attached to the individual
patch emails; each is a minimal crafted perf.data (~4 KB) that
isolates exactly one bug.

Wang Haoran (6):
perf/sched: fix memory leaks in schedstat processing
perf/header: validate bitmap size before allocation in do_read_bitmap
perf/header: reject data offset beyond file size
perf/header: add bounds check for domain index in process_cpu_domain_info
perf/sched: replace list_first_entry with list_first_entry_or_null
subcmd: fix memory leak in parse_options_subcommand

tools/lib/subcmd/parse-options.c | 3 ++-
tools/perf/builtin-sched.c | 32 +++++++++++++++++++++++++-------
tools/perf/util/header.c | 17 +++++++++++++++++
3 files changed, 44 insertions(+), 8 deletions(-)

--
2.53.0