perf header: Validate null-termination in PERF_RECORD_EVENT_UPDATE string fields

strdup(ev->unit) and strdup(ev->name) read until '\0' with no
guarantee the string is null-terminated within event->header.size.
The dump_trace fprintf path has the same problem with %s.

Validate before either path runs — same class of bug fixed for
MMAP/MMAP2/COMM/CGROUP by perf_event__check_nul().

Also harden the event_update swap handler to:
- Validate SCALE event size before swapping the double at
  offset 24, which exceeds the 24-byte min_size.
- Validate CPUS event size before accessing the cpu_map
  type/nr/long_size fields, which also start at the min_size
  boundary.
- Swap CPUS variant fields (type, nr, long_size) so the
  processing path sees native byte order.

Add validation in perf_event__process_event_update() for all
event update variants (UNIT, NAME, SCALE, CPUS) before
dump_trace or processing.

Validate CPUS nr against payload size for both PERF_CPU_MAP__CPUS
and PERF_CPU_MAP__MASK types on the fprintf (dump_trace) path:
- CPUS: check nr does not exceed available cpu entries
- MASK: check nr does not exceed available mask entries for
  both mask32 (long_size == 4) and mask64 (long_size == 8)
  layouts, with underflow guards on the offsetof subtraction

Fix a missing break before the default case in the CPUS
switch path.

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 13:31:40 -03:00
parent 355ca55e3e
commit 3c18544fb1
2 changed files with 242 additions and 7 deletions

View File

