[PATCH 19/29] perf header: Validate f_attr.ids section before use in perf_session__read_header()
From: Arnaldo Carvalho de Melo
Date: Tue May 26 2026 - 17:23:08 EST
From: Arnaldo Carvalho de Melo <acme@xxxxxxxxxx>
perf_session__read_header() reads f_attr.ids.size from the perf.data
file and divides it by sizeof(u64) to compute nr_ids, which is
declared as int. No validation is performed on the value before it
is used to allocate arrays and drive a read loop.
On 32-bit architectures, a crafted f_attr.ids.size of 0x100000000
(4 GB) produces nr_ids = 0x20000000, but the allocation size
1 * 0x20000000 * 8 overflows size_t to 0, so zalloc(0) returns a
valid pointer. The subsequent loop writes 0x20000000 IDs into that
zero-length buffer, corrupting the heap.
On 64-bit, the u64-to-int truncation silently drops high bits,
processing fewer IDs than the file claims. While not exploitable,
this is a data integrity issue.
Add validation before using f_attr.ids:
- Cap nr_attrs (attrs.size / attr_size) to MAX_NR_ATTRS (1 << 16)
with overflow-safe u64 comparison before assigning to int
- Reject ids.size not aligned to sizeof(u64)
- Cap ids.size / sizeof(u64) to MAX_IDS_PER_ATTR (1 << 24) to
prevent int truncation and size_t overflow on 32-bit
- Reject ids sections that extend past the end of the file,
guarded by S_ISREG() so non-regular files (block devices,
pipes) are not falsely rejected
Also fix perf_header__getbuffer64() to set errno = EIO when
readn() returns 0 (EOF). Without this, the out_errno path in
perf_session__read_header() returns -errno which is 0 (success)
on truncated files, causing downstream NULL dereferences.
Reported-by: sashiko-bot@xxxxxxxxxx # Running on a local machine
Cc: Ian Rogers <irogers@xxxxxxxxxx>
Cc: Jiri Olsa <jolsa@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/util/header.c | 77 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 76 insertions(+), 1 deletion(-)
diff --git a/tools/perf/util/header.c b/tools/perf/util/header.c
index f4e0e257ff7226ac..fe23bbd8370c0190 100644
--- a/tools/perf/util/header.c
+++ b/tools/perf/util/header.c
@@ -64,6 +64,25 @@
#include <event-parse.h>
#endif
+/*
+ * nr_ids * sizeof(struct perf_sample_id) must not overflow
+ * size_t on 32-bit; the struct is ~104 bytes (32-bit) or
+ * ~184 bytes (64-bit), so 1<<24 (16M) keeps the product
+ * under 2 GB on 32-bit.
+ *
+ * This is a per-attribute cap only — the total across all
+ * attributes is not capped because legitimate high-core-count
+ * workloads (e.g. 5000 tracepoints × 4096 CPUs) can exceed
+ * a single-attribute limit.
+ */
+#define MAX_IDS_PER_ATTR (1 << 24)
+/*
+ * Cap nr_attrs to prevent resource exhaustion from crafted
+ * files. 65536 is well beyond any real workload (perf stat
+ * typically uses < 100 events) but prevents u64-to-int
+ * truncation on the attr count.
+ */
+#define MAX_NR_ATTRS (1 << 16)
#define MAX_BPF_DATA_LEN (256 * 1024 * 1024)
#define MAX_BPF_PROGS 131072
#define MAX_CACHE_ENTRIES 32768
@@ -4468,8 +4487,13 @@ int perf_session__inject_header(struct perf_session *session,
static int perf_header__getbuffer64(struct perf_header *header,
int fd, void *buf, size_t size)
{
- if (readn(fd, buf, size) <= 0)
+ ssize_t n = readn(fd, buf, size);
+
+ if (n <= 0) {
+ if (n == 0)
+ errno = EIO;
return -1;
+ }
if (header->needs_swap)
mem_bswap_64(buf, size);
@@ -4803,6 +4827,8 @@ static int read_attr(int fd, struct perf_header *ph,
if (ret <= 0) {
pr_debug("cannot read %d bytes of header attr\n",
PERF_ATTR_SIZE_VER0);
+ if (ret == 0)
+ errno = EIO;
return -1;
}
@@ -4903,6 +4929,7 @@ int perf_session__read_header(struct perf_session *session)
struct perf_file_header f_header;
struct perf_file_attr f_attr;
u64 f_id;
+ struct stat input_stat;
int nr_attrs, nr_ids, i, j, err = -ENOMEM;
int fd = perf_data__fd(data);
@@ -4951,6 +4978,15 @@ int perf_session__read_header(struct perf_session *session)
return -EINVAL;
}
+ if (fstat(fd, &input_stat) < 0)
+ return -errno;
+
+ /* Check before assigning to int to avoid u64-to-int truncation */
+ if (f_header.attrs.size / f_header.attr_size > MAX_NR_ATTRS) {
+ pr_err("Too many attributes: %" PRIu64 " (max %d)\n",
+ f_header.attrs.size / f_header.attr_size, MAX_NR_ATTRS);
+ return -EINVAL;
+ }
nr_attrs = f_header.attrs.size / f_header.attr_size;
lseek(fd, f_header.attrs.offset, SEEK_SET);
@@ -4967,6 +5003,45 @@ int perf_session__read_header(struct perf_session *session)
perf_event__attr_swap(&f_attr.attr);
}
+ /*
+ * Validate ids section: must be aligned to u64, and
+ * the count must fit in an int to avoid truncation in
+ * nr_ids and size_t overflow in perf_evsel__alloc_id()
+ * on 32-bit architectures.
+ */
+ if (f_attr.ids.size % sizeof(u64)) {
+ pr_err("Invalid ids section size %" PRIu64 " for attr %d, not aligned to u64\n",
+ f_attr.ids.size, i);
+ err = -EINVAL;
+ goto out_delete_evlist;
+ }
+
+ /*
+ * Cap the ID count to avoid int truncation of nr_ids
+ * on 64-bit and size_t overflow in the allocation
+ * paths (nr_ids * sizeof(u64), nr_ids *
+ * sizeof(struct perf_sample_id)) on 32-bit.
+ */
+ if (f_attr.ids.size / sizeof(u64) > MAX_IDS_PER_ATTR) {
+ pr_err("Invalid ids section size %" PRIu64 " for attr %d, too many IDs\n",
+ f_attr.ids.size, i);
+ err = -EINVAL;
+ goto out_delete_evlist;
+ }
+
+ /*
+ * FIXME: see perf_header__process_sections() — block
+ * devices bypass this check because st_size is 0.
+ */
+ if (S_ISREG(input_stat.st_mode) &&
+ (f_attr.ids.offset > (u64)input_stat.st_size ||
+ f_attr.ids.size > (u64)input_stat.st_size - f_attr.ids.offset)) {
+ pr_err("Invalid ids section for attr %d: offset=%" PRIu64 " size=%" PRIu64 " exceeds file size %" PRIu64 "\n",
+ i, f_attr.ids.offset, f_attr.ids.size, (u64)input_stat.st_size);
+ err = -EINVAL;
+ goto out_delete_evlist;
+ }
+
tmp = lseek(fd, 0, SEEK_CUR);
evsel = evsel__new(&f_attr.attr);
--
2.54.0