]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
perf session: Validate nr fields against event size on both swap and common paths
authorArnaldo Carvalho de Melo <acme@redhat.com>
Sat, 2 May 2026 16:14:06 +0000 (13:14 -0300)
committerArnaldo Carvalho de Melo <acme@redhat.com>
Fri, 29 May 2026 14:44:32 +0000 (11:44 -0300)
Several event types use an nr field to control iteration over
variable-length arrays.  The swap handlers byte-swap and loop using
these fields without bounds checks, and the native processing path
trusts them as well.

Add bounds checks on both paths for:

- PERF_RECORD_THREAD_MAP: validate nr against payload, return -1
  on the swap path.  On the native path, reject with -EINVAL.

- PERF_RECORD_NAMESPACES: clamp nr on the swap path (safe because
  each entry is indexed by type; missing entries just won't be
  resolved).  Skip the event on the native path.

- PERF_RECORD_CPU_MAP: clamp nr for CPUS and MASK sub-types on
  the swap path.  Add bounds checks for mask64 which previously
  had no nr validation.  Skip the event on the native path.

- PERF_RECORD_STAT_CONFIG: clamp nr on the swap path (safe because
  each config entry is self-describing via its tag).  Skip the
  event on the native path.

The swap path (cross-endian, writable MAP_PRIVATE mapping) can
safely clamp by writing back to the event.  The native path
(read-only MAP_SHARED mapping) must skip instead of clamping
because writing to the mmap'd event would segfault.

Also fix stat_config swap range: change size += 1 to
size += sizeof(event->stat_config.nr) for clarity.  The old +1
happened to work because mem_bswap_64 processes 8-byte chunks,
but the intent is to include the 8-byte nr field in the swap
range.

Changes in v2:
- Document that PERF_RECORD_NAMESPACES max_nr includes trailing
  sample_id space when sample_id_all is present — harmless on the
  swap path because both per-element bswap_64 and swap_sample_id_all()
  perform the same u64 byte swap (Reported-by: sashiko-bot@kernel.org)

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/session.c

