perf session: Validate HEADER_ATTR attr.size before swapping

Harden PERF_RECORD_HEADER_ATTR handling against crafted perf.data:

- Validate attr.size: must be >= PERF_ATTR_SIZE_VER0, a multiple
  of sizeof(u64), and fit within the event payload.
- Copy only min(attr.size, sizeof(struct perf_event_attr)) bytes
  into a local attr, zeroing the rest so legacy files don't leak
  adjacent event data into new fields.
- Keep the original attr.size so perf_event__synthesize_attr()
  uses it for both allocation and ID-array placement.

Fix perf_event__synthesize_attr() to use attr->size (not the
compiled sizeof) for event allocation and layout, so perf inject
correctly re-synthesizes attrs from files recorded by a different
perf version.  Without this, the ID array destination pointer
(computed via perf_record_header_attr_id()) would be inconsistent
with the allocation when attr->size differs from sizeof.

Also fix the parse-no-sample-id-all test to set attr.size, which
is now validated, and improve error handling in read_attr() for
short reads and invalid attr sizes.

Handle ABI0 pipe/inject events where attr.size is 0: use a local
attr_size variable set to PERF_ATTR_SIZE_VER0 for both the bounded
copy and ID array position, instead of writing back to the event.
Native-endian files may be MAP_SHARED (read-only mmap), so writing
to the event buffer would SIGSEGV.  The swap path handles ABI0 in
perf_event__attr_swap() which writes to the MAP_PRIVATE copy.