@ -5117,15 +5117,76 @@ size_t perf_event__fprintf_event_update(union perf_event *event, FILE *fp)
switch (ev->type) {
case PERF_EVENT_UPDATE__SCALE:
if (event->header.size < offsetof(struct perf_record_event_update, scale) +
sizeof(ev->scale)) {
ret += fprintf(fp, "... scale: (truncated)\n");
break;
}
ret += fprintf(fp, "... scale: %f\n", ev->scale.scale);
break;
case PERF_EVENT_UPDATE__UNIT:
ret += fprintf(fp, "... unit: %s\n", ev->unit);
case PERF_EVENT_UPDATE__NAME: {
size_t str_off = offsetof(struct perf_record_event_update, unit);
size_t max_len = event->header.size > str_off ?
event->header.size - str_off : 0;
if (max_len == 0 || strnlen(ev->unit, max_len) == max_len) {
ret += fprintf(fp, "... %s: (unterminated)\n",
ev->type == PERF_EVENT_UPDATE__UNIT ? "unit" : "name");
break;
}
ret += fprintf(fp, "... %s: %s\n",
ev->type == PERF_EVENT_UPDATE__UNIT ? "unit" : "name",
ev->unit);
break;
case PERF_EVENT_UPDATE__NAME:
ret += fprintf(fp, "... name: %s\n", ev->name);
break;
case PERF_EVENT_UPDATE__CPUS:
}
case PERF_EVENT_UPDATE__CPUS: {
size_t cpus_off = offsetof(struct perf_record_event_update, cpus);
u32 cpus_payload;
if (event->header.size < cpus_off + sizeof(__u16) +
sizeof(struct perf_record_range_cpu_map)) {
ret += fprintf(fp, "... cpus: (truncated)\n");
break;
}
/*
* Validate nr against payload this function may be
* called from the stub handler (dump_trace path) which
* bypasses perf_event__process_event_update() validation.
*/
cpus_payload = event->header.size - cpus_off;
if (ev->cpus.cpus.type == PERF_CPU_MAP__CPUS) {
if (cpus_payload < offsetof(struct perf_record_cpu_map_data, cpus_data.cpu) ||
ev->cpus.cpus.cpus_data.nr >
(cpus_payload - offsetof(struct perf_record_cpu_map_data, cpus_data.cpu)) /
sizeof(ev->cpus.cpus.cpus_data.cpu[0])) {
ret += fprintf(fp, "... cpus: nr %u exceeds payload\n",
ev->cpus.cpus.cpus_data.nr);
break;
}
} else if (ev->cpus.cpus.type == PERF_CPU_MAP__MASK) {
if (ev->cpus.cpus.mask32_data.long_size == 4) {
if (cpus_payload < offsetof(struct perf_record_cpu_map_data, mask32_data.mask) ||
ev->cpus.cpus.mask32_data.nr >
(cpus_payload - offsetof(struct perf_record_cpu_map_data, mask32_data.mask)) /
sizeof(ev->cpus.cpus.mask32_data.mask[0])) {
ret += fprintf(fp, "... cpus: mask nr %u exceeds payload\n",
ev->cpus.cpus.mask32_data.nr);
break;
}
} else if (ev->cpus.cpus.mask64_data.long_size == 8) {
if (cpus_payload < offsetof(struct perf_record_cpu_map_data, mask64_data.mask) ||
ev->cpus.cpus.mask64_data.nr >
(cpus_payload - offsetof(struct perf_record_cpu_map_data, mask64_data.mask)) /
sizeof(ev->cpus.cpus.mask64_data.mask[0])) {
ret += fprintf(fp, "... cpus: mask nr %u exceeds payload\n",
ev->cpus.cpus.mask64_data.nr);
break;
}
}
}
ret += fprintf(fp, "... ");
map = cpu_map__new_data(&ev->cpus.cpus);
@ -5135,6 +5196,7 @@ size_t perf_event__fprintf_event_update(union perf_event *event, FILE *fp)
} else
ret += fprintf(fp, "failed to get cpus\n");
break;
}
default:
ret += fprintf(fp, "... unknown type\n");
break;
@ -5269,6 +5331,83 @@ int perf_event__process_event_update(const struct perf_tool *tool __maybe_unused
struct evsel *evsel;
struct perf_cpu_map *map;
/*
* Validate payload before dump_trace or processing both
* paths access variant-specific fields without further checks.
*/
if (ev->type == PERF_EVENT_UPDATE__UNIT ||
ev->type == PERF_EVENT_UPDATE__NAME) {
size_t str_off = offsetof(struct perf_record_event_update, unit);
size_t max_len = event->header.size > str_off ?
event->header.size - str_off : 0;
if (max_len == 0 || strnlen(ev->unit, max_len) == max_len) {
pr_warning("WARNING: PERF_RECORD_EVENT_UPDATE: %s not null-terminated, skipping\n",
ev->type == PERF_EVENT_UPDATE__UNIT ? "unit" : "name");
return 0;
}
} else if (ev->type == PERF_EVENT_UPDATE__SCALE) {
if (event->header.size < offsetof(struct perf_record_event_update, scale) +
sizeof(ev->scale)) {
pr_warning("WARNING: PERF_RECORD_EVENT_UPDATE: SCALE payload too small, skipping\n");
return 0;
}
} else if (ev->type == PERF_EVENT_UPDATE__CPUS) {
size_t cpus_off = offsetof(struct perf_record_event_update, cpus);
size_t min_cpus = sizeof(__u16) +
sizeof(struct perf_record_range_cpu_map);
u32 cpus_payload;
if (event->header.size < cpus_off + min_cpus) {
pr_warning("WARNING: PERF_RECORD_EVENT_UPDATE: CPUS payload too small, skipping\n");
return 0;
}
/*
* Validate per-variant nr against the remaining
* payload on the native path the swap path clamps
* nr in perf_event__event_update_swap(), but native
* events are read-only and cannot be clamped in place.
* cpu_map__new_data() trusts nr for allocation and
* iteration, so unchecked values cause OOB reads.
*/
cpus_payload = event->header.size - cpus_off;
switch (ev->cpus.cpus.type) {
case PERF_CPU_MAP__CPUS:
if (ev->cpus.cpus.cpus_data.nr >
(cpus_payload - offsetof(struct perf_record_cpu_map_data, cpus_data.cpu)) /
sizeof(ev->cpus.cpus.cpus_data.cpu[0])) {
pr_warning("WARNING: EVENT_UPDATE CPUS: nr %u exceeds payload, skipping\n",
ev->cpus.cpus.cpus_data.nr);
return 0;
}
break;
case PERF_CPU_MAP__MASK:
if (ev->cpus.cpus.mask32_data.long_size == 4) {
if (cpus_payload < offsetof(struct perf_record_cpu_map_data, mask32_data.mask) ||
ev->cpus.cpus.mask32_data.nr >
(cpus_payload - offsetof(struct perf_record_cpu_map_data, mask32_data.mask)) /
sizeof(ev->cpus.cpus.mask32_data.mask[0])) {
pr_warning("WARNING: EVENT_UPDATE MASK: nr %u exceeds payload, skipping\n",
ev->cpus.cpus.mask32_data.nr);
return 0;
}
} else if (ev->cpus.cpus.mask64_data.long_size == 8) {
if (cpus_payload < offsetof(struct perf_record_cpu_map_data, mask64_data.mask) ||
ev->cpus.cpus.mask64_data.nr >
(cpus_payload - offsetof(struct perf_record_cpu_map_data, mask64_data.mask)) /
sizeof(ev->cpus.cpus.mask64_data.mask[0])) {
pr_warning("WARNING: EVENT_UPDATE MASK: nr %u exceeds payload, skipping\n",
ev->cpus.cpus.mask64_data.nr);
return 0;
}
}
break;
default:
break;
}
}
if (dump_trace)
perf_event__fprintf_event_update(event, stdout);
@ -5300,6 +5439,7 @@ int perf_event__process_event_update(const struct perf_tool *tool __maybe_unused
evsel->core.pmu_cpus = map;
} else
pr_err("failed to get event_update cpus\n");
break;
default:
break;
}

