[PATCH 23/28] perf tools: Harden compressed event processing
From: Arnaldo Carvalho de Melo
Date: Sat May 09 2026 - 23:37:56 EST
From: Arnaldo Carvalho de Melo <acme@xxxxxxxxxx>
Add several hardening checks to the compressed event decompression
pipeline:
1. Guard against decomp_last_rem underflow: check that
decomp_last->head does not exceed decomp_last->size before
subtracting. A u64 underflow here would produce a huge
decomp_len, causing an oversized mmap allocation.
2. Validate comp_mmap_len from the HEADER_COMPRESSED feature
section: reject values that are not 4K-aligned, smaller than
4096, or larger than ~2 GB (prevents size_t overflow when
adding decomp_last_rem on 32-bit, while allowing legitimate
large mmap buffers from perf record -m).
3. Validate COMPRESSED event header size: reject events where
header.size is too small to contain the fixed struct fields,
preventing underflow in the payload size calculation.
4. Validate COMPRESSED2 event data_size: check that data_size
does not exceed the available payload (header.size minus the
fixed struct fields) for the newer compressed format.
5. Reject compressed events when the HEADER_COMPRESSED feature
is missing from the file header, which means no decompression
context was initialized.
Reported-by: sashiko-bot@xxxxxxxxxx # Running on a local machine
Assisted-by: Claude Opus 4.6 (1M context) <noreply@xxxxxxxxxxxxx>
Signed-off-by: Arnaldo Carvalho de Melo <acme@xxxxxxxxxx>
---
tools/perf/util/header.c | 17 +++++++++++++++++
tools/perf/util/tool.c | 38 +++++++++++++++++++++++++++++++++++++-
2 files changed, 54 insertions(+), 1 deletion(-)
diff --git a/tools/perf/util/header.c b/tools/perf/util/header.c
index bda8705e87648800..994e54167ea3196b 100644
--- a/tools/perf/util/header.c
+++ b/tools/perf/util/header.c
@@ -3849,6 +3849,23 @@ static int process_compressed(struct feat_fd *ff,
if (do_read_u32(ff, &(env->comp_mmap_len)))
return -1;
+ /*
+ * FIXME: perf.data should record the recording system's page
+ * size — it affects mmap buffer alignment, sample addresses,
+ * and data_page_size/code_page_size interpretation. Without
+ * it we assume 4K (the smallest Linux page size) as a safe
+ * minimum alignment for comp_mmap_len validation.
+ *
+ * Cap at 2 GB to keep decomp_len + decomp_last_rem +
+ * sizeof(struct decomp) within size_t range on 32-bit.
+ */
+ if (env->comp_mmap_len < 4096 || env->comp_mmap_len % 4096 ||
+ env->comp_mmap_len > (2U * 1024 * 1024 * 1024 - 4096)) {
+ pr_err("Invalid HEADER_COMPRESSED: comp_mmap_len (%u) must be a 4K-aligned value in [4096, %u]\n",
+ env->comp_mmap_len, 2U * 1024 * 1024 * 1024 - 4096);
+ return -1;
+ }
+
return 0;
}
diff --git a/tools/perf/util/tool.c b/tools/perf/util/tool.c
index ff2150517b75587a..96fa6d6c55cdca1e 100644
--- a/tools/perf/util/tool.c
+++ b/tools/perf/util/tool.c
@@ -24,7 +24,15 @@ static int perf_session__process_compressed_event(const struct perf_tool *tool _
size_t mmap_len, decomp_len = perf_session__env(session)->comp_mmap_len;
struct decomp *decomp, *decomp_last = session->active_decomp->decomp_last;
+ if (!decomp_len) {
+ pr_err("Compressed events found but HEADER_COMPRESSED not set\n");
+ return -1;
+ }
+
if (decomp_last) {
+ /* Prevent u64 underflow in decomp_last_rem */
+ if (decomp_last->head > decomp_last->size)
+ return -1;
decomp_last_rem = decomp_last->size - decomp_last->head;
decomp_len += decomp_last_rem;
}
@@ -47,14 +55,37 @@ static int perf_session__process_compressed_event(const struct perf_tool *tool _
decomp->size = decomp_last_rem;
}
+ /*
+ * Events are read directly from the mmap'd file; fields could
+ * theoretically change via a FUSE-backed file, but that applies
+ * to the entire event processing pipeline, not just here.
+ */
if (event->header.type == PERF_RECORD_COMPRESSED) {
+ if (event->header.size < sizeof(struct perf_record_compressed))
+ goto err_decomp;
src = (void *)event + sizeof(struct perf_record_compressed);
src_size = event->pack.header.size - sizeof(struct perf_record_compressed);
} else if (event->header.type == PERF_RECORD_COMPRESSED2) {
+ /*
+ * prefetch_event() only guarantees that the 8-byte
+ * event header fits; validate that header.size covers
+ * the data_size field before accessing it, otherwise a
+ * crafted event reads data_size from adjacent memory.
+ */
+ if (event->header.size < sizeof(struct perf_record_compressed2))
+ goto err_decomp;
src = (void *)event + sizeof(struct perf_record_compressed2);
src_size = event->pack2.data_size;
+ /*
+ * data_size is independent of header.size (which
+ * includes padding); verify it doesn't exceed the
+ * actual payload to prevent out-of-bounds reads in
+ * zstd_decompress_stream().
+ */
+ if (src_size > event->header.size - sizeof(struct perf_record_compressed2))
+ goto err_decomp;
} else {
- return -1;
+ goto err_decomp;
}
decomp_size = zstd_decompress_stream(session->active_decomp->zstd_decomp, src, src_size,
@@ -77,6 +108,11 @@ static int perf_session__process_compressed_event(const struct perf_tool *tool _
pr_debug("decomp (B): %zd to %zd\n", src_size, decomp_size);
return 0;
+
+err_decomp:
+ munmap(decomp, mmap_len);
+ pr_err("Couldn't decompress data\n");
+ return -1;
}
#endif
--
2.54.0