[PATCH v3 1/9] perf evsel: Clamp sample_id size for ksymbol, bpf, and text_poke events

From: Ian Rogers

Date: Tue Sep 29 2026 - 02:59:06 EST


On kernels without the fix ("perf/core: Restore header fields in
sideband output callbacks") to restore event_id.header.size in
perf_event_ksymbol_output(), perf_event_bpf_output(), and
perf_event_text_poke_output(), concurrent perf sessions cause those
sideband records to be emitted with header.size inflated by multiple
id_header_size increments while the single id_sample is written
immediately after the event payload. Indexing backwards from
event->header.size reads uninitialized ring-buffer bytes at the end of
the record, causing evlist__event2evsel() to fail with -EFAULT.

Add evsel__event_size() to clamp the effective size used to locate the
trailing id_sample for PERF_RECORD_KSYMBOL, PERF_RECORD_BPF_EVENT, and
PERF_RECORD_TEXT_POKE to payload + id_hdr_size while leaving
event->header.size intact for advancing the ring-buffer/file stream.

Fixes: 9aa0bfa370b2 ("perf tools: Handle PERF_RECORD_KSYMBOL")
Fixes: 45178a928a4b ("perf tools: Handle PERF_RECORD_BPF_EVENT")
Fixes: 246eba8e9041 ("perf tools: Add support for PERF_RECORD_TEXT_POKE")
Link: https://lore.kernel.org/r/20260929014206.4175245-1-irogers@xxxxxxxxxx
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@xxxxxxxxxx>
---
tools/perf/util/evlist.c | 6 ++--
tools/perf/util/evsel.c | 59 +++++++++++++++++++++++++++++++++++++++-
tools/perf/util/evsel.h | 1 +
3 files changed, 63 insertions(+), 3 deletions(-)

diff --git a/tools/perf/util/evlist.c b/tools/perf/util/evlist.c
index 9392d912d254..c2402e4791b6 100644
--- a/tools/perf/util/evlist.c
+++ b/tools/perf/util/evlist.c
@@ -973,13 +973,15 @@ static int evlist__event2id(struct evlist *evlist, union perf_event *event, u64
const __u64 *array = event->sample.array;
ssize_t n;

- n = (event->header.size - sizeof(event->header)) >> 3;
-
if (event->header.type == PERF_RECORD_SAMPLE) {
+ n = (event->header.size - sizeof(event->header)) >> 3;
if (evlist__id_pos(evlist) >= n)
return -1;
*id = array[evlist__id_pos(evlist)];
} else {
+ u16 size = evsel__event_size(evlist__first(evlist), event);
+
+ n = (size - sizeof(event->header)) >> 3;
if (evlist__is_pos(evlist) > n)
return -1;
n -= evlist__is_pos(evlist);
diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c
index 3367242c5764..9c5e7510f0c0 100644
--- a/tools/perf/util/evsel.c
+++ b/tools/perf/util/evsel.c
@@ -3216,7 +3216,7 @@ static int perf_evsel__parse_id_sample(const union perf_event *event,
const __u64 *array = event->sample.array;
bool swapped = evsel->needs_swap;
union u64_swap u;
- int i = ((event->header.size - sizeof(event->header)) / sizeof(u64)) - 1;
+ int i = ((evsel__event_size(evsel, event) - sizeof(event->header)) / sizeof(u64)) - 1;

if (type & PERF_SAMPLE_IDENTIFIER) {
if (i < 0)
@@ -3966,6 +3966,63 @@ u16 evsel__id_hdr_size(const struct evsel *evsel)
return size;
}

+/*
+ * Prior to kernel fix, perf_event_ksymbol_output(), perf_event_bpf_output(),
+ * and perf_event_text_poke_output() in kernel/events/core.c did not save and
+ * restore event_id.header.size across perf_iterate_sb() iterations. When
+ * multiple perf_events had attr.ksymbol, attr.bpf_event, or attr.text_poke
+ * enabled, header.size was incremented by id_header_size for each matching
+ * event while only a single id_sample was written immediately after the event
+ * payload. Clamp the effective size used to locate the trailing id_sample to
+ * payload + id_hdr_size so events recorded on unpatched kernels can be parsed
+ * without reading uninitialized ring-buffer bytes.
+ */
+u16 evsel__event_size(const struct evsel *evsel, const union perf_event *event)
+{
+ u16 size = event->header.size;
+ u16 id_hdr_size;
+ size_t payload;
+
+ if (!evsel->core.attr.sample_id_all)
+ return size;
+
+ switch (event->header.type) {
+ case PERF_RECORD_KSYMBOL: {
+ const char *name = event->ksymbol.name;
+ size_t fixed = offsetof(struct perf_record_ksymbol, name);
+ size_t max_len, len;
+
+ if (size <= fixed)
+ return size;
+ max_len = size - fixed;
+ len = strnlen(name, max_len);
+ if (len == max_len)
+ return size;
+ payload = fixed + PERF_ALIGN(len + 1, sizeof(u64));
+ break;
+ }
+ case PERF_RECORD_BPF_EVENT:
+ payload = sizeof(struct perf_record_bpf_event);
+ break;
+ case PERF_RECORD_TEXT_POKE: {
+ size_t fixed = offsetof(struct perf_record_text_poke_event, bytes);
+
+ if (size < fixed)
+ return size;
+ payload = PERF_ALIGN(fixed + (size_t)event->text_poke.old_len +
+ event->text_poke.new_len, sizeof(u64));
+ break;
+ }
+ default:
+ return size;
+ }
+
+ id_hdr_size = evsel__id_hdr_size(evsel);
+ if (payload + id_hdr_size < size)
+ return payload + id_hdr_size;
+ return size;
+}
+
#ifdef HAVE_LIBTRACEEVENT
struct tep_format_field *evsel__field(struct evsel *evsel, const char *name)
{
diff --git a/tools/perf/util/evsel.h b/tools/perf/util/evsel.h
index 5c5799cee601..174f3414fd3c 100644
--- a/tools/perf/util/evsel.h
+++ b/tools/perf/util/evsel.h
@@ -469,6 +469,7 @@ int evsel__parse_sample_timestamp(struct evsel *evsel, union perf_event *event,
u64 *timestamp);

u16 evsel__id_hdr_size(const struct evsel *evsel);
+u16 evsel__event_size(const struct evsel *evsel, const union perf_event *event);

static inline struct evsel *evsel__next(struct evsel *evsel)
{
--
2.56.0.rc1.315.gc6ed9934b7-goog