index aef10d42be35487a37f28474b286c7224f362287..8588e12f110fca70c7008aa5ec223affcc661822 100644 (file)
@@ -496,13 +496,35 @@ static int perf_event__throttle_swap(union perf_event *event,
 static int perf_event__namespaces_swap(union perf_event *event,
                                       bool sample_id_all)
 {
-       u64 i;
+       u64 i, nr, max_nr;
 
        event->namespaces.pid           = bswap_32(event->namespaces.pid);
        event->namespaces.tid           = bswap_32(event->namespaces.tid);
        event->namespaces.nr_namespaces = bswap_64(event->namespaces.nr_namespaces);
 
-       for (i = 0; i < event->namespaces.nr_namespaces; i++) {
+       nr = event->namespaces.nr_namespaces;
+       /*
+        * Cannot underflow: perf_event__min_size[] guarantees header.size >= sizeof.
+        * When sample_id_all is present max_nr slightly overestimates the
+        * array space because header.size includes the trailing sample_id.
+        * Harmless: both the per-element bswap_64 loop and swap_sample_id_all()
+        * perform the same u64 byte swap, so the result is correct regardless
+        * of where the boundary between array and sample_id falls.
+        */
+       max_nr = (event->header.size - sizeof(event->namespaces)) /
+                sizeof(event->namespaces.link_info[0]);
+       /*
+        * Safe to clamp: each namespace entry is indexed by type;
+        * missing entries just won't be resolved.
+        */
+       if (nr > max_nr) {
+               pr_warning("WARNING: PERF_RECORD_NAMESPACES: nr_namespaces %" PRIu64 " exceeds payload (max %" PRIu64 "), clamping\n",
+                          nr, max_nr);
+               nr = max_nr;
+               event->namespaces.nr_namespaces = nr;
+       }
+
+       for (i = 0; i < nr; i++) {
                struct perf_ns_link_info *ns = &event->namespaces.link_info[i];
 
                ns->dev = bswap_64(ns->dev);
@@ -734,11 +756,23 @@ static int perf_event__auxtrace_error_swap(union perf_event *event,
 static int perf_event__thread_map_swap(union perf_event *event,
                                       bool sample_id_all __maybe_unused)
 {
-       unsigned i;
+       unsigned int i;
+       u64 nr;
 
        event->thread_map.nr = bswap_64(event->thread_map.nr);
 
-       for (i = 0; i < event->thread_map.nr; i++)
+       /*
+        * Reject rather than clamp: unlike namespaces (indexed by type)
+        * or stat_config (self-describing tags), a truncated thread map
+        * is structurally broken — downstream would get a wrong map.
+        */
+       /* Cannot underflow: perf_event__min_size[] guarantees header.size >= sizeof */
+       nr = event->thread_map.nr;
+       if (nr > (event->header.size - sizeof(event->thread_map)) /
+                 sizeof(event->thread_map.entries[0]))
+               return -1;
+
+       for (i = 0; i < nr; i++)
                event->thread_map.entries[i].pid = bswap_64(event->thread_map.entries[i].pid);
        return 0;
 }
@@ -747,32 +781,80 @@ static int perf_event__cpu_map_swap(union perf_event *event,
                                    bool sample_id_all __maybe_unused)
 {
        struct perf_record_cpu_map_data *data = &event->cpu_map.data;
+       u32 payload = event->header.size - sizeof(event->header);
 
        data->type = bswap_16(data->type);
 
+       /*
+        * Safe to clamp: a shorter CPU map just means some CPUs
+        * are absent; tools process the CPUs that are present.
+        */
        switch (data->type) {
-       case PERF_CPU_MAP__CPUS:
-               data->cpus_data.nr = bswap_16(data->cpus_data.nr);
+       case PERF_CPU_MAP__CPUS: {
+               u16 nr, max_nr;
 
-               for (unsigned i = 0; i < data->cpus_data.nr; i++)
+               data->cpus_data.nr = bswap_16(data->cpus_data.nr);
+               nr = data->cpus_data.nr;
+               max_nr = (payload - offsetof(struct perf_record_cpu_map_data,
+                                            cpus_data.cpu)) /
+                        sizeof(data->cpus_data.cpu[0]);
+               if (nr > max_nr) {
+                       pr_warning("WARNING: PERF_RECORD_CPU_MAP: nr %u exceeds payload (max %u), clamping\n",
+                                  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:
+               case 4: {
+                       u16 nr, max_nr;
+
                        data->mask32_data.nr = bswap_16(data->mask32_data.nr);
-                       for (unsigned i = 0; i < data->mask32_data.nr; i++)
+                       nr = data->mask32_data.nr;
+                       max_nr = (payload - offsetof(struct perf_record_cpu_map_data,
+                                                    mask32_data.mask)) /
+                                sizeof(data->mask32_data.mask[0]);
+                       if (nr > max_nr) {
+                               pr_warning("WARNING: PERF_RECORD_CPU_MAP mask32: nr %u exceeds payload (max %u), clamping\n",
+                                          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:
+               }
+               case 8: {
+                       u16 nr, max_nr;
+
                        data->mask64_data.nr = bswap_16(data->mask64_data.nr);
-                       for (unsigned i = 0; i < data->mask64_data.nr; i++)
+                       nr = data->mask64_data.nr;
+                       if (payload < offsetof(struct perf_record_cpu_map_data, mask64_data.mask)) {
+                               data->mask64_data.nr = 0;
+                               break;
+                       }
+                       max_nr = (payload - offsetof(struct perf_record_cpu_map_data,
+                                                    mask64_data.mask)) /
+                                sizeof(data->mask64_data.mask[0]);
+                       if (nr > max_nr) {
+                               pr_warning("WARNING: PERF_RECORD_CPU_MAP mask64: nr %u exceeds payload (max %u), clamping\n",
+                                          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:
-                       pr_err("cpu_map swap: unsupported long size\n");
+                       pr_err("cpu_map swap: unsupported long size %u\n",
+                              data->mask32_data.long_size);
                }
                break;
        case PERF_CPU_MAP__RANGE_CPUS:
@@ -788,11 +870,27 @@ static int perf_event__cpu_map_swap(union perf_event *event,
 static int perf_event__stat_config_swap(union perf_event *event,
                                        bool sample_id_all __maybe_unused)
 {
-       u64 size;
+       u64 nr, max_nr, size;
 
-       size  = bswap_64(event->stat_config.nr) * sizeof(event->stat_config.data[0]);
-       size += 1; /* nr item itself */
+       nr = bswap_64(event->stat_config.nr);
+       /* Cannot underflow: perf_event__min_size[] guarantees header.size >= sizeof */
+       max_nr = (event->header.size - sizeof(event->stat_config)) /
+                sizeof(event->stat_config.data[0]);
+       /*
+        * Safe to clamp: each config entry is self-describing
+        * via its tag; missing entries keep their defaults.
+        */
+       if (nr > max_nr) {
+               pr_warning("WARNING: PERF_RECORD_STAT_CONFIG: nr %" PRIu64 " exceeds payload (max %" PRIu64 "), clamping\n",
+                          nr, max_nr);
+               nr = max_nr;
+       }
+       size = nr * sizeof(event->stat_config.data[0]);
+       /* The swap starts at &nr, so add its size to cover the full range */
+       size += sizeof(event->stat_config.nr);
        mem_bswap_64(&event->stat_config.nr, size);
+       /* Persist the clamped value in native byte order */
+       event->stat_config.nr = nr;
        return 0;
 }
 
@@ -1730,8 +1828,27 @@ static int machines__deliver_event(struct machines *machines,
                                           "COMM"))
                        return 0;
                return tool->comm(tool, event, sample, machine);
-       case PERF_RECORD_NAMESPACES:
+       case PERF_RECORD_NAMESPACES: {
+               /*
+                * Cannot underflow: perf_event__min_size[] guarantees header.size >= sizeof.
+                * Includes trailing sample_id space when present, but prevents OOB.
+                */
+               u64 max_nr = (event->header.size - sizeof(event->namespaces)) /
+                            sizeof(event->namespaces.link_info[0]);
+
+               /*
+                * Native-endian events are mmap'd read-only, so we
+                * cannot clamp nr in place.  Skip the event instead.
+                * The swap handler already clamps on the writable
+                * cross-endian path.
+                */
+               if (event->namespaces.nr_namespaces > max_nr) {
+                       pr_warning("WARNING: PERF_RECORD_NAMESPACES: nr_namespaces %" PRIu64 " exceeds payload (max %" PRIu64 "), skipping\n",
+                                  (u64)event->namespaces.nr_namespaces, max_nr);
+                       return 0;
+               }
                return tool->namespaces(tool, event, sample, machine);
+       }
        case PERF_RECORD_CGROUP:
                if (!perf_event__check_nul(event->cgroup.path,
                                           (void *)event + event->header.size,
@@ -1912,15 +2029,112 @@ static s64 perf_session__process_user_event(struct perf_session *session,
                perf_session__auxtrace_error_inc(session, event);
                err = tool->auxtrace_error(tool, session, event);
                break;
-       case PERF_RECORD_THREAD_MAP:
+       case PERF_RECORD_THREAD_MAP: {
+               u64 max_nr;
+
+               if (event->header.size < sizeof(event->thread_map)) {
+                       pr_err("PERF_RECORD_THREAD_MAP: header.size (%u) too small\n",
+                              event->header.size);
+                       err = -EINVAL;
+                       break;
+               }
+
+               max_nr = (event->header.size - sizeof(event->thread_map)) /
+                        sizeof(event->thread_map.entries[0]);
+               if (event->thread_map.nr > max_nr) {
+                       pr_err("PERF_RECORD_THREAD_MAP: nr %" PRIu64 " exceeds max %" PRIu64 "\n",
+                              (u64)event->thread_map.nr, max_nr);
+                       err = -EINVAL;
+                       break;
+               }
+
                err = tool->thread_map(tool, session, event);
                break;
-       case PERF_RECORD_CPU_MAP:
+       }
+       case PERF_RECORD_CPU_MAP: {
+               struct perf_record_cpu_map_data *data = &event->cpu_map.data;
+               u32 payload = event->header.size - sizeof(event->header);
+
+               /*
+                * Native-endian events are mmap'd read-only, so we
+                * cannot clamp nr fields in place.  Skip the event
+                * if any variant overflows.
+                */
+               switch (data->type) {
+               case PERF_CPU_MAP__CPUS: {
+                       u16 max_nr = (payload - offsetof(struct perf_record_cpu_map_data,
+                                                        cpus_data.cpu)) /
+                                    sizeof(data->cpus_data.cpu[0]);
+
+                       if (data->cpus_data.nr > max_nr) {
+                               pr_warning("WARNING: PERF_RECORD_CPU_MAP: nr %u exceeds payload (max %u), skipping\n",
+                                          data->cpus_data.nr, max_nr);
+                               err = 0;
+                               goto out;
+                       }
+                       break;
+               }
+               case PERF_CPU_MAP__MASK:
+                       if (data->mask32_data.long_size == 4) {
+                               u16 max_nr = (payload - offsetof(struct perf_record_cpu_map_data,
+                                                                mask32_data.mask)) /
+                                            sizeof(data->mask32_data.mask[0]);
+
+                               if (data->mask32_data.nr > max_nr) {
+                                       pr_warning("WARNING: PERF_RECORD_CPU_MAP mask32: nr %u exceeds payload (max %u), skipping\n",
+                                                  data->mask32_data.nr, max_nr);
+                                       err = 0;
+                                       goto out;
+                               }
+                       } else if (data->mask64_data.long_size == 8) {
+                               u16 max_nr;
+
+                               if (payload < offsetof(struct perf_record_cpu_map_data, mask64_data.mask)) {
+                                       err = 0;
+                                       goto out;
+                               }
+                               max_nr = (payload - offsetof(struct perf_record_cpu_map_data,
+                                                            mask64_data.mask)) /
+                                        sizeof(data->mask64_data.mask[0]);
+                               if (data->mask64_data.nr > max_nr) {
+                                       pr_warning("WARNING: PERF_RECORD_CPU_MAP mask64: nr %u exceeds payload (max %u), skipping\n",
+                                                  data->mask64_data.nr, max_nr);
+                                       err = 0;
+                                       goto out;
+                               }
+                       } else {
+                               pr_warning("WARNING: PERF_RECORD_CPU_MAP: unsupported long_size %u, skipping\n",
+                                          data->mask32_data.long_size);
+                               err = 0;
+                               goto out;
+                       }
+                       break;
+               default:
+                       break;
+               }
+
                err = tool->cpu_map(tool, session, event);
                break;
-       case PERF_RECORD_STAT_CONFIG:
+       }
+       case PERF_RECORD_STAT_CONFIG: {
+               /* Cannot underflow: perf_event__min_size[] guarantees header.size >= sizeof */
+               u64 max_nr = (event->header.size - sizeof(event->stat_config)) /
+                            sizeof(event->stat_config.data[0]);
+
+               /*
+                * Native-endian events are mmap'd read-only, so we
+                * cannot clamp nr in place.  Skip the event instead.
+                */
+               if (event->stat_config.nr > max_nr) {
+                       pr_warning("WARNING: PERF_RECORD_STAT_CONFIG: nr %" PRIu64 " exceeds payload (max %" PRIu64 "), skipping\n",
+                                  (u64)event->stat_config.nr, max_nr);
+                       err = 0;
+                       goto out;
+               }
+
                err = tool->stat_config(tool, session, event);
                break;
+       }
        case PERF_RECORD_STAT:
                err = tool->stat(tool, session, event);
                break;
@@ -1963,6 +2177,7 @@ static s64 perf_session__process_user_event(struct perf_session *session,
                err = -EINVAL;
                break;
        }
+out:
        perf_sample__exit(&sample);
        return err;
 }