header.size alignment is now validated centrally in
perf_session__process_event() (see "Add minimum event size and
alignment validation").

Reported-by: sashiko-bot@kernel.org # Running on a local machine
Reviewed-by: Ian Rogers <irogers@google.com>
Cc: Adrian Hunter <adrian.hunter@intel.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 13:07:27 -03:00
parent 3ade633da7
commit 6a38b515a5
5 changed files with 166 additions and 15 deletions

View File

@ -229,6 +229,7 @@ static int perf_event__repipe_attr(const struct perf_tool *tool,
struct perf_inject *inject = container_of(tool, struct perf_inject,
tool);
struct perf_event_attr attr;
u32 raw_attr_size, attr_size;
size_t n_ids;
u64 *ids;
int ret;
@ -244,24 +245,34 @@ static int perf_event__repipe_attr(const struct perf_tool *tool,
if (!inject->itrace_synth_opts.set)
return perf_event__repipe_synth(tool, event);
if (event->header.size < sizeof(struct perf_event_header) + sizeof(u64)) {
if (event->header.size < sizeof(struct perf_event_header) + PERF_ATTR_SIZE_VER0) {
pr_err("Attribute event size %u is too small\n", event->header.size);
return -EINVAL;
}
if (event->header.size - sizeof(event->header) < event->attr.attr.size) {
/*
* ABI0 pipe/inject events have attr.size == 0; default to
* PERF_ATTR_SIZE_VER0 (the ABI0 footprint) for the bounded
* copy and ID array position. Same pattern as
* perf_event__process_attr() in header.c.
*/
raw_attr_size = event->attr.attr.size;
attr_size = raw_attr_size ?: PERF_ATTR_SIZE_VER0;
if (raw_attr_size && (raw_attr_size < PERF_ATTR_SIZE_VER0 ||
raw_attr_size > event->header.size - sizeof(event->header))) {
pr_err("Attribute event size %u is too small for attr.size %u\n",
event->header.size, event->attr.attr.size);
event->header.size, raw_attr_size);
return -EINVAL;
}
memset(&attr, 0, sizeof(attr));
memcpy(&attr, &event->attr.attr,
min_t(size_t, sizeof(attr), (size_t)event->attr.attr.size));
min_t(size_t, sizeof(attr), attr_size));
n_ids = event->header.size - sizeof(event->header) - event->attr.attr.size;
n_ids = event->header.size - sizeof(event->header) - attr_size;
n_ids /= sizeof(u64);
ids = perf_record_header_attr_id(event);
ids = (void *)&event->attr.attr + attr_size;
attr.size = sizeof(struct perf_event_attr);
attr.sample_type &= ~PERF_SAMPLE_AUX;

View File

@ -82,6 +82,9 @@ static int test__parse_no_sample_id_all(struct test_suite *test __maybe_unused,
.type = PERF_RECORD_HEADER_ATTR,
.size = sizeof(struct test_attr_event),
},
.attr = {
.size = sizeof(struct perf_event_attr),
},
.id = 1,
};
struct test_attr_event event2 = {
@ -89,6 +92,9 @@ static int test__parse_no_sample_id_all(struct test_suite *test __maybe_unused,
.type = PERF_RECORD_HEADER_ATTR,
.size = sizeof(struct test_attr_event),
},
.attr = {
.size = sizeof(struct perf_event_attr),
},
.id = 2,
};
struct perf_record_mmap event3 = {

View File

@ -4770,9 +4770,15 @@ static int read_attr(int fd, struct perf_header *ph,
if (sz == 0) {
/* assume ABI0 */
sz = PERF_ATTR_SIZE_VER0;
} else if (sz < PERF_ATTR_SIZE_VER0) {
pr_debug("bad attr size %zu, expected at least %d\n",
sz, PERF_ATTR_SIZE_VER0);
errno = EINVAL;
return -1;
} else if (sz > our_sz) {
pr_debug("file uses a more recent and unsupported ABI"
" (%zu bytes extra)\n", sz - our_sz);
errno = EINVAL;
return -1;
}
/* what we have not yet read and that we know about */
@ -4782,11 +4788,21 @@ static int read_attr(int fd, struct perf_header *ph,
ptr += PERF_ATTR_SIZE_VER0;
ret = readn(fd, ptr, left);
if (ret <= 0) {
if (ret == 0)
errno = EIO;
return -1;
}
}
/* read perf_file_section, ids are read in caller */
ret = readn(fd, &f_attr->ids, sizeof(f_attr->ids));
if (ret <= 0) {
if (ret == 0)
errno = EIO;
return -1;
}
return ret <= 0 ? -1 : 0;
return 0;
}
#ifdef HAVE_LIBTRACEEVENT
@ -5094,11 +5110,42 @@ int perf_event__process_attr(const struct perf_tool *tool __maybe_unused,
union perf_event *event,
struct evlist **pevlist)
{
u32 i, n_ids;
struct perf_event_attr attr;
u32 i, n_ids, raw_attr_size;
u64 *ids;
size_t attr_size, copy_size;
struct evsel *evsel;
struct evlist *evlist = *pevlist;
/*
* HEADER_ATTR event layout (pipe/inject mode):
*
* [header (8 bytes)] [attr (attr_size bytes)] [id0 id1 ... idN]
* |<------------------ header.size --------------------------->|
*
* attr_size varies across perf versions: VER0 = 64 bytes,
* current sizeof(struct perf_event_attr) = larger. A newer
* producer may emit a larger attr than we understand.
*
* attr.size == 0 (ABI0) means the producer didn't set it
* (e.g., bench/inject-buildid, older perf). Treat as VER0.
*
* Require 8-byte alignment so the u64 ID array is aligned
* and attr.size fits cleanly within the payload.
*
* Read attr.size once the event may be on a shared mmap
* and re-reading could yield a different value.
*/
raw_attr_size = event->attr.attr.size;
if (event->header.size < sizeof(event->header) + PERF_ATTR_SIZE_VER0 ||
(raw_attr_size && (raw_attr_size < PERF_ATTR_SIZE_VER0 ||
raw_attr_size % sizeof(u64) ||
raw_attr_size > event->header.size - sizeof(event->header)))) {
pr_err("PERF_RECORD_HEADER_ATTR: invalid attr.size %u (event size %u, min %d)\n",
raw_attr_size, event->header.size, PERF_ATTR_SIZE_VER0);
return -EINVAL;
}
if (dump_trace)
perf_event__fprintf_attr(event, stdout);
@ -5108,13 +5155,46 @@ int perf_event__process_attr(const struct perf_tool *tool __maybe_unused,
return -ENOMEM;
}
evsel = evsel__new(&event->attr.attr);
/*
* attr_size = footprint of the attr in the event determines
* where the ID array starts. For ABI0, assume VER0 (64 bytes).
*
* copy_size = how much we copy into our local struct, capped at
* sizeof(attr) so a newer producer's larger attr doesn't
* overflow. Fields beyond copy_size are zeroed.
*
* Do NOT write attr_size back to the event native-endian
* files use MAP_SHARED (read-only), writing would SIGSEGV.
* The swap path handles ABI0 in perf_event__attr_swap()
* which writes to the writable MAP_PRIVATE copy instead.
*/
attr_size = raw_attr_size ?: PERF_ATTR_SIZE_VER0;
copy_size = min(attr_size, sizeof(attr));
memcpy(&attr, &event->attr.attr, copy_size);
if (copy_size < sizeof(attr))
memset((void *)&attr + copy_size, 0, sizeof(attr) - copy_size);
/*
* Normalize ABI0: the swap path sets attr.size = VER0 on the
* event, but the native path leaves it as 0. Set it on the
* local copy so perf inject re-synthesizes with consistent
* layout regardless of endianness.
*/
attr.size = attr_size;
evsel = evsel__new(&attr);
if (evsel == NULL)
return -ENOMEM;
evlist__add(evlist, evsel);
n_ids = event->header.size - sizeof(event->header) - event->attr.attr.size;
/*
* IDs occupy the remainder after header + attr. Use attr_size
* (not copy_size) even if the producer's attr is larger than
* our struct, the IDs start after attr_size bytes in the event.
* Validation above guarantees attr_size <= payload size.
*/
n_ids = event->header.size - sizeof(event->header) - attr_size;
n_ids = n_ids / sizeof(u64);
/*
* We don't have the cpu and thread maps on the header, so
@ -5124,7 +5204,13 @@ int perf_event__process_attr(const struct perf_tool *tool __maybe_unused,
if (perf_evsel__alloc_id(&evsel->core, 1, n_ids))
return -ENOMEM;
ids = perf_record_header_attr_id(event);
/*
* Locate IDs at attr_size bytes past the attr start in the
* event. Cannot use perf_record_header_attr_id() that
* macro reads event->attr.attr.size, which is 0 for ABI0
* on the native-endian path (no swap handler to fix it up).
*/
ids = (void *)&event->attr.attr + attr_size;
for (i = 0; i < n_ids; i++) {
perf_evlist__id_add(&evlist->core, &evsel->core, 0, i, ids[i]);
}

View File

@ -623,8 +623,39 @@ do { \
static int perf_event__hdr_attr_swap(union perf_event *event,
bool sample_id_all __maybe_unused)
{
u32 attr_size, payload_size;
size_t size;
/*
* Validate attr.size (still foreign-endian) before calling
* perf_event__attr_swap(), which uses it via bswap_safe()
* to decide which fields to swap. A crafted attr.size
* larger than the event payload would swap past the event
* boundary and corrupt adjacent memory.
*
* header.size alignment is already validated by
* perf_session__process_event(). The min_size table
* guarantees header.size >= sizeof(header) +
* PERF_ATTR_SIZE_VER0, so attr.size is safe to access.
*/
attr_size = bswap_32(event->attr.attr.size);
/*
* ABI0: size field not set. This only happens in pipe/inject
* mode where HEADER_ATTR events carry their own attr. For
* regular perf.data files, read_attr() uses f_header.attr_size
* from the file header instead. Assume PERF_ATTR_SIZE_VER0.
*/
if (!attr_size)
attr_size = PERF_ATTR_SIZE_VER0;
payload_size = event->header.size - sizeof(event->header);
if (attr_size < PERF_ATTR_SIZE_VER0 || attr_size % sizeof(u64) ||
attr_size > payload_size) {
pr_err("PERF_RECORD_HEADER_ATTR: invalid attr.size %u (min: %d, max: %u, 8-byte aligned)\n",
attr_size, PERF_ATTR_SIZE_VER0, payload_size);
return -1;
}
perf_event__attr_swap(&event->attr.attr);
size = event->header.size;

View File

@ -2181,11 +2181,21 @@ int perf_event__synthesize_attr(const struct perf_tool *tool, struct perf_event_
u32 ids, u64 *id, perf_event__handler_t process)
{
union perf_event *ev;
size_t size;
size_t attr_size, size;
int err;
size = sizeof(struct perf_event_attr);
size = PERF_ALIGN(size, sizeof(u64));
/*
* Use attr->size for the event layout, not the compiled
* sizeof(struct perf_event_attr), so that synthesized events
* match the source perf.data layout. This matters for perf
* inject, which re-synthesizes attrs from a file that may
* have been recorded by a different version of perf.
* perf_record_header_attr_id() locates the ID array at
* attr->size bytes past the attr.
*/
attr_size = attr->size ?: sizeof(struct perf_event_attr);
size = PERF_ALIGN(attr_size, sizeof(u64));
size += sizeof(struct perf_event_header);
size += ids * sizeof(u64);
@ -2194,7 +2204,14 @@ int perf_event__synthesize_attr(const struct perf_tool *tool, struct perf_event_
if (ev == NULL)
return -ENOMEM;
ev->attr.attr = *attr;
/*
* Copy only the bytes we understand; zalloc ensures that any
* extra bytes between sizeof(struct perf_event_attr) and
* attr_size are zero when the source file uses a newer, larger
* struct.
*/
memcpy(&ev->attr.attr, attr, min(sizeof(struct perf_event_attr), attr_size));
ev->attr.attr.size = attr_size;
memcpy(perf_record_header_attr_id(ev), id, ids * sizeof(u64));
ev->attr.header.type = PERF_RECORD_HEADER_ATTR;