View File

@ -708,8 +708,103 @@ static int perf_event__build_id_swap(union perf_event *event,
static int perf_event__event_update_swap(union perf_event *event,
bool sample_id_all __maybe_unused)
{
event->event_update.type = bswap_64(event->event_update.type);
event->event_update.id = bswap_64(event->event_update.id);
struct perf_record_event_update *ev = &event->event_update;
ev->type = bswap_64(ev->type);
ev->id = bswap_64(ev->id);
/*
* Swap variant-specific fields so the processing path
* sees native byte order.
*/
if (ev->type == PERF_EVENT_UPDATE__SCALE) {
if (event->header.size < offsetof(struct perf_record_event_update, scale) +
sizeof(ev->scale))
return -1;
mem_bswap_64(&ev->scale.scale, sizeof(ev->scale.scale));
} else if (ev->type == PERF_EVENT_UPDATE__CPUS) {
u32 cpus_payload;
struct perf_record_cpu_map_data *data = &ev->cpus.cpus;
/* CPUS fields start at the same offset as scale (union) */
if (event->header.size < offsetof(struct perf_record_event_update, cpus) +
sizeof(__u16) + sizeof(struct perf_record_range_cpu_map))
return -1;
cpus_payload = event->header.size - offsetof(struct perf_record_event_update, cpus);
data->type = bswap_16(data->type);
/*
* Full swap including array elements same logic as
* perf_event__cpu_map_swap() but scoped to the
* embedded cpu_map_data within EVENT_UPDATE.
*/
switch (data->type) {
case PERF_CPU_MAP__CPUS: {
u16 nr, max_nr;
data->cpus_data.nr = bswap_16(data->cpus_data.nr);
nr = data->cpus_data.nr;
max_nr = (cpus_payload - offsetof(struct perf_record_cpu_map_data,
cpus_data.cpu)) /
sizeof(data->cpus_data.cpu[0]);
if (nr > max_nr) {
nr = max_nr;
data->cpus_data.nr = nr;
}
for (unsigned int i = 0; i < nr; i++)
data->cpus_data.cpu[i] = bswap_16(data->cpus_data.cpu[i]);
break;
}
case PERF_CPU_MAP__MASK:
data->mask32_data.long_size = bswap_16(data->mask32_data.long_size);
switch (data->mask32_data.long_size) {
case 4: {
u16 nr, max_nr;
data->mask32_data.nr = bswap_16(data->mask32_data.nr);
nr = data->mask32_data.nr;
max_nr = (cpus_payload - offsetof(struct perf_record_cpu_map_data,
mask32_data.mask)) /
sizeof(data->mask32_data.mask[0]);
if (nr > max_nr) {
nr = max_nr;
data->mask32_data.nr = nr;
}
for (unsigned int i = 0; i < nr; i++)
data->mask32_data.mask[i] = bswap_32(data->mask32_data.mask[i]);
break;
}
case 8: {
u16 nr, max_nr;
data->mask64_data.nr = bswap_16(data->mask64_data.nr);
nr = data->mask64_data.nr;
if (cpus_payload < offsetof(struct perf_record_cpu_map_data, mask64_data.mask)) {
data->mask64_data.nr = 0;
break;
}
max_nr = (cpus_payload - offsetof(struct perf_record_cpu_map_data,
mask64_data.mask)) /
sizeof(data->mask64_data.mask[0]);
if (nr > max_nr) {
nr = max_nr;
data->mask64_data.nr = nr;
}
for (unsigned int i = 0; i < nr; i++)
data->mask64_data.mask[i] = bswap_64(data->mask64_data.mask[i]);
break;
}
default:
break;
}
break;
case PERF_CPU_MAP__RANGE_CPUS:
data->range_cpu_data.start_cpu = bswap_16(data->range_cpu_data.start_cpu);
data->range_cpu_data.end_cpu = bswap_16(data->range_cpu_data.end_cpu);
break;
default:
break;
}
}
return 0;
}