perf header: Validate f_attr.ids section before use in perf_session__read_header()

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@kernel.org # Running on a local machine
Reviewed-by: Ian Rogers <irogers@google.com>
Cc: Jiri Olsa <jolsa@kernel.org>
Cc: Namhyung Kim <namhyung@kernel.org>
Assisted-by: Claude:claude-opus-4.6-1m
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
This commit is contained in:
Arnaldo Carvalho de Melo 2026-05-02 14:28:06 -03:00
parent 7d19f2008a
commit 7685cc0f2e

View File

@ -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);