]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
perf header: Validate null-termination in PERF_RECORD_EVENT_UPDATE string fields
authorArnaldo Carvalho de Melo <acme@redhat.com>
Sat, 2 May 2026 16:31:40 +0000 (13:31 -0300)
committerArnaldo Carvalho de Melo <acme@redhat.com>
Fri, 29 May 2026 14:44:33 +0000 (11:44 -0300)
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>
tools/perf/util/header.c
tools/perf/util/session.c

index c0b5c99f462ad925322bd39b780d06fe736d99a2..9e3a08b1f8ae5a73d633fe95162a7cb2939e1175 100644 (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);
-               break;
-       case PERF_EVENT_UPDATE__NAME:
-               ret += fprintf(fp, "... name:  %s\n", ev->name);
+       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__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;
        }
index 95eb793026de6d8dfc5b4b987e3f0d9986c3960d..8280413f4528f53c4129f4565c308109f031fb79 100644 (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;
 }