* [PATCH v1 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
@ 2026-04-24 22:05 Ian Rogers
2026-04-24 22:05 ` [PATCH v1 2/2] perf test: Add inject ASLR test Ian Rogers
2026-04-25 2:05 ` [PATCH v2 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
0 siblings, 2 replies; 15+ messages in thread
From: Ian Rogers @ 2026-04-24 22:05 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Jiri Olsa, Ian Rogers, Adrian Hunter, James Clark,
Gabriel Marin, linux-kernel, linux-perf-users
If perf.data files are taken from one machine to another they make
leak virtual addresses and so weaken ASLR on the machine they are
coming from. Add a '--aslr' option for perf inject that remaps all
virtual addresses, or drops data/events, so that the virtual address
information isn't leaked.
When events are not known/handled by the tool they are dropped. This
makes the tool conservative and it should never leak ASLR information,
but it means virtual address remapping is needed for cases like
auxtrace.
Signed-off-by: Ian Rogers <irogers@google.com>
Co-developed-by: Gabriel Marin <gmx@google.com>
Signed-off-by: Gabriel Marin <gmx@google.com>
---
tools/perf/builtin-inject.c | 11 +-
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 752 ++++++++++++++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
4 files changed, 773 insertions(+), 1 deletion(-)
create mode 100644 tools/perf/util/aslr.c
create mode 100644 tools/perf/util/aslr.h
diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c
index f174bc69cec4..fa3a71e23f1b 100644
--- a/tools/perf/builtin-inject.c
+++ b/tools/perf/builtin-inject.c
@@ -8,6 +8,7 @@
*/
#include "builtin.h"
+#include "util/aslr.h"
#include "util/color.h"
#include "util/dso.h"
#include "util/vdso.h"
@@ -123,6 +124,7 @@ struct perf_inject {
bool in_place_update_dry_run;
bool copy_kcore_dir;
bool convert_callchain;
+ bool aslr;
const char *input_name;
struct perf_data output;
u64 bytes_written;
@@ -2564,6 +2566,8 @@ int cmd_inject(int argc, const char **argv)
" instance has a subdir"),
OPT_BOOLEAN(0, "convert-callchain", &inject.convert_callchain,
"Generate callchains using DWARF and drop register/stack data"),
+ OPT_BOOLEAN(0, "aslr", &inject.aslr,
+ "Remap virtual memory addresses similar to ASLR"),
OPT_END()
};
const char * const inject_usage[] = {
@@ -2571,6 +2575,7 @@ int cmd_inject(int argc, const char **argv)
NULL
};
bool ordered_events;
+ struct perf_tool *tool = &inject.tool;
if (!inject.itrace_synth_opts.set) {
/* Disable eager loading of kernel symbols that adds overhead to perf inject. */
@@ -2684,7 +2689,9 @@ int cmd_inject(int argc, const char **argv)
inject.tool.schedstat_domain = perf_event__repipe_op2_synth;
inject.tool.dont_split_sample_group = true;
inject.tool.merge_deferred_callchains = false;
- inject.session = __perf_session__new(&data, &inject.tool,
+ if (inject.aslr)
+ tool = aslr_tool__new(&inject.tool);
+ inject.session = __perf_session__new(&data, tool,
/*trace_event_repipe=*/inject.output.is_pipe,
/*host_env=*/NULL);
@@ -2789,6 +2796,8 @@ int cmd_inject(int argc, const char **argv)
strlist__delete(inject.known_build_ids);
zstd_fini(&(inject.session->zstd_data));
perf_session__delete(inject.session);
+ if (inject.aslr)
+ aslr_tool__delete(tool);
out_close_output:
if (!inject.in_place_update)
perf_data__close(&inject.output);
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 70cc91d00804..65b96f3b87e2 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -6,6 +6,7 @@ perf-util-y += arm64-frame-pointer-unwind-support.o
perf-util-y += addr2line.o
perf-util-y += addr_location.o
perf-util-y += annotate.o
+perf-util-y += aslr.o
perf-util-y += blake2s.o
perf-util-y += block-info.o
perf-util-y += block-range.o
diff --git a/tools/perf/util/aslr.c b/tools/perf/util/aslr.c
new file mode 100644
index 000000000000..23ef7b68896c
--- /dev/null
+++ b/tools/perf/util/aslr.c
@@ -0,0 +1,752 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "aslr.h"
+
+#include "addr_location.h"
+#include "debug.h"
+#include "event.h"
+#include "evsel.h"
+#include "machine.h"
+#include "map.h"
+#include "thread.h"
+#include "tool.h"
+
+#include <internal/lib.h> // page_size
+#include <linux/compiler.h>
+#include <errno.h>
+#include <inttypes.h>
+
+struct remap_addresses_key {
+ u64 start_addr;
+ pid_t pid;
+};
+
+struct aslr_tool {
+ /** @tool: The tool implemented here and a pointer to a delegate to process the data. */
+ struct delegate_tool tool;
+ /** @machine: The machine with the input, not remapped, virtual address layout. */
+ struct machine machine;
+ /** @event_copy: Buffer used to create an event to pass to the delegate. */
+ char event_copy[PERF_SAMPLE_MAX_SIZE];
+ /** @remap_addresses: mapping from remap_addresses_key to remapped address. */
+ struct hashmap remap_addresses;
+ /** @top_addresses: mapping from process to max remapped address. */
+ struct hashmap top_addresses;
+ /** @first_kernel_mapping: flag indicating if we are still to process any kernel mapping. */
+ bool first_kernel_mapping;
+};
+
+static const pid_t kernel_pid = -1;
+
+/* Start remapping user processes from a small non-zero offset. */
+static const u64 user_space_start = 0x200000;
+static const u64 kernel_space_start = 0xffff800010000000;
+
+static size_t remap_addresses__hash(long _key, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key = (struct remap_addresses_key *)_key;
+
+ return key->start_addr ^ (key->start_addr >> 12) ^ key->pid;
+}
+
+static bool remap_addresses__equal(long _key1, long _key2, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key1 = (struct remap_addresses_key *)_key1;
+ struct remap_addresses_key *key2 = (struct remap_addresses_key *)_key2;
+
+ return key1->pid == key2->pid && key1->start_addr == key2->start_addr;
+}
+
+static size_t top_addresses__hash(long key, void *ctx __maybe_unused)
+{
+ return key;
+}
+
+static bool top_addresses__equal(long key1, long key2, void *ctx __maybe_unused)
+{
+ return key1 == key2;
+}
+
+static u64 round_up_to_page_size(u64 addr)
+{
+ return (addr + page_size - 1) & ~((u64)page_size - 1);
+}
+
+static u64 aslr_tool__remap_address(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 addr)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u8 effective_cpumode = cpumode;
+
+ if (!aslr_thread)
+ return 0; // No thread.
+
+ addr_location__init(&al);
+ if (!thread__find_map(aslr_thread, cpumode, addr, &al)) {
+ /*
+ * If lookup fails with specified cpumode, try fallback to the other space
+ * to be robust against bad cpumode in samples.
+ */
+ effective_cpumode = (cpumode == PERF_RECORD_MISC_KERNEL) ?
+ PERF_RECORD_MISC_USER : PERF_RECORD_MISC_KERNEL;
+ if (!thread__find_map(aslr_thread, effective_cpumode, addr, &al)) {
+ pr_debug("Cannot find mmap for address %lx in either space, pid=%d\n",
+ addr, aslr_thread->pid_);
+ addr_location__exit(&al);
+ return 0; // No mmap.
+ }
+ }
+
+ key.pid = effective_cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+ key.start_addr = map__start(al.map);
+ if (!hashmap__find(&aslr->remap_addresses, &key, &remap_addr)) {
+ pr_debug("Cannot find a remapped entry for address %lx in mapping %lx(%lx) for pid=%d\n",
+ addr, map__start(al.map), map__size(al.map), key.pid);
+ addr_location__exit(&al);
+ return 0;
+ }
+ remap_addr += addr - map__start(al.map);
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_mapping(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 start, u64 len)
+{
+ struct addr_location prev_al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ /* If mapping is contiguous to the previous process mapping. */
+ bool is_contiguous = false;
+ bool first_mapping = false; // first process mapping.
+
+ if (!aslr_thread)
+ return 0; // No thread.
+
+ addr_location__init(&prev_al);
+ if (thread__find_map(aslr_thread, cpumode, start-1, &prev_al)) {
+ if (map__start(prev_al.map) + map__size(prev_al.map) == start) {
+ is_contiguous = true;
+ } else {
+ pr_debug("Previous mmap [%lx, %lx] overlaps current map [%lx, %lx]\n",
+ map__start(prev_al.map),
+ map__start(prev_al.map) + map__size(prev_al.map),
+ start, start+len);
+ }
+ }
+ addr_location__exit(&prev_al);
+
+ key.pid = cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+ key.start_addr = start;
+ if (hashmap__find(&aslr->remap_addresses, &key, &remap_addr))
+ return remap_addr;
+
+ if (!hashmap__find(&aslr->top_addresses, key.pid, &remap_addr)) {
+ /* First mapping in this process. Don't add a page gap. */
+ first_mapping = true;
+ remap_addr = (cpumode == PERF_RECORD_MISC_KERNEL ?
+ kernel_space_start : user_space_start);
+ }
+
+ remap_addr = round_up_to_page_size(remap_addr);
+ if (!is_contiguous && !first_mapping)
+ remap_addr += page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+
+ if (!new_key)
+ return 0;
+ *new_key = key;
+ if (hashmap__add(&aslr->remap_addresses, new_key, remap_addr) != 0) {
+ pr_debug("Failed to add remap_addresses entry for pid=%d, mapping start=%lx, remapped start=%lx",
+ key.pid, start, remap_addr);
+ free(new_key);
+ return 0;
+ }
+ }
+
+ hashmap__insert(&aslr->top_addresses, key.pid, remap_addr+len,
+ first_mapping ? HASHMAP_ADD : HASHMAP_UPDATE, NULL, NULL);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_ksymbol(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u64 addr, u32 len)
+{
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ bool first_mapping = false;
+
+ if (!aslr_thread)
+ return 0; // No thread.
+
+ key.pid = aslr_thread->pid_;
+ key.start_addr = addr;
+ if (hashmap__find(&aslr->remap_addresses, &key, &remap_addr))
+ return remap_addr;
+
+ first_mapping = !hashmap__find(&aslr->top_addresses, key.pid, &remap_addr);
+ if (first_mapping)
+ remap_addr = kernel_space_start;
+ remap_addr = round_up_to_page_size(remap_addr) + page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+
+ if (!new_key)
+ return 0;
+ *new_key = key;
+ if (hashmap__add(&aslr->remap_addresses, new_key, remap_addr) < 0) {
+ pr_debug("Failed to add remap_addresses entry for pid=%d, ksymbol=%lx, remapped address=%lx",
+ key.pid, addr, remap_addr);
+ free(new_key);
+ return 0;
+ }
+ }
+
+ hashmap__insert(&aslr->top_addresses, key.pid, remap_addr+len,
+ first_mapping ? HASHMAP_ADD : HASHMAP_UPDATE, NULL, NULL);
+ return remap_addr;
+}
+
+
+static int aslr_tool__process_mmap(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ u8 cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+ struct thread *thread;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(&aslr->machine, event->mmap.pid, event->mmap.tid);
+ memcpy(&new_event->mmap, &event->mmap, event->mmap.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap.start, event->mmap.len);
+ if (aslr->first_kernel_mapping && cpumode == PERF_RECORD_MISC_KERNEL) {
+ /* If this is the first kernel image, we need to adjust the pgoff by a
+ * similar delta.
+ */
+ new_event->mmap.pgoff = event->mmap.pgoff - event->mmap.start +
+ new_event->mmap.start;
+ aslr->first_kernel_mapping = false;
+ }
+ return delegate->mmap(delegate, new_event, sample, machine);
+}
+
+static int aslr_tool__process_mmap2(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ u8 cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+ struct thread *thread;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap2(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(&aslr->machine, event->mmap2.pid, event->mmap2.tid);
+ memcpy(&new_event->mmap2, &event->mmap2, event->mmap2.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap2.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap2.start, event->mmap2.len);
+ if (aslr->first_kernel_mapping && cpumode == PERF_RECORD_MISC_KERNEL) {
+ /* If this is the first kernel image, we need to adjust the pgoff by a
+ * similar delta.
+ */
+ new_event->mmap2.pgoff = event->mmap2.pgoff - event->mmap2.start +
+ new_event->mmap2.start;
+ aslr->first_kernel_mapping = false;
+ }
+ return delegate->mmap2(delegate, new_event, sample, machine);
+}
+
+static int aslr_tool__process_comm(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_comm(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ return delegate->comm(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_fork(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_fork(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ return delegate->fork(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_exit(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_exit(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ return delegate->exit(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_text_poke(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ u8 cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+ struct thread *thread;
+
+ thread = machine__findnew_thread(&aslr->machine, sample->pid, sample->tid);
+ memcpy(&new_event->text_poke, &event->text_poke, event->text_poke.header.size);
+ new_event->text_poke.addr = aslr_tool__remap_address(aslr, thread, cpumode,
+ event->text_poke.addr);
+
+ return delegate->text_poke(delegate, new_event, sample, machine);
+}
+
+static int aslr_tool__process_ksymbol(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ struct thread *thread;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_ksymbol(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(&aslr->machine, kernel_pid, 0);
+ memcpy(&new_event->ksymbol, &event->ksymbol, event->ksymbol.header.size);
+ /* Remaps the ksymbol.start */
+ new_event->ksymbol.addr = aslr_tool__remap_ksymbol(aslr, thread,
+ event->ksymbol.addr, event->ksymbol.len);
+
+ return delegate->ksymbol(delegate, new_event, sample, machine);
+}
+
+static inline int copy_u64(__u64 *in_array, __u64 *out_array,
+ size_t *i, size_t *j, const __u64 max_i)
+{
+ if (*i > max_i)
+ return -EFAULT;
+ out_array[(*j)++] = in_array[(*i)++];
+ return 0;
+}
+
+static int aslr_tool__process_sample(const struct perf_tool *tool, union perf_event *event,
+ struct perf_sample *sample,
+ struct evsel *evsel, struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int ret;
+ u64 sample_type = evsel->core.attr.sample_type;
+ struct thread *thread = machine__findnew_thread(&aslr->machine, sample->pid, sample->tid);
+ const __u64 max_i = event->header.size / sizeof(__u64);
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ struct perf_sample new_sample;
+ __u64 *in_array, *out_array;
+ u8 cpumode = sample->cpumode;
+ u64 addr;
+ size_t i = 0, j = 0;
+
+ new_event->sample.header = event->sample.header;
+
+ in_array = &event->sample.array[0];
+ out_array = &new_event->sample.array[0];
+
+
+
+ if (sample_type & PERF_SAMPLE_IDENTIFIER)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // id
+ if (sample_type & PERF_SAMPLE_IP) {
+ i++;
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode,
+ sample->ip);
+ }
+ if (sample_type & PERF_SAMPLE_TID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // pid, tid
+ if (sample_type & PERF_SAMPLE_TIME)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // time
+ if (sample_type & PERF_SAMPLE_ADDR) {
+ i++; // addr
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode,
+ sample->addr);
+ }
+ if (sample_type & PERF_SAMPLE_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // id
+ if (sample_type & PERF_SAMPLE_STREAM_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // stream_id
+ if (sample_type & PERF_SAMPLE_CPU)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // cpu, res
+ if (sample_type & PERF_SAMPLE_PERIOD)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // period
+ if (sample_type & PERF_SAMPLE_READ) {
+ if ((evsel->core.attr.read_format & PERF_FORMAT_GROUP) == 0) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // value
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // time_enabled
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // time_running
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // id
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // lost
+ } else {
+ u64 nr;
+
+ if (i > max_i)
+ return -EFAULT;
+ nr = out_array[j++] = in_array[i++];
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // time_enabled
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // time_running
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // value
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // id
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // lost
+ }
+ }
+ }
+ if (sample_type & PERF_SAMPLE_CALLCHAIN) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // nr
+
+ for (u64 cntr = 0; cntr < sample->callchain->nr; cntr++) {
+ if (i > max_i)
+ return -EFAULT;
+ i++;
+ addr = sample->callchain->ips[cntr];
+ if (addr >= PERF_CONTEXT_MAX) {
+ // Copy context values as is.
+ out_array[j++] = addr;
+ switch (addr) {
+ case PERF_CONTEXT_HV:
+ cpumode = PERF_RECORD_MISC_HYPERVISOR;
+ break;
+ case PERF_CONTEXT_KERNEL:
+ cpumode = PERF_RECORD_MISC_KERNEL;
+ break;
+ case PERF_CONTEXT_USER:
+ cpumode = PERF_RECORD_MISC_USER;
+ break;
+ default:
+ pr_debug("invalid callchain context: %"PRIx64"\n", addr);
+ /*
+ * It seems the callchain is corrupted.
+ * Discard sample.
+ */
+ return 0;
+ }
+ continue;
+ }
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode,
+ sample->callchain->ips[cntr]);
+ }
+ }
+ if (sample_type & PERF_SAMPLE_RAW) {
+ size_t bytes = sizeof(u32) + sample->raw_size;
+
+ if ((i + (bytes / sizeof(u64))) > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], bytes);
+ i += bytes / sizeof(u64);
+ j += bytes / sizeof(u64);
+ // TODO: certain raw samples can be remapped, such as
+ // tracepoints by examining their fields.
+ pr_debug("Dropping raw samples as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_BRANCH_STACK) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // nr
+ if (sample_type & PERF_SAMPLE_BRANCH_HW_INDEX)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // hw_idx
+ if (i + (sample->branch_stack->nr * 3) > max_i)
+ return -EFAULT;
+ for (u64 cntr = 0; cntr < sample->branch_stack->nr; cntr++) {
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, sample->cpumode,
+ in_array[i++]); // from
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, sample->cpumode,
+ in_array[i++]); // to
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // flags
+ }
+ if (sample_type & PERF_SAMPLE_BRANCH_COUNTERS) {
+ if (i + sample->branch_stack->nr > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], sample->branch_stack->nr * sizeof(u64));
+ i += sample->branch_stack->nr;
+ j += sample->branch_stack->nr;
+ // TODO: confirm branch counters don't leak ASLR information.
+ pr_debug("Dropping sample branch counters as possible ASLR leak\n");
+ return 0;
+ }
+ }
+ if (sample_type & PERF_SAMPLE_REGS_USER) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // abi
+ if (sample->user_regs->abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_user);
+
+ if (i + nr > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ // TODO: can this be less conservative?
+ pr_debug("Dropping regs user sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_STACK_USER) {
+ u64 size;
+
+ if (i > max_i)
+ return -EFAULT;
+ size = out_array[j++] = in_array[i++];
+ if (size > 0) {
+ memcpy(&out_array[j], &in_array[i], size);
+ i += size / sizeof(u64);
+ j += size / sizeof(u64);
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // dyn_size
+ }
+ // TODO: can this be less conservative?
+ pr_debug("Dropping stack user sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_WEIGHT_TYPE)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // perf_sample_weight
+ if (sample_type & PERF_SAMPLE_DATA_SRC)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // data_src
+ if (sample_type & PERF_SAMPLE_TRANSACTION)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // transaction
+ if (sample_type & PERF_SAMPLE_REGS_INTR) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // abi
+ if (sample->intr_regs->abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_intr);
+
+ if (i + nr > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ // TODO: can this be less conservative?
+ pr_debug("Dropping interrupt register sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_PHYS_ADDR) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // phys_addr
+ // TODO: can this be less conservative?
+ pr_debug("Dropping physical address sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_CGROUP)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // cgroup
+ if (sample_type & PERF_SAMPLE_DATA_PAGE_SIZE)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // data_page_size
+ if (sample_type & PERF_SAMPLE_CODE_PAGE_SIZE)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; // code_page_size
+
+ if (sample_type & PERF_SAMPLE_AUX) {
+ u64 size;
+
+ if (i > max_i)
+ return -EFAULT;
+ size = out_array[j++] = in_array[i++];
+ if (i + (size / sizeof(u64)) > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], size);
+ i += size / sizeof(u64);
+ j += size / sizeof(u64);
+ // TODO: can this be less conservative?
+ pr_debug("Dropping aux sample as possible ASLR leak\n");
+ return 0;
+ }
+
+ if (evsel__is_offcpu_event(evsel)) {
+ // TODO: can this be less conservative?
+ pr_debug("Dropping off-CPU sample as possible ASLR leak\n");
+ return 0;
+ }
+
+ perf_sample__init(&new_sample, /*all=*/ true);
+ ret = evsel__parse_sample(evsel, new_event, &new_sample);
+ if (ret)
+ return ret;
+
+ ret = delegate->sample(delegate, new_event, &new_sample, evsel, machine);
+ perf_sample__exit(&new_sample);
+ return ret;
+}
+
+static int aslr_tool__process_attr(const struct perf_tool *tool,
+ union perf_event *event,
+ struct evlist **pevlist)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+
+ memcpy(&new_event->attr, &event->attr, event->attr.header.size);
+ new_event->attr.attr.bp_addr = 0; // Conservatively remove addresses.
+ new_event->attr.attr.kprobe_addr = 0; // Conservatively remove addresses.
+
+ return delegate->attr(delegate, new_event, pevlist);
+}
+
+static void aslr_tool__init(struct aslr_tool *aslr, struct perf_tool *delegate)
+{
+ delegate_tool__init(&aslr->tool, delegate);
+ aslr->tool.tool.ordered_events = true;
+
+ machine__init(&aslr->machine, "", HOST_KERNEL_ID);
+
+ hashmap__init(&aslr->remap_addresses,
+ remap_addresses__hash, remap_addresses__equal,
+ /*ctx=*/NULL);
+ hashmap__init(&aslr->top_addresses,
+ top_addresses__hash, top_addresses__equal,
+ /*ctx=*/NULL);
+ aslr->first_kernel_mapping = true;
+
+ aslr->tool.tool.sample = aslr_tool__process_sample;
+ // read - reads a counter, okay to delegate.
+ aslr->tool.tool.mmap = aslr_tool__process_mmap;
+ aslr->tool.tool.mmap2 = aslr_tool__process_mmap2;
+ aslr->tool.tool.comm = aslr_tool__process_comm;
+ aslr->tool.tool.fork = aslr_tool__process_fork;
+ aslr->tool.tool.exit = aslr_tool__process_exit;
+ // namesspaces, cgroup, lost, lost_sample, aux,
+ // itrace_start, aux_output_hw_id, context_switch, throttle, unthrottle
+ // - no virtual addresses.
+ aslr->tool.tool.ksymbol = aslr_tool__process_ksymbol;
+ // bpf - no virtual address.
+ aslr->tool.tool.text_poke = aslr_tool__process_text_poke;
+ aslr->tool.tool.attr = aslr_tool__process_attr;
+ // event_update, tracing_data, finished_round, build_id, id_index,
+ // auxtrace_info, auxtrace_error, time_conv, thread_map, cpu_map,
+ // stat_config, stat, feature, finished_init, bpf_metadata, compressed,
+ // auxtrace - no virtual addresses.
+}
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate)
+{
+ struct aslr_tool *aslr = malloc(sizeof(*aslr));
+
+ if (!aslr)
+ return NULL;
+
+ aslr_tool__init(aslr, delegate);
+ return &aslr->tool.tool;
+}
+
+void aslr_tool__delete(struct perf_tool *tool)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct hashmap_entry *cur;
+ size_t bkt;
+
+ hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt)
+ zfree(&cur->pkey);
+
+ hashmap__clear(&aslr->remap_addresses);
+ hashmap__clear(&aslr->top_addresses);
+ machine__exit(&aslr->machine);
+ free(aslr);
+}
diff --git a/tools/perf/util/aslr.h b/tools/perf/util/aslr.h
new file mode 100644
index 000000000000..ea984d82681f
--- /dev/null
+++ b/tools/perf/util/aslr.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PERF_ASLR_H
+#define __PERF_ASLR_H
+
+struct perf_tool;
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate);
+void aslr_tool__delete(struct perf_tool *aslr);
+
+#endif /* __PERF_ASLR_H */
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v1 2/2] perf test: Add inject ASLR test
2026-04-24 22:05 [PATCH v1 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-04-24 22:05 ` Ian Rogers
2026-04-25 2:05 ` [PATCH v2 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
1 sibling, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-04-24 22:05 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo,
Namhyung Kim, Jiri Olsa, Ian Rogers, Adrian Hunter, James Clark,
Gabriel Marin, linux-kernel, linux-perf-users
Add a new shell test `inject_aslr.sh` to verify the `perf inject --aslr`
feature. The test covers:
- Basic address remapping for user space samples.
- Pipe mode coverage for `perf record` piped into `perf inject --aslr`.
- Callchain address remapping.
- Consistency of `perf report` output before and after injection.
- Pipe mode report consistency.
- Dropping of samples that leak ASLR info (physical addresses).
- Kernel address remapping (skipping gracefully if permissions restrict
recording the kernel map).
- Kernel report consistency with address normalization.
Assisted-by: Gemini:gemini-3.1-pro-preview
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/tests/shell/inject_aslr.sh | 381 ++++++++++++++++++++++++++
1 file changed, 381 insertions(+)
create mode 100755 tools/perf/tests/shell/inject_aslr.sh
diff --git a/tools/perf/tests/shell/inject_aslr.sh b/tools/perf/tests/shell/inject_aslr.sh
new file mode 100755
index 000000000000..17544fe9ef6c
--- /dev/null
+++ b/tools/perf/tests/shell/inject_aslr.sh
@@ -0,0 +1,381 @@
+#!/bin/bash
+# perf inject --aslr test
+# SPDX-License-Identifier: GPL-2.0
+
+shelldir=$(dirname "$0")
+# shellcheck source=lib/perf_has_symbol.sh
+. "${shelldir}"/lib/perf_has_symbol.sh
+
+sym="noploop"
+
+# Set path to built perf
+PERF="/tmp/perf3/perf"
+
+skip_test_missing_symbol ${sym}
+
+# Create global temp directory
+temp_dir=$(mktemp -d /tmp/perf-test-aslr.XXXXXXXXXX)
+data="${temp_dir}/perf.data"
+data2="${temp_dir}/perf.data2"
+
+prog="${PERF} test -w noploop"
+[ "$(uname -m)" = "s390x" ] && prog="$prog 3"
+err=0
+
+set -e
+
+cleanup() {
+ # Check if temp_dir is set and looks sane before removing
+ if [[ "${temp_dir}" =~ ^/tmp/perf-test-aslr\. ]]; then
+ rm -rf "${temp_dir}"
+ fi
+ trap - EXIT TERM INT
+}
+
+trap_cleanup() {
+ echo "Unexpected signal in ${FUNCNAME[1]}"
+ cleanup
+ exit 1
+}
+trap trap_cleanup TERM INT
+
+get_noploop_addr() {
+ local file=$1
+ ${PERF} script -i "$file" | awk '{for(i=1;i<=NF;i++) if($i ~ /noploop\+/) {print $(i-1); exit}}'
+}
+
+test_basic_aslr() {
+ echo "Test basic ASLR remapping"
+ local data
+ data=$(mktemp /tmp/perf.data.basic.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.basic.XXXXXX)
+
+ ${PERF} record -e task-clock:u -o "${data}" ${prog}
+ ${PERF} inject -v --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Basic ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Basic ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Basic ASLR test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+}
+
+test_pipe_aslr() {
+ echo "Test pipe mode ASLR remapping"
+ local data
+ data=$(mktemp /tmp/perf.data.pipe.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.pipe.XXXXXX)
+
+ # Use tee to save the original pipe data for comparison
+ ${PERF} record -e task-clock:u -o - ${prog} | tee "${data}" | ${PERF} inject --aslr -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Pipe ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Pipe ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Pipe ASLR test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+}
+
+test_callchain_aslr() {
+ echo "Test Callchain ASLR remapping"
+ local data
+ data=$(mktemp /tmp/perf.data.callchain.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.callchain.XXXXXX)
+
+ ${PERF} record -g -e task-clock:u -o "${data}" ${prog}
+ ${PERF} inject --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Callchain ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Callchain ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ # Also check that the full script output differs (to cover callchains)
+ orig_script=$(${PERF} script -i "${data}" | grep -A 5 noploop | head -n 20)
+ new_script=$(${PERF} script -i "${data2}" | grep -A 5 noploop | head -n 20)
+
+ if [ "$orig_script" = "$new_script" ]; then
+ echo "Callchain ASLR test [Failed - callchain output is identical]"
+ err=1
+ else
+ echo "Callchain ASLR test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+ fi
+}
+
+test_report_aslr() {
+ echo "Test perf report consistency"
+ local data
+ data=$(mktemp /tmp/perf.data.report.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.report.XXXXXX)
+ local data_clean
+ data_clean=$(mktemp /tmp/perf.data.clean.XXXXXX)
+
+ ${PERF} record -e task-clock:u -o "${data}" ${prog}
+ # Use -b to inject build-ids and force ordered events processing in both
+ ${PERF} inject -b -i "${data}" -o "${data_clean}"
+ ${PERF} inject -v -b --aslr -i "${data}" -o "${data2}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ ${PERF} report -i "${data_clean}" --stdio > "${report1}"
+ ${PERF} report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}"
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}"
+
+ diff -u "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ -s "${diff_file}" ]; then
+ echo "Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Report ASLR test [Success]"
+ fi
+ rm -f "${data}" "${data2}" "${data_clean}" "${data}.old" "${data2}.old" "${data_clean}.old"
+}
+
+test_pipe_report_aslr() {
+ echo "Test pipe mode perf report consistency"
+ local data
+ data=$(mktemp /tmp/perf.data.pipe_report.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.pipe_report.XXXXXX)
+ local data_clean
+ data_clean=$(mktemp /tmp/perf.data.clean.XXXXXX)
+
+ # Use tee to save the original pipe data, then process it with inject -b
+ ${PERF} record -e task-clock:u -o - ${prog} | \
+ tee "${data}" | \
+ ${PERF} inject -b --aslr -o "${data2}"
+ ${PERF} inject -b -i "${data}" -o "${data_clean}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ ${PERF} report -i "${data_clean}" --stdio > "${report1}"
+ ${PERF} report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}"
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}"
+
+ diff -u "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ -s "${diff_file}" ]; then
+ echo "Pipe Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Pipe Report ASLR test [Success]"
+ fi
+ rm -f "${data}" "${data2}" "${data_clean}" "${data}.old" "${data2}.old" "${data_clean}.old"
+}
+
+test_dropped_samples() {
+ echo "Test dropped samples (phys-data)"
+ local data
+ data=$(mktemp /tmp/perf.data.dropped.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.dropped.XXXXXX)
+
+ # Check if --phys-data is supported by recording a short run
+ if ! ${PERF} record -e task-clock:u --phys-data -o "${data}" -- sleep 0.1 > /dev/null 2>&1; then
+ echo "Skipping dropped samples test as --phys-data is not supported"
+ return
+ fi
+
+ ${PERF} record -e task-clock:u --phys-data -o "${data}" ${prog}
+ ${PERF} inject --aslr -i "${data}" -o "${data2}"
+
+ # Verify that samples are dropped.
+ samples_count=$(${PERF} script -i "${data2}" | wc -l)
+
+ if [ "$samples_count" -gt 0 ]; then
+ echo "Dropped samples test [Failed - samples were not dropped]"
+ err=1
+ else
+ echo "Dropped samples test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+}
+
+test_kernel_aslr() {
+ echo "Test kernel ASLR remapping"
+ local kdata
+ kdata=$(mktemp /tmp/perf.data.kernel.XXXXXX)
+ local kdata2
+ kdata2=$(mktemp /tmp/perf.data2.kernel.XXXXXX)
+ local log_file
+ log_file=$(mktemp /tmp/kernel_record.log.XXXXXX)
+
+ # Try to record kernel samples
+ if ! ${PERF} record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel ASLR test as recording failed (maybe no permissions)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel ASLR test as kernel map could not be recorded (permissions restricted)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ ${PERF} inject -v --aslr -i "${kdata}" -o "${kdata2}"
+
+ # Check if kernel addresses are remapped.
+ orig_addr=$(${PERF} script -i "${kdata}" | awk '{for(i=1;i<=NF;i++) if($i ~ /^ffff/) {print $i; exit}}')
+ new_addr=$(${PERF} script -i "${kdata2}" | awk '{for(i=1;i<=NF;i++) if($i ~ /^ffff/) {print $i; exit}}')
+
+ echo "Kernel ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Kernel ASLR test [Failed - no kernel samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Kernel ASLR test [Success]"
+ rm -f "${kdata}" "${kdata2}" "${log_file}" "${kdata}.old" "${kdata2}.old"
+ fi
+}
+
+test_kernel_report_aslr() {
+ echo "Test kernel perf report consistency"
+ local kdata
+ kdata=$(mktemp /tmp/perf.data.kernel_report.XXXXXX)
+ local kdata2
+ kdata2=$(mktemp /tmp/perf.data2.kernel_report.XXXXXX)
+ local data_clean
+ data_clean=$(mktemp /tmp/perf.data.clean.XXXXXX)
+ local log_file
+ log_file=$(mktemp /tmp/kernel_report_record.log.XXXXXX)
+
+ # Try to record kernel samples
+ if ! ${PERF} record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel report test as recording failed (maybe no permissions)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel report test as kernel map could not be recorded (permissions restricted)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ # Use -b to inject build-ids and force ordered events processing in both
+ ${PERF} inject -b -i "${kdata}" -o "${data_clean}"
+ ${PERF} inject -v -b --aslr -i "${kdata}" -o "${kdata2}"
+
+ local report1="${temp_dir}/report_kernel1"
+ local report2="${temp_dir}/report_kernel2"
+ local report1_clean="${temp_dir}/report_kernel1.clean"
+ local report2_clean="${temp_dir}/report_kernel2.clean"
+ local diff_file="${temp_dir}/diff_kernel"
+
+ ${PERF} report -i "${data_clean}" --stdio > "${report1}"
+ ${PERF} report -i "${kdata2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' > "${report1_clean}"
+ grep '%' "${report2}" | grep -v '^#' > "${report2_clean}"
+
+ # Normalize kernel DSOs and addresses in clean reports
+ # This allows kernel modules to be either a module or kernel.kallsyms
+ local report1_norm="${temp_dir}/report_kernel1.norm"
+ local report2_norm="${temp_dir}/report_kernel2.norm"
+ awk '{$3 = ($3 ~ /^\[.*\]$/ ? "[kernel]" : $3); \
+ for(i=1;i<=NF;i++) \
+ if($i ~ /^ffff/ || $i ~ /^0x/ || $i == "0000000000000000") \
+ $i = "[addr]"; \
+ print}' "${report1_clean}" | sort > "${report1_norm}"
+ awk '{$3 = ($3 ~ /^\[.*\]$/ ? "[kernel]" : $3); \
+ for(i=1;i<=NF;i++) \
+ if($i ~ /^ffff/ || $i ~ /^0x/ || $i == "0000000000000000") \
+ $i = "[addr]"; \
+ print}' "${report2_clean}" | sort > "${report2_norm}"
+
+ diff -u "${report1_norm}" "${report2_norm}" > "${diff_file}" || true
+
+ if [ -s "${diff_file}" ]; then
+ echo "Kernel Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Kernel Report ASLR test [Success]"
+ fi
+ rm -f "${kdata}" "${kdata2}" "${data_clean}" "${log_file}" "${kdata}.old" "${kdata2}.old" "${data_clean}.old"
+}
+
+test_basic_aslr
+test_pipe_aslr
+test_callchain_aslr
+test_report_aslr
+test_pipe_report_aslr
+test_dropped_samples
+test_kernel_aslr
+test_kernel_report_aslr
+
+cleanup
+exit $err
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-04-24 22:05 [PATCH v1 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-04-24 22:05 ` [PATCH v1 2/2] perf test: Add inject ASLR test Ian Rogers
@ 2026-04-25 2:05 ` Ian Rogers
2026-04-25 2:05 ` [PATCH v2 2/2] perf test: Add inject ASLR test Ian Rogers
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
1 sibling, 2 replies; 15+ messages in thread
From: Ian Rogers @ 2026-04-25 2:05 UTC (permalink / raw)
To: irogers
Cc: acme, adrian.hunter, gmx, james.clark, jolsa, linux-kernel,
linux-perf-users, mingo, namhyung, peterz
If perf.data files are taken from one machine to another they make
leak virtual addresses and so weaken ASLR on the machine they are
coming from. Add a '--aslr' option for perf inject that remaps all
virtual addresses, or drops data/events, so that the virtual address
information isn't leaked.
When events are not known/handled by the tool they are dropped. This
makes the tool conservative and it should never leak ASLR information,
but it means virtual address remapping is needed for cases like
auxtrace.
Assisted-by: Gemini:gemini-3.1-pro-preview
Signed-off-by: Ian Rogers <irogers@google.com>
Co-developed-by: Gabriel Marin <gmx@google.com>
Signed-off-by: Gabriel Marin <gmx@google.com>
---
tools/perf/builtin-inject.c | 11 +-
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 816 ++++++++++++++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
4 files changed, 837 insertions(+), 1 deletion(-)
create mode 100644 tools/perf/util/aslr.c
create mode 100644 tools/perf/util/aslr.h
diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c
index f174bc69cec4..fa3a71e23f1b 100644
--- a/tools/perf/builtin-inject.c
+++ b/tools/perf/builtin-inject.c
@@ -8,6 +8,7 @@
*/
#include "builtin.h"
+#include "util/aslr.h"
#include "util/color.h"
#include "util/dso.h"
#include "util/vdso.h"
@@ -123,6 +124,7 @@ struct perf_inject {
bool in_place_update_dry_run;
bool copy_kcore_dir;
bool convert_callchain;
+ bool aslr;
const char *input_name;
struct perf_data output;
u64 bytes_written;
@@ -2564,6 +2566,8 @@ int cmd_inject(int argc, const char **argv)
" instance has a subdir"),
OPT_BOOLEAN(0, "convert-callchain", &inject.convert_callchain,
"Generate callchains using DWARF and drop register/stack data"),
+ OPT_BOOLEAN(0, "aslr", &inject.aslr,
+ "Remap virtual memory addresses similar to ASLR"),
OPT_END()
};
const char * const inject_usage[] = {
@@ -2571,6 +2575,7 @@ int cmd_inject(int argc, const char **argv)
NULL
};
bool ordered_events;
+ struct perf_tool *tool = &inject.tool;
if (!inject.itrace_synth_opts.set) {
/* Disable eager loading of kernel symbols that adds overhead to perf inject. */
@@ -2684,7 +2689,9 @@ int cmd_inject(int argc, const char **argv)
inject.tool.schedstat_domain = perf_event__repipe_op2_synth;
inject.tool.dont_split_sample_group = true;
inject.tool.merge_deferred_callchains = false;
- inject.session = __perf_session__new(&data, &inject.tool,
+ if (inject.aslr)
+ tool = aslr_tool__new(&inject.tool);
+ inject.session = __perf_session__new(&data, tool,
/*trace_event_repipe=*/inject.output.is_pipe,
/*host_env=*/NULL);
@@ -2789,6 +2796,8 @@ int cmd_inject(int argc, const char **argv)
strlist__delete(inject.known_build_ids);
zstd_fini(&(inject.session->zstd_data));
perf_session__delete(inject.session);
+ if (inject.aslr)
+ aslr_tool__delete(tool);
out_close_output:
if (!inject.in_place_update)
perf_data__close(&inject.output);
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 70cc91d00804..65b96f3b87e2 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -6,6 +6,7 @@ perf-util-y += arm64-frame-pointer-unwind-support.o
perf-util-y += addr2line.o
perf-util-y += addr_location.o
perf-util-y += annotate.o
+perf-util-y += aslr.o
perf-util-y += blake2s.o
perf-util-y += block-info.o
perf-util-y += block-range.o
diff --git a/tools/perf/util/aslr.c b/tools/perf/util/aslr.c
new file mode 100644
index 000000000000..b21c5e82539d
--- /dev/null
+++ b/tools/perf/util/aslr.c
@@ -0,0 +1,816 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "aslr.h"
+
+#include "addr_location.h"
+#include "debug.h"
+#include "event.h"
+#include "evsel.h"
+#include "machine.h"
+#include "map.h"
+#include "thread.h"
+#include "tool.h"
+
+#include <internal/lib.h> /* page_size */
+#include <linux/compiler.h>
+#include <errno.h>
+#include <inttypes.h>
+
+struct remap_addresses_key {
+ u64 start_addr;
+ pid_t pid;
+};
+
+struct aslr_tool {
+ /** @tool: The tool implemented here and a pointer to a delegate to process the data. */
+ struct delegate_tool tool;
+ /** @machine: The machine with the input, not remapped, virtual address layout. */
+ struct machine machine;
+ /** @event_copy: Buffer used to create an event to pass to the delegate. */
+ char event_copy[PERF_SAMPLE_MAX_SIZE];
+ /** @remap_addresses: mapping from remap_addresses_key to remapped address. */
+ struct hashmap remap_addresses;
+ /** @top_addresses: mapping from process to max remapped address. */
+ struct hashmap top_addresses;
+ /** @first_kernel_mapping: flag indicating if we are still to process any kernel mapping. */
+ bool first_kernel_mapping;
+};
+
+static const pid_t kernel_pid = -1;
+
+/* Start remapping user processes from a small non-zero offset. */
+static const u64 user_space_start = 0x200000;
+static const u64 kernel_space_start = 0xffff800010000000;
+
+static size_t remap_addresses__hash(long _key, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key = (struct remap_addresses_key *)_key;
+
+ return key->start_addr ^ (key->start_addr >> 12) ^ key->pid;
+}
+
+static bool remap_addresses__equal(long _key1, long _key2, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key1 = (struct remap_addresses_key *)_key1;
+ struct remap_addresses_key *key2 = (struct remap_addresses_key *)_key2;
+
+ return key1->pid == key2->pid && key1->start_addr == key2->start_addr;
+}
+
+static size_t top_addresses__hash(long key, void *ctx __maybe_unused)
+{
+ return key;
+}
+
+static bool top_addresses__equal(long key1, long key2, void *ctx __maybe_unused)
+{
+ return key1 == key2;
+}
+
+static u64 round_up_to_page_size(u64 addr)
+{
+ return (addr + page_size - 1) & ~((u64)page_size - 1);
+}
+
+static u64 aslr_tool__remap_address(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 addr)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u64 *remap_addr_ptr = NULL;
+ u8 effective_cpumode = cpumode;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&al);
+ if (!thread__find_map(aslr_thread, cpumode, addr, &al)) {
+ /*
+ * If lookup fails with specified cpumode, try fallback to the other space
+ * to be robust against bad cpumode in samples.
+ */
+ effective_cpumode = (cpumode == PERF_RECORD_MISC_KERNEL) ?
+ PERF_RECORD_MISC_USER : PERF_RECORD_MISC_KERNEL;
+ if (!thread__find_map(aslr_thread, effective_cpumode, addr, &al)) {
+ pr_debug("Cannot find mmap for address %lx in either space, pid=%d\n",
+ addr, aslr_thread->pid_);
+ addr_location__exit(&al);
+ return 0; /* No mmap. */
+ }
+ }
+
+ key.pid = effective_cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+ key.start_addr = map__start(al.map);
+ if (!hashmap__find(&aslr->remap_addresses, &key, &remap_addr_ptr)) {
+ pr_debug("Cannot find a remapped entry for address %lx in mapping %lx(%lx) for pid=%d\n",
+ addr, map__start(al.map), map__size(al.map), key.pid);
+ addr_location__exit(&al);
+ return 0;
+ }
+ remap_addr = *remap_addr_ptr + (addr - map__start(al.map));
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_mapping(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 start, u64 len)
+{
+ struct addr_location prev_al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u64 *remap_addr_ptr = NULL;
+ u64 *max_addr_ptr = NULL;
+ /* If mapping is contiguous to the previous process mapping. */
+ bool is_contiguous = false;
+ bool first_mapping = false; /* first process mapping. */
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&prev_al);
+ if (thread__find_map(aslr_thread, cpumode, start-1, &prev_al)) {
+ if (map__start(prev_al.map) + map__size(prev_al.map) == start) {
+ is_contiguous = true;
+ } else {
+ pr_debug("Previous mmap [%lx, %lx] overlaps current map [%lx, %lx]\n",
+ map__start(prev_al.map),
+ map__start(prev_al.map) + map__size(prev_al.map),
+ start, start+len);
+ }
+ }
+ addr_location__exit(&prev_al);
+
+ key.pid = cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+ key.start_addr = start;
+ if (hashmap__find(&aslr->remap_addresses, &key, &remap_addr_ptr))
+ return *remap_addr_ptr;
+
+ if (!hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ /* First mapping in this process. Don't add a page gap. */
+ first_mapping = true;
+ remap_addr = (cpumode == PERF_RECORD_MISC_KERNEL ?
+ kernel_space_start : user_space_start);
+ } else {
+ remap_addr = *max_addr_ptr;
+ }
+
+ remap_addr = round_up_to_page_size(remap_addr);
+ if (!is_contiguous && !first_mapping)
+ remap_addr += page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+
+ remap_addr_ptr = malloc(sizeof(u64));
+
+ if (!new_key || !remap_addr_ptr) {
+ free(new_key);
+ free(remap_addr_ptr);
+ return 0;
+ }
+ *new_key = key;
+ *remap_addr_ptr = remap_addr;
+ if (hashmap__add(&aslr->remap_addresses, new_key, remap_addr_ptr) != 0) {
+ pr_debug("Failed to add remap_addresses entry for pid=%d, mapping start=%lx, remapped start=%lx",
+ key.pid, start, remap_addr);
+ free(new_key);
+ free(remap_addr_ptr);
+ return 0;
+ }
+ }
+
+ max_addr_ptr = malloc(sizeof(u64));
+
+ if (!max_addr_ptr)
+ return 0;
+ *max_addr_ptr = remap_addr + len;
+ hashmap__insert(&aslr->top_addresses, key.pid, max_addr_ptr,
+ first_mapping ? HASHMAP_ADD : HASHMAP_UPDATE, NULL, NULL);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_ksymbol(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u64 addr, u32 len)
+{
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u64 *max_addr_ptr = NULL;
+ u64 *remap_addr_ptr = NULL;
+ bool first_mapping = false;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ key.pid = aslr_thread->pid_;
+ key.start_addr = addr;
+ if (hashmap__find(&aslr->remap_addresses, &key, &remap_addr_ptr))
+ return *remap_addr_ptr;
+
+ first_mapping = !hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr);
+ if (first_mapping)
+ remap_addr = kernel_space_start;
+ else
+ remap_addr = *max_addr_ptr;
+ remap_addr = round_up_to_page_size(remap_addr) + page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+
+ remap_addr_ptr = malloc(sizeof(u64));
+
+ if (!new_key || !remap_addr_ptr) {
+ free(new_key);
+ free(remap_addr_ptr);
+ return 0;
+ }
+ *new_key = key;
+ *remap_addr_ptr = remap_addr;
+ if (hashmap__add(&aslr->remap_addresses, new_key, remap_addr_ptr) < 0) {
+ pr_debug("Failed to add remap_addresses entry for pid=%d, ksymbol=%lx, remapped address=%lx",
+ key.pid, addr, remap_addr);
+ free(new_key);
+ free(remap_addr_ptr);
+ return 0;
+ }
+ }
+
+ max_addr_ptr = malloc(sizeof(u64));
+
+ if (!max_addr_ptr)
+ return 0;
+ *max_addr_ptr = remap_addr + len;
+ hashmap__insert(&aslr->top_addresses, key.pid, max_addr_ptr,
+ first_mapping ? HASHMAP_ADD : HASHMAP_UPDATE, NULL, NULL);
+ return remap_addr;
+}
+
+
+static int aslr_tool__process_mmap(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ u8 cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+ struct thread *thread;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(&aslr->machine, event->mmap.pid, event->mmap.tid);
+ memcpy(&new_event->mmap, &event->mmap, event->mmap.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap.start, event->mmap.len);
+ if (aslr->first_kernel_mapping && cpumode == PERF_RECORD_MISC_KERNEL) {
+ /*
+ * If this is the first kernel image, we need to adjust the pgoff by a
+ * similar delta.
+ */
+ new_event->mmap.pgoff = event->mmap.pgoff - event->mmap.start +
+ new_event->mmap.start;
+ aslr->first_kernel_mapping = false;
+ }
+ err = delegate->mmap(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_mmap2(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ u8 cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+ struct thread *thread;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap2(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(&aslr->machine, event->mmap2.pid, event->mmap2.tid);
+ memcpy(&new_event->mmap2, &event->mmap2, event->mmap2.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap2.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap2.start, event->mmap2.len);
+ if (aslr->first_kernel_mapping && cpumode == PERF_RECORD_MISC_KERNEL) {
+ /*
+ * If this is the first kernel image, we need to adjust the pgoff by a
+ * similar delta.
+ */
+ new_event->mmap2.pgoff = event->mmap2.pgoff - event->mmap2.start +
+ new_event->mmap2.start;
+ aslr->first_kernel_mapping = false;
+ }
+ err = delegate->mmap2(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_comm(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_comm(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ return delegate->comm(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_fork(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_fork(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ return delegate->fork(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_exit(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_exit(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ return delegate->exit(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_text_poke(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ u8 cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+ struct thread *thread;
+ int err;
+
+ thread = machine__findnew_thread(&aslr->machine, sample->pid, sample->tid);
+ memcpy(&new_event->text_poke, &event->text_poke, event->text_poke.header.size);
+ new_event->text_poke.addr = aslr_tool__remap_address(aslr, thread, cpumode,
+ event->text_poke.addr);
+
+ err = delegate->text_poke(delegate, new_event, sample, machine);
+
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_ksymbol(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ struct thread *thread;
+ int err;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_ksymbol(tool, event, sample, &aslr->machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(&aslr->machine, kernel_pid, 0);
+ memcpy(&new_event->ksymbol, &event->ksymbol, event->ksymbol.header.size);
+ /* Remaps the ksymbol.start */
+ new_event->ksymbol.addr = aslr_tool__remap_ksymbol(aslr, thread,
+ event->ksymbol.addr, event->ksymbol.len);
+
+ err = delegate->ksymbol(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static inline int copy_u64(__u64 *in_array, __u64 *out_array,
+ size_t *i, size_t *j, const __u64 max_i)
+{
+ if (*i >= max_i - 1)
+ return -EFAULT;
+ out_array[(*j)++] = in_array[(*i)++];
+ return 0;
+}
+
+static int aslr_tool__process_sample(const struct perf_tool *tool, union perf_event *event,
+ struct perf_sample *sample,
+ struct evsel *evsel, struct machine *machine)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ int ret;
+ u64 sample_type = evsel->core.attr.sample_type;
+ struct thread *thread = machine__findnew_thread(&aslr->machine, sample->pid, sample->tid);
+ const __u64 max_i = event->header.size / sizeof(__u64);
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+ struct perf_sample new_sample;
+ __u64 *in_array, *out_array;
+ u8 cpumode = sample->cpumode;
+ u64 addr;
+ size_t i = 0, j = 0;
+
+ new_event->sample.header = event->sample.header;
+
+ in_array = &event->sample.array[0];
+ out_array = &new_event->sample.array[0];
+
+
+
+ if (sample_type & PERF_SAMPLE_IDENTIFIER)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* id */
+ if (sample_type & PERF_SAMPLE_IP) {
+ i++;
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode,
+ sample->ip);
+ }
+ if (sample_type & PERF_SAMPLE_TID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* pid, tid */
+ if (sample_type & PERF_SAMPLE_TIME)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* time */
+ if (sample_type & PERF_SAMPLE_ADDR) {
+ i++; /* addr */
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode,
+ sample->addr);
+ }
+ if (sample_type & PERF_SAMPLE_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* id */
+ if (sample_type & PERF_SAMPLE_STREAM_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* stream_id */
+ if (sample_type & PERF_SAMPLE_CPU)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* cpu, res */
+ if (sample_type & PERF_SAMPLE_PERIOD)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* period */
+ if (sample_type & PERF_SAMPLE_READ) {
+ if ((evsel->core.attr.read_format & PERF_FORMAT_GROUP) == 0) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* value */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* time_enabled */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* time_running */
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* id */
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* lost */
+ } else {
+ u64 nr;
+
+ if (i > max_i)
+ return -EFAULT;
+ nr = out_array[j++] = in_array[i++];
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* time_enabled */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* time_running */
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* value */
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* id */
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* lost */
+ }
+ }
+ }
+ if (sample_type & PERF_SAMPLE_CALLCHAIN) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* nr */
+
+ for (u64 cntr = 0; cntr < sample->callchain->nr; cntr++) {
+ if (i > max_i)
+ return -EFAULT;
+ i++;
+ addr = sample->callchain->ips[cntr];
+ if (addr >= PERF_CONTEXT_MAX) {
+ /* Copy context values as is. */
+ out_array[j++] = addr;
+ switch (addr) {
+ case PERF_CONTEXT_HV:
+ cpumode = PERF_RECORD_MISC_HYPERVISOR;
+ break;
+ case PERF_CONTEXT_KERNEL:
+ cpumode = PERF_RECORD_MISC_KERNEL;
+ break;
+ case PERF_CONTEXT_USER:
+ cpumode = PERF_RECORD_MISC_USER;
+ break;
+ case PERF_CONTEXT_GUEST:
+ case PERF_CONTEXT_GUEST_KERNEL:
+ case PERF_CONTEXT_GUEST_USER:
+ case PERF_CONTEXT_USER_DEFERRED:
+ break;
+ default:
+ pr_debug("invalid callchain context: %"PRIx64"\n", addr);
+ /*
+ * It seems the callchain is corrupted.
+ * Discard sample.
+ */
+ return 0;
+ }
+ continue;
+ }
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode,
+ sample->callchain->ips[cntr]);
+ }
+ }
+ if (sample_type & PERF_SAMPLE_RAW) {
+ size_t bytes = sizeof(u32) + sample->raw_size;
+
+ size_t u64_words = (bytes + 7) / 8;
+
+ if ((i + u64_words) > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], bytes);
+ i += u64_words;
+ j += u64_words;
+ /*
+ * TODO: certain raw samples can be remapped, such as
+ * tracepoints by examining their fields.
+ */
+ pr_debug("Dropping raw samples as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_BRANCH_STACK) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* nr */
+ if (evsel->core.attr.branch_sample_type & PERF_SAMPLE_BRANCH_HW_INDEX)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* hw_idx */
+ if (sample->branch_stack->nr > (ULLONG_MAX / 3))
+ return -EFAULT;
+ if (i + (sample->branch_stack->nr * 3) > max_i)
+ return -EFAULT;
+ for (u64 cntr = 0; cntr < sample->branch_stack->nr; cntr++) {
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, sample->cpumode,
+ in_array[i++]); /* from */
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, sample->cpumode,
+ in_array[i++]); /* to */
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* flags */
+ }
+ if (evsel->core.attr.branch_sample_type & PERF_SAMPLE_BRANCH_COUNTERS) {
+ if (i + sample->branch_stack->nr > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], sample->branch_stack->nr * sizeof(u64));
+ i += sample->branch_stack->nr;
+ j += sample->branch_stack->nr;
+ /* TODO: confirm branch counters don't leak ASLR information. */
+ pr_debug("Dropping sample branch counters as possible ASLR leak\n");
+ return 0;
+ }
+ }
+ if (sample_type & PERF_SAMPLE_REGS_USER) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* abi */
+ if (sample->user_regs->abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_user);
+
+ if (i + nr > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping regs user sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_STACK_USER) {
+ u64 size;
+
+ if (i > max_i)
+ return -EFAULT;
+ size = out_array[j++] = in_array[i++];
+ if (size > 0) {
+ memcpy(&out_array[j], &in_array[i], size);
+ i += size / sizeof(u64);
+ j += size / sizeof(u64);
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* dyn_size */
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping stack user sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_WEIGHT_TYPE)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* perf_sample_weight */
+ if (sample_type & PERF_SAMPLE_DATA_SRC)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* data_src */
+ if (sample_type & PERF_SAMPLE_TRANSACTION)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* transaction */
+ if (sample_type & PERF_SAMPLE_REGS_INTR) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* abi */
+ if (sample->intr_regs->abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_intr);
+
+ if (i + nr > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping interrupt register sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_PHYS_ADDR) {
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* phys_addr */
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping physical address sample as possible ASLR leak\n");
+ return 0;
+ }
+ if (sample_type & PERF_SAMPLE_CGROUP)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* cgroup */
+ if (sample_type & PERF_SAMPLE_DATA_PAGE_SIZE)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* data_page_size */
+ if (sample_type & PERF_SAMPLE_CODE_PAGE_SIZE)
+ if (copy_u64(in_array, out_array, &i, &j, max_i))
+ return -EFAULT; /* code_page_size */
+
+ if (sample_type & PERF_SAMPLE_AUX) {
+ u64 size;
+
+ if (i > max_i)
+ return -EFAULT;
+ size = out_array[j++] = in_array[i++];
+ if (i + (size / sizeof(u64)) > max_i)
+ return -EFAULT;
+ memcpy(&out_array[j], &in_array[i], size);
+ i += size / sizeof(u64);
+ j += size / sizeof(u64);
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping aux sample as possible ASLR leak\n");
+ return 0;
+ }
+
+ if (evsel__is_offcpu_event(evsel)) {
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping off-CPU sample as possible ASLR leak\n");
+ return 0;
+ }
+
+ perf_sample__init(&new_sample, /*all=*/ true);
+ ret = evsel__parse_sample(evsel, new_event, &new_sample);
+ if (ret)
+ return ret;
+
+ ret = delegate->sample(delegate, new_event, &new_sample, evsel, machine);
+ perf_sample__exit(&new_sample);
+ thread__put(thread);
+ return ret;
+}
+
+static int aslr_tool__process_attr(const struct perf_tool *tool,
+ union perf_event *event,
+ struct evlist **pevlist)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct perf_tool *delegate = aslr->tool.delegate;
+ union perf_event *new_event = (union perf_event *)aslr->event_copy;
+
+ memcpy(&new_event->attr, &event->attr, event->attr.header.size);
+ if (new_event->attr.attr.type == PERF_TYPE_BREAKPOINT)
+ new_event->attr.attr.bp_addr = 0; /* Conservatively remove addresses. */
+ if (new_event->attr.attr.kprobe_addr >= 0xffff800000000000)
+ new_event->attr.attr.kprobe_addr = 0; /* Conservatively remove addresses. */
+
+ return delegate->attr(delegate, new_event, pevlist);
+}
+
+static void aslr_tool__init(struct aslr_tool *aslr, struct perf_tool *delegate)
+{
+ delegate_tool__init(&aslr->tool, delegate);
+ aslr->tool.tool.ordered_events = true;
+
+ machine__init(&aslr->machine, "", HOST_KERNEL_ID);
+
+ hashmap__init(&aslr->remap_addresses,
+ remap_addresses__hash, remap_addresses__equal,
+ /*ctx=*/NULL);
+ hashmap__init(&aslr->top_addresses,
+ top_addresses__hash, top_addresses__equal,
+ /*ctx=*/NULL);
+ aslr->first_kernel_mapping = true;
+
+ aslr->tool.tool.sample = aslr_tool__process_sample;
+ /* read - reads a counter, okay to delegate. */
+ aslr->tool.tool.mmap = aslr_tool__process_mmap;
+ aslr->tool.tool.mmap2 = aslr_tool__process_mmap2;
+ aslr->tool.tool.comm = aslr_tool__process_comm;
+ aslr->tool.tool.fork = aslr_tool__process_fork;
+ aslr->tool.tool.exit = aslr_tool__process_exit;
+ /* namesspaces, cgroup, lost, lost_sample, aux, */
+ /* itrace_start, aux_output_hw_id, context_switch, throttle, unthrottle */
+ /* - no virtual addresses. */
+ aslr->tool.tool.ksymbol = aslr_tool__process_ksymbol;
+ /* bpf - no virtual address. */
+ aslr->tool.tool.text_poke = aslr_tool__process_text_poke;
+ aslr->tool.tool.attr = aslr_tool__process_attr;
+ /* event_update, tracing_data, finished_round, build_id, id_index, */
+ /* auxtrace_info, auxtrace_error, time_conv, thread_map, cpu_map, */
+ /* stat_config, stat, feature, finished_init, bpf_metadata, compressed, */
+ /* auxtrace - no virtual addresses. */
+}
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate)
+{
+ struct aslr_tool *aslr = malloc(sizeof(*aslr));
+
+ if (!aslr)
+ return NULL;
+
+ aslr_tool__init(aslr, delegate);
+ return &aslr->tool.tool;
+}
+
+void aslr_tool__delete(struct perf_tool *tool)
+{
+ struct delegate_tool *del_tool = container_of(tool, struct delegate_tool, tool);
+ struct aslr_tool *aslr = container_of(del_tool, struct aslr_tool, tool);
+ struct hashmap_entry *cur;
+ size_t bkt;
+
+ hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
+ zfree(&cur->pkey);
+ zfree(&cur->pvalue);
+ }
+ hashmap__for_each_entry(&aslr->top_addresses, cur, bkt) {
+ zfree(&cur->pvalue);
+ }
+
+ hashmap__clear(&aslr->remap_addresses);
+ hashmap__clear(&aslr->top_addresses);
+ machine__exit(&aslr->machine);
+ free(aslr);
+}
diff --git a/tools/perf/util/aslr.h b/tools/perf/util/aslr.h
new file mode 100644
index 000000000000..ea984d82681f
--- /dev/null
+++ b/tools/perf/util/aslr.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PERF_ASLR_H
+#define __PERF_ASLR_H
+
+struct perf_tool;
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate);
+void aslr_tool__delete(struct perf_tool *aslr);
+
+#endif /* __PERF_ASLR_H */
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v2 2/2] perf test: Add inject ASLR test
2026-04-25 2:05 ` [PATCH v2 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-04-25 2:05 ` Ian Rogers
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
1 sibling, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-04-25 2:05 UTC (permalink / raw)
To: irogers
Cc: acme, adrian.hunter, gmx, james.clark, jolsa, linux-kernel,
linux-perf-users, mingo, namhyung, peterz
Add a new shell test `inject_aslr.sh` to verify the `perf inject --aslr`
feature. The test covers:
- Basic address remapping for user space samples.
- Pipe mode coverage for `perf record` piped into `perf inject --aslr`.
- Callchain address remapping.
- Consistency of `perf report` output before and after injection.
- Pipe mode report consistency.
- Dropping of samples that leak ASLR info (physical addresses).
- Kernel address remapping (skipping gracefully if permissions restrict
recording the kernel map).
- Kernel report consistency with address normalization.
Assisted-by: Gemini:gemini-3.1-pro-preview
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/tests/shell/inject_aslr.sh | 386 ++++++++++++++++++++++++++
1 file changed, 386 insertions(+)
create mode 100755 tools/perf/tests/shell/inject_aslr.sh
diff --git a/tools/perf/tests/shell/inject_aslr.sh b/tools/perf/tests/shell/inject_aslr.sh
new file mode 100755
index 000000000000..951809eecfd4
--- /dev/null
+++ b/tools/perf/tests/shell/inject_aslr.sh
@@ -0,0 +1,386 @@
+#!/bin/bash
+# perf inject --aslr test
+# SPDX-License-Identifier: GPL-2.0
+
+shelldir=$(dirname "$0")
+# shellcheck source=lib/perf_has_symbol.sh
+. "${shelldir}"/lib/perf_has_symbol.sh
+
+sym="noploop"
+
+skip_test_missing_symbol ${sym}
+
+# Create global temp directory
+temp_dir=$(mktemp -d /tmp/perf-test-aslr.XXXXXXXXXX)
+data="${temp_dir}/perf.data"
+data2="${temp_dir}/perf.data2"
+
+prog="perf test -w noploop"
+[ "$(uname -m)" = "s390x" ] && prog="$prog 3"
+err=0
+
+set -e
+
+cleanup() {
+ # Check if temp_dir is set and looks sane before removing
+ if [[ "${temp_dir}" =~ ^/tmp/perf-test-aslr\. ]]; then
+ rm -rf "${temp_dir}"
+ fi
+ trap - EXIT TERM INT
+}
+
+trap_cleanup() {
+ echo "Unexpected signal in ${FUNCNAME[1]}"
+ cleanup
+ exit 1
+}
+trap trap_cleanup TERM INT
+
+get_noploop_addr() {
+ local file=$1
+ perf script -i "$file" | awk '{for(i=1;i<=NF;i++) if($i ~ /noploop\+/) {print $(i-1); exit}}'
+}
+
+test_basic_aslr() {
+ echo "Test basic ASLR remapping"
+ local data
+ data=$(mktemp /tmp/perf.data.basic.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.basic.XXXXXX)
+
+ perf record -e task-clock:u -o "${data}" ${prog}
+ perf inject -v --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Basic ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Basic ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Basic ASLR test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+}
+
+test_pipe_aslr() {
+ echo "Test pipe mode ASLR remapping"
+ local data
+ data=$(mktemp /tmp/perf.data.pipe.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.pipe.XXXXXX)
+
+ # Use tee to save the original pipe data for comparison
+ perf record -e task-clock:u -o - ${prog} | tee "${data}" | perf inject --aslr -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Pipe ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Pipe ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Pipe ASLR test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+}
+
+test_callchain_aslr() {
+ echo "Test Callchain ASLR remapping"
+ local data
+ data=$(mktemp /tmp/perf.data.callchain.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.callchain.XXXXXX)
+
+ perf record -g -e task-clock:u -o "${data}" ${prog}
+ perf inject --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Callchain ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Callchain ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ # Also check that the full script output differs (to cover callchains)
+ orig_script=$(perf script -i "${data}" | grep -A 5 noploop | head -n 20)
+ new_script=$(perf script -i "${data2}" | grep -A 5 noploop | head -n 20)
+
+ if [ "$orig_script" = "$new_script" ]; then
+ echo "Callchain ASLR test [Failed - callchain output is identical]"
+ err=1
+ else
+ echo "Callchain ASLR test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+ fi
+}
+
+test_report_aslr() {
+ echo "Test perf report consistency"
+ local data
+ data=$(mktemp /tmp/perf.data.report.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.report.XXXXXX)
+ local data_clean
+ data_clean=$(mktemp /tmp/perf.data.clean.XXXXXX)
+
+ perf record -e task-clock:u -o "${data}" ${prog}
+ # Use -b to inject build-ids and force ordered events processing in both
+ perf inject -b -i "${data}" -o "${data_clean}"
+ perf inject -v -b --aslr -i "${data}" -o "${data2}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}"
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}"
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ -s "${diff_file}" ]; then
+ echo "Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Report ASLR test [Success]"
+ fi
+ rm -f "${data}" "${data2}" "${data_clean}" "${data}.old" "${data2}.old" "${data_clean}.old"
+}
+
+test_pipe_report_aslr() {
+ echo "Test pipe mode perf report consistency"
+ local data
+ data=$(mktemp /tmp/perf.data.pipe_report.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.pipe_report.XXXXXX)
+ local data_clean
+ data_clean=$(mktemp /tmp/perf.data.clean.XXXXXX)
+
+ # Use tee to save the original pipe data, then process it with inject -b
+ perf record -e task-clock:u -o - ${prog} | \
+ tee "${data}" | \
+ perf inject -b --aslr -o "${data2}"
+ perf inject -b -i "${data}" -o "${data_clean}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}"
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}"
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ -s "${diff_file}" ]; then
+ echo "Pipe Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Pipe Report ASLR test [Success]"
+ fi
+ rm -f "${data}" "${data2}" "${data_clean}" "${data}.old" "${data2}.old" "${data_clean}.old"
+}
+
+test_dropped_samples() {
+ echo "Test dropped samples (phys-data)"
+ local data
+ data=$(mktemp /tmp/perf.data.dropped.XXXXXX)
+ local data2
+ data2=$(mktemp /tmp/perf.data2.dropped.XXXXXX)
+
+ # Check if --phys-data is supported by recording a short run
+ if ! perf record -e task-clock:u --phys-data -o "${data}" -- sleep 0.1 > /dev/null 2>&1; then
+ echo "Skipping dropped samples test as --phys-data is not supported"
+ return
+ fi
+
+ perf record -e task-clock:u --phys-data -o "${data}" ${prog}
+ perf inject --aslr -i "${data}" -o "${data2}"
+
+ # Verify that samples are dropped.
+ samples_count=$(perf script -i "${data2}" | wc -l)
+
+ if [ "$samples_count" -gt 0 ]; then
+ echo "Dropped samples test [Failed - samples were not dropped]"
+ err=1
+ else
+ echo "Dropped samples test [Success]"
+ rm -f "${data}" "${data2}" "${data}.old" "${data2}.old"
+ fi
+}
+
+test_kernel_aslr() {
+ echo "Test kernel ASLR remapping"
+ local kdata
+ kdata=$(mktemp /tmp/perf.data.kernel.XXXXXX)
+ local kdata2
+ kdata2=$(mktemp /tmp/perf.data2.kernel.XXXXXX)
+ local log_file
+ log_file=$(mktemp /tmp/kernel_record.log.XXXXXX)
+
+ # Try to record kernel samples
+ if ! perf record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel ASLR test as recording failed (maybe no permissions)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel ASLR test as kernel map could not be recorded (permissions restricted)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ perf inject -v --aslr -i "${kdata}" -o "${kdata2}"
+
+ # Check if kernel addresses are remapped.
+ orig_addr=$(perf script -i "${kdata}" | \
+ awk '{for(i=1;i<=NF;i++) if($i ~ /^ffff/) {print $i; exit}}')
+ new_addr=$(perf script -i "${kdata2}" | \
+ awk '{for(i=1;i<=NF;i++) if($i ~ /^ffff/) {print $i; exit}}')
+
+ echo "Kernel ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Kernel ASLR test [Failed - no kernel samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Kernel ASLR test [Success]"
+ rm -f "${kdata}" "${kdata2}" "${log_file}" "${kdata}.old" "${kdata2}.old"
+ fi
+}
+
+test_kernel_report_aslr() {
+ echo "Test kernel perf report consistency"
+ local kdata
+ kdata=$(mktemp /tmp/perf.data.kernel_report.XXXXXX)
+ local kdata2
+ kdata2=$(mktemp /tmp/perf.data2.kernel_report.XXXXXX)
+ local data_clean
+ data_clean=$(mktemp /tmp/perf.data.clean.XXXXXX)
+ local log_file
+ log_file=$(mktemp /tmp/kernel_report_record.log.XXXXXX)
+
+ # Try to record kernel samples
+ if ! perf record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel report test as recording failed (maybe no permissions)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel report test as kernel map could not be recorded (permissions restricted)"
+ rm -f "${kdata}" "${log_file}"
+ return
+ fi
+
+ # Use -b to inject build-ids and force ordered events processing in both
+ perf inject -b -i "${kdata}" -o "${data_clean}"
+ perf inject -v -b --aslr -i "${kdata}" -o "${kdata2}"
+
+ local report1="${temp_dir}/report_kernel1"
+ local report2="${temp_dir}/report_kernel2"
+ local report1_clean="${temp_dir}/report_kernel1.clean"
+ local report2_clean="${temp_dir}/report_kernel2.clean"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${kdata2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' > "${report1_clean}"
+ grep '%' "${report2}" | grep -v '^#' > "${report2_clean}"
+
+ # Normalize kernel DSOs and addresses in clean reports
+ # This allows kernel modules to be either a module or kernel.kallsyms
+ local report1_norm="${temp_dir}/report_kernel1.norm"
+ local report2_norm="${temp_dir}/report_kernel2.norm"
+
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); \
+ gsub(/0x[0-9a-f]+/, "[addr]", $0); \
+ gsub(/ffff[0-9a-f]+/, "[addr]", $0); \
+ gsub(/0000000000000000/, "[addr]", $0); \
+ print}' "${report1_clean}" > "${report1_norm}"
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); \
+ gsub(/0x[0-9a-f]+/, "[addr]", $0); \
+ gsub(/ffff[0-9a-f]+/, "[addr]", $0); \
+ gsub(/0000000000000000/, "[addr]", $0); \
+ print}' "${report2_clean}" > "${report2_norm}"
+
+ # Calculate sum of percentages for [kernel] samples
+ sum1=$(awk '/\[kernel\]/ {sum += $1} END {print sum}' "${report1_norm}")
+ sum2=$(awk '/\[kernel\]/ {sum += $1} END {print sum}' "${report2_norm}")
+
+ echo "Kernel report sums: sum1=$sum1, sum2=$sum2"
+
+ # Compare sums with tolerance
+ if awk -v s1="$sum1" -v s2="$sum2" \
+ 'BEGIN {diff = s1 - s2; if (diff < 0) diff = -diff; if (diff < 0.05) exit 0; else exit 1}'; then
+ echo "Kernel Report ASLR test [Success]"
+ else
+ echo "Kernel Report ASLR test [Failed - sums differ too much]"
+ err=1
+ fi
+
+ rm -f "${kdata}" "${kdata2}" "${data_clean}" "${log_file}" \
+ "${kdata}.old" "${kdata2}.old" "${data_clean}.old"
+}
+
+test_basic_aslr
+test_pipe_aslr
+test_callchain_aslr
+test_report_aslr
+test_pipe_report_aslr
+test_dropped_samples
+test_kernel_aslr
+test_kernel_report_aslr
+
+cleanup
+exit $err
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes
2026-04-25 2:05 ` [PATCH v2 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-04-25 2:05 ` [PATCH v2 2/2] perf test: Add inject ASLR test Ian Rogers
@ 2026-05-04 3:51 ` Ian Rogers
2026-05-04 3:51 ` [PATCH v3 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
` (4 more replies)
1 sibling, 5 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 3:51 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
This patch series introduces the new 'perf inject --aslr' feature to remap
virtual memory addresses or drop physical memory event leaks when profile
record data is shared between machines. Bundled with this feature are two
independent, critical bug fixes inside core event dispatching tools that
harden perf session analysis against dynamic crashes and callchain mapping
failures.
--- Core Feature: 'perf inject --aslr' (Patches 3 and 4)
Transferring perf.data files across environments introduces a potential leak
of virtual address footprints, weakening Address Space Layout Randomization
(ASLR) on the originating machine. To mitigate this, we introduce the --aslr
flag into perf inject. Unknown or unhandled events are dropped conservatively,
while handled samples and branch loops undergo systematic virtual memory offset
obfuscation.
To ensure comprehensive memory and error-path safety, the ASLR tool implements:
- Machine namespaces ('struct machines') to safely interleave host mappings and
unprivileged guest (KVM) memory regions without boundary leakages.
- Multi-map anchor key matching ( anchored by DSO, invariant offsets, and PID)
resolving overlapping split-map lookups.
- Subtraction-based bounds check equations to mathematically secure branch stack
loops against integer overflows.
- Secure u64 dynamic buffer calculations on userspace stack and hardware tracing
payloads to prevent wrap-around heap overflows.
- Clean, error-checked skip advancement loops (skipn) past dynamic AUX streams in
piped records to maintain stream reader sync.
- Robust OOM fallback rollbacks of transient dictionary keys to guarantee
dictionary hashmap integrity on failures.
Verification is reinforced in Patch 4 with a new comprehensive POSIX shell
suite ('inject_aslr.sh'), hardened against SIGPIPE signal exits with stream
consuming awk loops and robust 'set -o pipefail' assertions.
--- Prerequisite Bug Fixes (Patches 1 and 2)
During development, two core event delegation issues were identified and
resolved to prevent crashes and data-loss during analysis:
1. perf sched: 'timehist' registers standard MMAP, COMM, EXIT, and FORK stubs,
but completely omitted registering MMAP2 callbacks. Because modern environments
output maps primarily via MMAP2 frames, this caused timehist sessions to silently
drop shared library mappings, causing dynamic callchain symbol resolutions to
fail. Patch 1 corrects this by properly registering perf_event__process_mmap2.
2. perf tool: Patch 2 fixes missing copies of schedstat callbacks inside delegated
wrapper tools (which caused segfaults on NULL stubs) and properly initializes/copies
the 'dont_split_sample_group' grouping parameters to prevent stack garbage from
triggering silent non-leader events drops during split deliver streams.
Ian Rogers (4):
perf sched: Add missing mmap2 handler in timehist
perf tool: Fix missing schedstat delegates and dont_split_sample_group
in delegate_tool
perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
perf test: Add inject ASLR test
tools/perf/builtin-inject.c | 18 +-
tools/perf/builtin-sched.c | 1 +
tools/perf/tests/shell/inject_aslr.sh | 423 +++++++++
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1157 +++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
tools/perf/util/tool.c | 6 +
7 files changed, 1615 insertions(+), 1 deletion(-)
create mode 100755 tools/perf/tests/shell/inject_aslr.sh
create mode 100644 tools/perf/util/aslr.c
create mode 100644 tools/perf/util/aslr.h
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply [flat|nested] 15+ messages in thread* [PATCH v3 1/4] perf sched: Add missing mmap2 handler in timehist
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
@ 2026-05-04 3:51 ` Ian Rogers
2026-05-04 3:51 ` [PATCH v3 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
` (3 subsequent siblings)
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 3:51 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
perf_sched__timehist() registers event handlers for options using the
sched->tool struct. It registers handlers for MMAP, COMM, EXIT, FORK, etc.
but completely omits registering a handler for MMAP2 events.
Failing to register both MMAP and MMAP2 handlers causes modern systems
(which primarily output MMAP2 records) to silently drop VMA map mappings.
This results in uninitialized machine/thread mapping structures, making it
impossible to resolve shared library instruction pointers (IPs) to dynamic
symbols/DSOs during timehist callchain analysis.
Fix this by correctly registering perf_event__process_mmap2 in
sched->tool inside perf_sched__timehist().
Assisted-by: Gemini-CLI:Google Gemini 3
Fixes: 5bbfec0ad93c ("perf sched: Implement timehist option")
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/builtin-sched.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/tools/perf/builtin-sched.c b/tools/perf/builtin-sched.c
index 555247568e7a..241c2f808f7b 100644
--- a/tools/perf/builtin-sched.c
+++ b/tools/perf/builtin-sched.c
@@ -3299,6 +3299,7 @@ static int perf_sched__timehist(struct perf_sched *sched)
*/
sched->tool.sample = perf_timehist__process_sample;
sched->tool.mmap = perf_event__process_mmap;
+ sched->tool.mmap2 = perf_event__process_mmap2;
sched->tool.comm = perf_event__process_comm;
sched->tool.exit = perf_event__process_exit;
sched->tool.fork = perf_event__process_fork;
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v3 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-04 3:51 ` [PATCH v3 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
@ 2026-05-04 3:51 ` Ian Rogers
2026-05-04 3:51 ` [PATCH v3 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
` (2 subsequent siblings)
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 3:51 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
delegate_tool was missing the delegate overrides for schedstat_cpu
and schedstat_domain. As a result, when allocated with zalloc, these
callbacks defaulted to NULL, causing a segmentation fault crash if
any schedstat events were delivered during event processing.
Fix this by adding delegate_schedstat_cpu and delegate_schedstat_domain
via the CREATE_DELEGATE_OP2 macro, and ensuring delegate_tool__init
correctly registers them.
Additionally, delegate_tool__init completely omitted copying the
dont_split_sample_group property from the delegate. This would cause
wrapper tools to default the flag to false, which corrupts piped event
processing (e.g., in perf inject) by triggering duplicate event
deliveries on split sample values in deliver_sample_group().
Similarly, perf_tool__init() omitted the initialization of this
boolean field. On stack-allocated tools that rely on this initializer
(like intel-tpebs or __cmd_evlist), this could result in uninitialized
stack garbage evaluating to true—silently dropping non-leader event
members in deliver_sample_group().
Fix both issues by properly copying the field in delegate_tool__init
and initializing it to false in perf_tool__init.
Assisted-by: Gemini-CLI:Google Gemini 3
Fixes: 6331b2669359 ("perf tool: Add a delegate_tool that just delegates actions to another tool")
Fixes: 79bcd34e0f3d ("perf inject: Fix leader sampling inserting additional samples")
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/tool.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/tools/perf/util/tool.c b/tools/perf/util/tool.c
index 013c7839e2cf..ff2150517b75 100644
--- a/tools/perf/util/tool.c
+++ b/tools/perf/util/tool.c
@@ -285,6 +285,7 @@ void perf_tool__init(struct perf_tool *tool, bool ordered_events)
tool->no_warn = false;
tool->show_feat_hdr = SHOW_FEAT_NO_HEADER;
tool->merge_deferred_callchains = true;
+ tool->dont_split_sample_group = false;
tool->sample = process_event_sample_stub;
tool->mmap = process_event_stub;
@@ -433,6 +434,8 @@ CREATE_DELEGATE_OP2(stat_config);
CREATE_DELEGATE_OP2(stat_round);
CREATE_DELEGATE_OP2(thread_map);
CREATE_DELEGATE_OP2(time_conv);
+CREATE_DELEGATE_OP2(schedstat_cpu);
+CREATE_DELEGATE_OP2(schedstat_domain);
CREATE_DELEGATE_OP2(tracing_data);
#define CREATE_DELEGATE_OP3(name) \
@@ -470,6 +473,7 @@ void delegate_tool__init(struct delegate_tool *tool, struct perf_tool *delegate)
tool->tool.no_warn = delegate->no_warn;
tool->tool.show_feat_hdr = delegate->show_feat_hdr;
tool->tool.merge_deferred_callchains = delegate->merge_deferred_callchains;
+ tool->tool.dont_split_sample_group = delegate->dont_split_sample_group;
tool->tool.sample = delegate_sample;
tool->tool.read = delegate_read;
@@ -516,4 +520,6 @@ void delegate_tool__init(struct delegate_tool *tool, struct perf_tool *delegate)
tool->tool.bpf_metadata = delegate_bpf_metadata;
tool->tool.compressed = delegate_compressed;
tool->tool.auxtrace = delegate_auxtrace;
+ tool->tool.schedstat_cpu = delegate_schedstat_cpu;
+ tool->tool.schedstat_domain = delegate_schedstat_domain;
}
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v3 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-04 3:51 ` [PATCH v3 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-04 3:51 ` [PATCH v3 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
@ 2026-05-04 3:51 ` Ian Rogers
2026-05-04 3:51 ` [PATCH v3 4/4] perf test: Add inject ASLR test Ian Rogers
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 3:51 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
If perf.data files are taken from one machine to another they make
leak virtual addresses and so weaken ASLR on the machine they are
coming from. Add a '--aslr' option for perf inject that remaps all
virtual addresses, or drops data/events, so that the virtual address
information isn't leaked.
When events are not known/handled by the tool they are dropped. This
makes the tool conservative and it should never leak ASLR information,
but it means virtual address remapping is needed for cases like
auxtrace.
To ensure comprehensive robustness and security, this tool:
- Employs guest namespace isolation by utilizing 'struct machines' to safely
interleave host and unprivileged KVM guest virtual address mappings.
- Resolves VMA split map failures (caused by maps__fixup_overlap_and_insert)
consistently by anchoring mappings on DSO and memory invariants.
- Guards against integer overflows in branch stack loops via subtraction-based
bounds arithmetic.
- Prevents heap buffer overflows by computing safe word limits on userspace
stacks and dynamic hardware tracing (AUX) sizes.
- Prevents key collisions/ABA lookups by correctly managing DSO reference counts
(dso__get/put).
- Cleans up error paths to avoid inconsistent hashmap mappings on OOM failures.
- Optimizes performance by removing redundant hot-path memory allocations.
- Cleanly advances session readers past dropped auxtrace streams.
Assisted-by: Gemini-CLI:Google Gemini 3
Signed-off-by: Ian Rogers <irogers@google.com>
Co-developed-by: Gabriel Marin <gmx@google.com>
Signed-off-by: Gabriel Marin <gmx@google.com>
---
v3: Combine split-map fixes, guest namespaces, bounds checks, OOM rollbacks,
hot path optimization, safe dso references, and I/O stream error handling
from v3/v4 development. Drop raw auxtrace events. Fix thread reference leaks
in event handlers. Fix 32-bit truncation bug in hashmaps using u64* values.
Prevent leaking uninitialized heap memory by zeroing copy buffer. Correct
bitmask checks for branch stack flags. Avoid PMU configuration corruption.
v2: First review feedback adjustments.
---
tools/perf/builtin-inject.c | 18 +-
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1157 +++++++++++++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
4 files changed, 1185 insertions(+), 1 deletion(-)
create mode 100644 tools/perf/util/aslr.c
create mode 100644 tools/perf/util/aslr.h
diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c
index f174bc69cec4..6e6bf6b67956 100644
--- a/tools/perf/builtin-inject.c
+++ b/tools/perf/builtin-inject.c
@@ -8,6 +8,7 @@
*/
#include "builtin.h"
+#include "util/aslr.h"
#include "util/color.h"
#include "util/dso.h"
#include "util/vdso.h"
@@ -123,6 +124,7 @@ struct perf_inject {
bool in_place_update_dry_run;
bool copy_kcore_dir;
bool convert_callchain;
+ bool aslr;
const char *input_name;
struct perf_data output;
u64 bytes_written;
@@ -2564,6 +2566,8 @@ int cmd_inject(int argc, const char **argv)
" instance has a subdir"),
OPT_BOOLEAN(0, "convert-callchain", &inject.convert_callchain,
"Generate callchains using DWARF and drop register/stack data"),
+ OPT_BOOLEAN(0, "aslr", &inject.aslr,
+ "Remap virtual memory addresses similar to ASLR"),
OPT_END()
};
const char * const inject_usage[] = {
@@ -2571,6 +2575,7 @@ int cmd_inject(int argc, const char **argv)
NULL
};
bool ordered_events;
+ struct perf_tool *tool = &inject.tool;
if (!inject.itrace_synth_opts.set) {
/* Disable eager loading of kernel symbols that adds overhead to perf inject. */
@@ -2684,12 +2689,21 @@ int cmd_inject(int argc, const char **argv)
inject.tool.schedstat_domain = perf_event__repipe_op2_synth;
inject.tool.dont_split_sample_group = true;
inject.tool.merge_deferred_callchains = false;
- inject.session = __perf_session__new(&data, &inject.tool,
+ if (inject.aslr) {
+ tool = aslr_tool__new(&inject.tool);
+ if (!tool) {
+ ret = -ENOMEM;
+ goto out_close_output;
+ }
+ }
+ inject.session = __perf_session__new(&data, tool,
/*trace_event_repipe=*/inject.output.is_pipe,
/*host_env=*/NULL);
if (IS_ERR(inject.session)) {
ret = PTR_ERR(inject.session);
+ if (inject.aslr)
+ aslr_tool__delete(tool);
goto out_close_output;
}
@@ -2789,6 +2803,8 @@ int cmd_inject(int argc, const char **argv)
strlist__delete(inject.known_build_ids);
zstd_fini(&(inject.session->zstd_data));
perf_session__delete(inject.session);
+ if (inject.aslr)
+ aslr_tool__delete(tool);
out_close_output:
if (!inject.in_place_update)
perf_data__close(&inject.output);
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 70cc91d00804..65b96f3b87e2 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -6,6 +6,7 @@ perf-util-y += arm64-frame-pointer-unwind-support.o
perf-util-y += addr2line.o
perf-util-y += addr_location.o
perf-util-y += annotate.o
+perf-util-y += aslr.o
perf-util-y += blake2s.o
perf-util-y += block-info.o
perf-util-y += block-range.o
diff --git a/tools/perf/util/aslr.c b/tools/perf/util/aslr.c
new file mode 100644
index 000000000000..32548352e1e5
--- /dev/null
+++ b/tools/perf/util/aslr.c
@@ -0,0 +1,1157 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "aslr.h"
+
+#include "addr_location.h"
+#include "debug.h"
+#include "event.h"
+#include "evsel.h"
+#include "machine.h"
+#include "map.h"
+#include "thread.h"
+#include "tool.h"
+#include "session.h"
+#include "data.h"
+#include "dso.h"
+
+#include <internal/lib.h> /* page_size */
+#include <linux/compiler.h>
+#include <linux/zalloc.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <unistd.h>
+
+static int skipn(int fd, u64 n)
+{
+ char buf[4096];
+ ssize_t ret;
+
+ while (n > 0) {
+ ret = read(fd, buf, (n < (u64)sizeof(buf) ? n : (u64)sizeof(buf)));
+ if (ret <= 0)
+ return ret;
+ n -= ret;
+ }
+
+ return 0;
+}
+
+/**
+ * struct remap_addresses_key - Key for mapping original addresses to remapped ones.
+ * @dso: Pointer to the DSO (Dynamic Shared Object) associated with the mapping.
+ * @invariant: Unique offset invariant within the VMA (Virtual Memory Area).
+ * Calculated as `start - pgoff`. This value remains constant when
+ * perf's internal `maps__fixup_overlap_and_insert` splits a map into
+ * fragmented VMA pieces due to overlapping events, allowing us to
+ * resolve split maps consistently back to the original VMA.
+ * @pid: Process ID associated with the mapping.
+ */
+struct remap_addresses_key {
+ struct dso *dso;
+ u64 invariant;
+ pid_t pid;
+};
+
+struct aslr_mapping {
+ struct list_head node;
+ u64 orig_start;
+ u64 len;
+ u64 remap_start;
+};
+
+struct aslr_tool {
+ /** @tool: The tool implemented here and a pointer to a delegate to process the data. */
+ struct delegate_tool tool;
+ /** @machines: The machines with the input, not remapped, virtual address layout. */
+ struct machines machines;
+ /** @event_copy: Buffer used to create an event to pass to the delegate. */
+ char event_copy[PERF_SAMPLE_MAX_SIZE];
+ /** @remap_addresses: mapping from remap_addresses_key to remapped address. */
+ struct hashmap remap_addresses;
+ /** @top_addresses: mapping from process to max remapped address. */
+ struct hashmap top_addresses;
+ /** @first_kernel_mapping: flag indicating if we are still to process any kernel mapping. */
+ bool first_kernel_mapping;
+};
+
+static const pid_t kernel_pid = -1;
+
+/* Start remapping user processes from a small non-zero offset. */
+static const u64 user_space_start = 0x200000;
+static const u64 kernel_space_start = 0xffff800010000000;
+
+static size_t remap_addresses__hash(long _key, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key = (struct remap_addresses_key *)_key;
+
+ return (size_t)key->dso ^ key->invariant ^ key->pid;
+}
+
+static bool remap_addresses__equal(long _key1, long _key2, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key1 = (struct remap_addresses_key *)_key1;
+ struct remap_addresses_key *key2 = (struct remap_addresses_key *)_key2;
+
+ return key1->dso == key2->dso &&
+ key1->invariant == key2->invariant &&
+ key1->pid == key2->pid;
+}
+
+static size_t top_addresses__hash(long key, void *ctx __maybe_unused)
+{
+ return key;
+}
+
+static bool top_addresses__equal(long key1, long key2, void *ctx __maybe_unused)
+{
+ return key1 == key2;
+}
+
+static u64 round_up_to_page_size(u64 addr)
+{
+ return (addr + page_size - 1) & ~((u64)page_size - 1);
+}
+
+static u64 aslr_tool__remap_address(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 addr)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 remap_addr = 0;
+ u8 effective_cpumode = cpumode;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&al);
+ if (!thread__find_map(aslr_thread, cpumode, addr, &al)) {
+ /*
+ * If lookup fails with specified cpumode, try fallback to the other space
+ * to be robust against bad cpumode in samples.
+ */
+ effective_cpumode = (cpumode == PERF_RECORD_MISC_KERNEL) ?
+ PERF_RECORD_MISC_USER : PERF_RECORD_MISC_KERNEL;
+ if (!thread__find_map(aslr_thread, effective_cpumode, addr, &al)) {
+ addr_location__exit(&al);
+ return 0; /* No mmap. */
+ }
+ }
+
+ key.dso = map__dso(al.map);
+ key.invariant = map__start(al.map) - map__pgoff(al.map);
+ key.pid = effective_cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+
+ if (hashmap__find(&aslr->remap_addresses, &key, &remapped_invariant_ptr)) {
+ remap_addr = *remapped_invariant_ptr + map__pgoff(al.map) +
+ (addr - map__start(al.map));
+ } else {
+ pr_debug("Cannot find a remapped entry for address %lx in mapping %lx(%lx) for pid=%d\n",
+ addr, map__start(al.map), map__size(al.map), key.pid);
+ }
+
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_mapping(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 start, u64 len, u64 pgoff)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 *max_addr_ptr = NULL;
+ bool is_contiguous = false;
+ bool first_mapping = false;
+ bool key_found = false;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&al);
+ if (thread__find_map(aslr_thread, cpumode, start, &al))
+ key.dso = map__dso(al.map);
+ else
+ key.dso = NULL;
+
+ key.invariant = start - pgoff;
+ key.pid = cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+
+ if (hashmap__find(&aslr->remap_addresses, &key, &remapped_invariant_ptr)) {
+ remap_addr = *remapped_invariant_ptr + pgoff;
+ key_found = true;
+ } else {
+ struct addr_location prev_al;
+
+ addr_location__init(&prev_al);
+ if (thread__find_map(aslr_thread, cpumode, start - 1, &prev_al)) {
+ if (map__start(prev_al.map) + map__size(prev_al.map) == start) {
+ is_contiguous = true;
+ } else {
+ pr_debug("Previous mmap [%lx, %lx] overlaps current map [%lx, %lx]\n",
+ map__start(prev_al.map),
+ map__start(prev_al.map) + map__size(prev_al.map),
+ start, start+len);
+ }
+ }
+ addr_location__exit(&prev_al);
+
+ if (!hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ first_mapping = true;
+ remap_addr = (cpumode == PERF_RECORD_MISC_KERNEL ?
+ kernel_space_start : user_space_start);
+ } else {
+ remap_addr = *max_addr_ptr;
+ }
+
+ remap_addr = round_up_to_page_size(remap_addr);
+ if (!is_contiguous && !first_mapping)
+ remap_addr += page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+ u64 *new_val = malloc(sizeof(u64));
+
+ if (!new_key || !new_val) {
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_key = key;
+ new_key->dso = dso__get(key.dso);
+ *new_val = remap_addr - pgoff;
+
+ if (hashmap__add(&aslr->remap_addresses, new_key, new_val) != 0) {
+ dso__put(new_key->dso);
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ }
+ }
+
+ /* Update top_addresses */
+ {
+ u64 *new_max = malloc(sizeof(u64));
+ u64 *old_val = NULL;
+ int err;
+
+ if (!new_max) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ if (!key_found) {
+ hashmap__delete(&aslr->remap_addresses, &key,
+ &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ }
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_max = remap_addr + len;
+
+ if (hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ if (*max_addr_ptr > *new_max)
+ *new_max = *max_addr_ptr;
+ }
+
+ err = hashmap__insert(&aslr->top_addresses, key.pid, new_max,
+ (first_mapping && !key_found) ?
+ HASHMAP_ADD : HASHMAP_UPDATE,
+ NULL, &old_val);
+ if (err) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ free(new_max);
+ if (!key_found) {
+ hashmap__delete(&aslr->remap_addresses, &key,
+ &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ }
+ addr_location__exit(&al);
+ return 0;
+ }
+ free(old_val);
+ }
+
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_ksymbol(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u64 addr, u32 len)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 *max_addr_ptr = NULL;
+ bool first_mapping = false;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&al);
+ if (thread__find_map(aslr_thread, PERF_RECORD_MISC_KERNEL, addr, &al))
+ key.dso = map__dso(al.map);
+ else
+ key.dso = NULL;
+
+ key.invariant = addr; /* pgoff is 0 for ksymbols */
+ key.pid = aslr_thread->pid_;
+
+ if (hashmap__find(&aslr->remap_addresses, &key, &remapped_invariant_ptr)) {
+ remap_addr = *remapped_invariant_ptr;
+ addr_location__exit(&al);
+ return remap_addr;
+ }
+
+ if (!hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ first_mapping = true;
+ remap_addr = kernel_space_start;
+ } else {
+ remap_addr = *max_addr_ptr;
+ }
+
+ remap_addr = round_up_to_page_size(remap_addr) + page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+ u64 *new_val = malloc(sizeof(u64));
+
+ if (!new_key || !new_val) {
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_key = key;
+ new_key->dso = dso__get(key.dso);
+ *new_val = remap_addr;
+
+ if (hashmap__add(&aslr->remap_addresses, new_key, new_val) < 0) {
+ dso__put(new_key->dso);
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ }
+
+ {
+ u64 *new_max = malloc(sizeof(u64));
+ u64 *old_val = NULL;
+ int err;
+
+ if (!new_max) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ hashmap__delete(&aslr->remap_addresses, &key, &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_max = remap_addr + len;
+
+ if (hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ if (*max_addr_ptr > *new_max)
+ *new_max = *max_addr_ptr;
+ }
+
+ err = hashmap__insert(&aslr->top_addresses, key.pid, new_max,
+ first_mapping ?
+ HASHMAP_ADD : HASHMAP_UPDATE,
+ NULL, &old_val);
+ if (err) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ free(new_max);
+ hashmap__delete(&aslr->remap_addresses, &key, &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ addr_location__exit(&al);
+ return 0;
+ }
+ free(old_val);
+ }
+
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+
+static int aslr_tool__process_mmap(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ u8 cpumode;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(aslr_machine, event->mmap.pid, event->mmap.tid);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->mmap, &event->mmap, event->mmap.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap.start,
+ event->mmap.len,
+ event->mmap.pgoff);
+ if (aslr->first_kernel_mapping && cpumode == PERF_RECORD_MISC_KERNEL) {
+ /*
+ * If this is the first kernel image, we need to adjust the pgoff by a
+ * similar delta.
+ */
+ new_event->mmap.pgoff = event->mmap.pgoff - event->mmap.start +
+ new_event->mmap.start;
+ aslr->first_kernel_mapping = false;
+ }
+ err = delegate->mmap(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_mmap2(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ u8 cpumode;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap2(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(aslr_machine, event->mmap2.pid, event->mmap2.tid);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->mmap2, &event->mmap2, event->mmap2.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap2.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap2.start,
+ event->mmap2.len,
+ event->mmap2.pgoff);
+ if (aslr->first_kernel_mapping && cpumode == PERF_RECORD_MISC_KERNEL) {
+ /*
+ * If this is the first kernel image, we need to adjust the pgoff by a
+ * similar delta.
+ */
+ new_event->mmap2.pgoff = event->mmap2.pgoff - event->mmap2.start +
+ new_event->mmap2.start;
+ aslr->first_kernel_mapping = false;
+ }
+ err = delegate->mmap2(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_comm(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_comm(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ return delegate->comm(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_fork(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_fork(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ return delegate->fork(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_exit(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_exit(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ return delegate->exit(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_text_poke(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ u8 cpumode;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ thread = machine__findnew_thread(aslr_machine, sample->pid, sample->tid);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->text_poke, &event->text_poke, event->text_poke.header.size);
+ new_event->text_poke.addr = aslr_tool__remap_address(aslr, thread, cpumode,
+ event->text_poke.addr);
+
+ err = delegate->text_poke(delegate, new_event, sample, machine);
+
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_ksymbol(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_ksymbol(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(aslr_machine, kernel_pid, 0);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->ksymbol, &event->ksymbol, event->ksymbol.header.size);
+ /* Remaps the ksymbol.start */
+ new_event->ksymbol.addr = aslr_tool__remap_ksymbol(aslr, thread,
+ event->ksymbol.addr, event->ksymbol.len);
+
+ err = delegate->ksymbol(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_sample(const struct perf_tool *tool, union perf_event *event,
+ struct perf_sample *sample,
+ struct evsel *evsel, struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ int ret;
+ u64 sample_type;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ __u64 max_i;
+ __u64 max_j;
+ union perf_event *new_event;
+ struct perf_sample new_sample;
+ __u64 *in_array, *out_array;
+ u8 cpumode;
+ u64 addr;
+ size_t i;
+ size_t j;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ ret = -EFAULT;
+ sample_type = evsel->core.attr.sample_type;
+ max_i = (event->header.size - sizeof(struct perf_event_header)) / sizeof(__u64);
+ max_j = (PERF_SAMPLE_MAX_SIZE - sizeof(struct perf_event_header)) / sizeof(__u64);
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = sample->cpumode;
+ i = 0;
+ j = 0;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ thread = machine__findnew_thread(aslr_machine, sample->pid, sample->tid);
+
+ if (!thread)
+ return -ENOMEM;
+
+ if (max_i > PERF_SAMPLE_MAX_SIZE / sizeof(u64))
+ goto out_put;
+
+
+
+ new_event->sample.header = event->sample.header;
+
+ in_array = &event->sample.array[0];
+ out_array = &new_event->sample.array[0];
+
+#define CHECK_BOUNDS(required_i, required_j) \
+ do { \
+ if (i + (required_i) > max_i || j + (required_j) > max_j) { \
+ ret = -EFAULT; \
+ goto out_put; \
+ } \
+ } while (0)
+
+#define COPY_U64() \
+ do { \
+ CHECK_BOUNDS(1, 1); \
+ out_array[j++] = in_array[i++]; \
+ } while (0)
+
+#define REMAP_U64(addr_field) \
+ do { \
+ CHECK_BOUNDS(1, 1); \
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode, addr_field); \
+ i++; \
+ } while (0)
+
+ if (sample_type & PERF_SAMPLE_IDENTIFIER)
+ COPY_U64(); /* id */
+ if (sample_type & PERF_SAMPLE_IP)
+ REMAP_U64(sample->ip);
+ if (sample_type & PERF_SAMPLE_TID)
+ COPY_U64(); /* pid, tid */
+ if (sample_type & PERF_SAMPLE_TIME)
+ COPY_U64(); /* time */
+ if (sample_type & PERF_SAMPLE_ADDR)
+ REMAP_U64(sample->addr);
+ if (sample_type & PERF_SAMPLE_ID)
+ COPY_U64(); /* id */
+ if (sample_type & PERF_SAMPLE_STREAM_ID)
+ COPY_U64(); /* stream_id */
+ if (sample_type & PERF_SAMPLE_CPU)
+ COPY_U64(); /* cpu, res */
+ if (sample_type & PERF_SAMPLE_PERIOD)
+ COPY_U64(); /* period */
+ if (sample_type & PERF_SAMPLE_READ) {
+ if ((evsel->core.attr.read_format & PERF_FORMAT_GROUP) == 0) {
+ COPY_U64(); /* value */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ COPY_U64(); /* time_enabled */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ COPY_U64(); /* time_running */
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ COPY_U64(); /* id */
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ COPY_U64(); /* lost */
+ } else {
+ u64 nr;
+
+ CHECK_BOUNDS(1, 1);
+ nr = out_array[j++] = in_array[i++];
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ COPY_U64(); /* time_enabled */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ COPY_U64(); /* time_running */
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ COPY_U64(); /* value */
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ COPY_U64(); /* id */
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ COPY_U64(); /* lost */
+ }
+ }
+ }
+ if (sample_type & PERF_SAMPLE_CALLCHAIN) {
+ u64 nr;
+
+ CHECK_BOUNDS(1, 1);
+ nr = out_array[j++] = in_array[i++];
+
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ CHECK_BOUNDS(1, 1);
+ addr = in_array[i++];
+ if (addr >= PERF_CONTEXT_MAX) {
+ out_array[j++] = addr;
+ switch (addr) {
+ case PERF_CONTEXT_HV:
+ cpumode = PERF_RECORD_MISC_HYPERVISOR;
+ break;
+ case PERF_CONTEXT_KERNEL:
+ cpumode = PERF_RECORD_MISC_KERNEL;
+ break;
+ case PERF_CONTEXT_USER:
+ cpumode = PERF_RECORD_MISC_USER;
+ break;
+ case PERF_CONTEXT_GUEST:
+ cpumode = PERF_RECORD_MISC_GUEST_KERNEL;
+ break;
+ case PERF_CONTEXT_GUEST_KERNEL:
+ cpumode = PERF_RECORD_MISC_GUEST_KERNEL;
+ break;
+ case PERF_CONTEXT_GUEST_USER:
+ cpumode = PERF_RECORD_MISC_GUEST_USER;
+ break;
+ case PERF_CONTEXT_USER_DEFERRED:
+ /*
+ * Immediately followed by a 64-bit
+ * stitching cookie. Skip/Copy it!
+ */
+ CHECK_BOUNDS(1, 1);
+ out_array[j++] = in_array[i++];
+ cntr++;
+ break;
+ default:
+ pr_debug("invalid callchain context: %"PRIx64"\n", addr);
+ ret = 0;
+ goto out_put;
+ }
+ continue;
+ }
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode, addr);
+ }
+ }
+ if (sample_type & PERF_SAMPLE_RAW) {
+ size_t bytes = sizeof(u32) + sample->raw_size;
+ size_t u64_words = (bytes + 7) / 8;
+
+ if (i + u64_words > max_i || j + u64_words > max_j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], bytes);
+ i += u64_words;
+ j += u64_words;
+ /*
+ * TODO: certain raw samples can be remapped, such as
+ * tracepoints by examining their fields.
+ */
+ pr_debug("Dropping raw samples as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_BRANCH_STACK) {
+ u64 nr;
+
+ CHECK_BOUNDS(1, 1);
+ nr = out_array[j++] = in_array[i++];
+
+ if (evsel->core.attr.branch_sample_type & PERF_SAMPLE_BRANCH_HW_INDEX)
+ COPY_U64(); /* hw_idx */
+
+ if (nr > (ULLONG_MAX / 3)) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ if (nr * 3 > max_i - i || nr * 3 > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ out_array[j++] = aslr_tool__remap_address(aslr, thread,
+ sample->cpumode,
+ in_array[i++]); /* from */
+ out_array[j++] = aslr_tool__remap_address(aslr, thread,
+ sample->cpumode,
+ in_array[i++]); /* to */
+ out_array[j++] = in_array[i++]; /* flags */
+ }
+ if (evsel->core.attr.branch_sample_type & PERF_SAMPLE_BRANCH_COUNTERS) {
+ if (nr > max_i - i || nr > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ /* TODO: confirm branch counters don't leak ASLR information. */
+ pr_debug("Dropping sample branch counters as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ }
+ if (sample_type & PERF_SAMPLE_REGS_USER) {
+ u64 abi;
+
+ COPY_U64(); /* abi */
+ abi = out_array[j-1];
+ if (abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_user);
+
+ if (nr > max_i - i || nr > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping regs user sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_STACK_USER) {
+ u64 size;
+
+ CHECK_BOUNDS(1, 1);
+ size = out_array[j++] = in_array[i++];
+ if (size > 0) {
+ size_t u64_words = size / 8 + (size % 8 ? 1 : 0);
+
+ if (u64_words > max_i - i || u64_words > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], size);
+ i += u64_words;
+ j += u64_words;
+
+ COPY_U64(); /* dyn_size */
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping stack user sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_WEIGHT_TYPE)
+ COPY_U64(); /* perf_sample_weight */
+ if (sample_type & PERF_SAMPLE_DATA_SRC)
+ COPY_U64(); /* data_src */
+ if (sample_type & PERF_SAMPLE_TRANSACTION)
+ COPY_U64(); /* transaction */
+ if (sample_type & PERF_SAMPLE_REGS_INTR) {
+ u64 abi;
+
+ COPY_U64(); /* abi */
+ abi = out_array[j-1];
+ if (abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_intr);
+
+ if (nr > max_i - i || nr > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping interrupt register sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_PHYS_ADDR) {
+ COPY_U64(); /* phys_addr */
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping physical address sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_CGROUP)
+ COPY_U64(); /* cgroup */
+ if (sample_type & PERF_SAMPLE_DATA_PAGE_SIZE)
+ COPY_U64(); /* data_page_size */
+ if (sample_type & PERF_SAMPLE_CODE_PAGE_SIZE)
+ COPY_U64(); /* code_page_size */
+
+ if (sample_type & PERF_SAMPLE_AUX) {
+ u64 size;
+
+ CHECK_BOUNDS(1, 1);
+ size = out_array[j++] = in_array[i++];
+ if (size > 0) {
+ size_t u64_words = size / 8 + (size % 8 ? 1 : 0);
+
+ if (u64_words > max_i - i || u64_words > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], size);
+ i += u64_words;
+ j += u64_words;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping aux sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+
+ if (evsel__is_offcpu_event(evsel)) {
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping off-CPU sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+
+ new_event->sample.header.size = sizeof(struct perf_event_header) + j * sizeof(u64);
+
+ perf_sample__init(&new_sample, /*all=*/ true);
+ ret = evsel__parse_sample(evsel, new_event, &new_sample);
+ if (ret) {
+ perf_sample__exit(&new_sample);
+ goto out_put;
+ }
+
+ ret = delegate->sample(delegate, new_event, &new_sample, evsel, machine);
+ perf_sample__exit(&new_sample);
+
+out_put:
+ thread__put(thread);
+ return ret;
+}
+
+#undef CHECK_BOUNDS
+#undef COPY_U64
+#undef REMAP_U64
+
+
+static int aslr_tool__process_attr(const struct perf_tool *tool,
+ union perf_event *event,
+ struct evlist **pevlist)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+
+ memcpy(&new_event->attr, &event->attr, event->attr.header.size);
+ if (new_event->attr.attr.type == PERF_TYPE_BREAKPOINT)
+ new_event->attr.attr.bp_addr = 0; /* Conservatively remove addresses. */
+
+ return delegate->attr(delegate, new_event, pevlist);
+}
+
+static s64 aslr_tool__process_auxtrace(const struct perf_tool *tool __maybe_unused,
+ struct perf_session *session,
+ union perf_event *event)
+{
+ if (perf_data__is_pipe(session->data)) {
+ int err = skipn(perf_data__fd(session->data), event->auxtrace.size);
+
+ if (err < 0)
+ return err;
+ }
+ return event->auxtrace.size;
+}
+
+static int aslr_tool__process_auxtrace_info(const struct perf_tool *tool __maybe_unused,
+ struct perf_session *session __maybe_unused,
+ union perf_event *event __maybe_unused)
+{
+ return 0;
+}
+
+static int aslr_tool__process_auxtrace_error(const struct perf_tool *tool __maybe_unused,
+ struct perf_session *session __maybe_unused,
+ union perf_event *event __maybe_unused)
+{
+ return 0;
+}
+
+static void aslr_tool__init(struct aslr_tool *aslr, struct perf_tool *delegate)
+{
+ delegate_tool__init(&aslr->tool, delegate);
+ aslr->tool.tool.ordered_events = true;
+
+ machines__init(&aslr->machines);
+
+ hashmap__init(&aslr->remap_addresses,
+ remap_addresses__hash, remap_addresses__equal,
+ /*ctx=*/NULL);
+ hashmap__init(&aslr->top_addresses,
+ top_addresses__hash, top_addresses__equal,
+ /*ctx=*/NULL);
+ aslr->first_kernel_mapping = true;
+
+ aslr->tool.tool.sample = aslr_tool__process_sample;
+ /* read - reads a counter, okay to delegate. */
+ aslr->tool.tool.mmap = aslr_tool__process_mmap;
+ aslr->tool.tool.mmap2 = aslr_tool__process_mmap2;
+ aslr->tool.tool.comm = aslr_tool__process_comm;
+ aslr->tool.tool.fork = aslr_tool__process_fork;
+ aslr->tool.tool.exit = aslr_tool__process_exit;
+ /* namesspaces, cgroup, lost, lost_sample, aux, */
+ /* itrace_start, aux_output_hw_id, context_switch, throttle, unthrottle */
+ /* - no virtual addresses. */
+ aslr->tool.tool.ksymbol = aslr_tool__process_ksymbol;
+ /* bpf - no virtual address. */
+ aslr->tool.tool.text_poke = aslr_tool__process_text_poke;
+ aslr->tool.tool.attr = aslr_tool__process_attr;
+ /* event_update, tracing_data, finished_round, build_id, id_index, */
+ /* event_update, tracing_data, finished_round, build_id, id_index, */
+ /* auxtrace_info, auxtrace_error, time_conv, thread_map, cpu_map, */
+ /* stat_config, stat, feature, finished_init, bpf_metadata, compressed, */
+ /* auxtrace - no virtual addresses. */
+ aslr->tool.tool.auxtrace = aslr_tool__process_auxtrace;
+ aslr->tool.tool.auxtrace_info = aslr_tool__process_auxtrace_info;
+ aslr->tool.tool.auxtrace_error = aslr_tool__process_auxtrace_error;
+}
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate)
+{
+ struct aslr_tool *aslr = zalloc(sizeof(*aslr));
+
+ if (!aslr)
+ return NULL;
+
+ aslr_tool__init(aslr, delegate);
+ return &aslr->tool.tool;
+}
+
+void aslr_tool__delete(struct perf_tool *tool)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct hashmap_entry *cur;
+ size_t bkt;
+
+ if (!tool)
+ return;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+
+ hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
+ struct remap_addresses_key *key = (struct remap_addresses_key *)cur->pkey;
+
+ if (key)
+ dso__put(key->dso);
+ zfree(&cur->pkey);
+ zfree(&cur->pvalue);
+ }
+ hashmap__for_each_entry(&aslr->top_addresses, cur, bkt) {
+ zfree(&cur->pvalue);
+ }
+
+ hashmap__clear(&aslr->remap_addresses);
+ hashmap__clear(&aslr->top_addresses);
+ machines__exit(&aslr->machines);
+ free(aslr);
+}
diff --git a/tools/perf/util/aslr.h b/tools/perf/util/aslr.h
new file mode 100644
index 000000000000..ea984d82681f
--- /dev/null
+++ b/tools/perf/util/aslr.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PERF_ASLR_H
+#define __PERF_ASLR_H
+
+struct perf_tool;
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate);
+void aslr_tool__delete(struct perf_tool *aslr);
+
+#endif /* __PERF_ASLR_H */
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v3 4/4] perf test: Add inject ASLR test
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (2 preceding siblings ...)
2026-05-04 3:51 ` [PATCH v3 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-05-04 3:51 ` Ian Rogers
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 3:51 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
Add a new shell test `inject_aslr.sh` to verify the `perf inject --aslr`
feature. The test covers:
- Basic address remapping for user space samples.
- Pipe mode coverage for `perf record` piped into `perf inject --aslr`.
- Callchain address remapping.
- Consistency of `perf report` output before and after injection.
- Pipe mode report consistency.
- Dropping of samples that leak ASLR info (physical addresses).
- Kernel address remapping (skipping gracefully if permissions restrict
recording the kernel map).
- Kernel report consistency with address normalization.
The test suite is hardened with:
- Global 'set -o pipefail' pipeline checks to catch failures in perf script.
- Safe awk processing loop closures that consume whole streams to avoid SIGPIPE
signal aborts.
- False success assertions to verify callchain data isn't dynamically dropped.
- Graceful error paths on empty sample records.
- Multi-arch support (32-bit and 64-bit address normalizations).
Assisted-by: Gemini-CLI:Google Gemini 3
Signed-off-by: Ian Rogers <irogers@google.com>
---
v3: Harden script with pipefail, SIGPIPE awk pipeline fixes, callchain empty
data asserts, baseline sample verification, and grep report abort
protections. Ensure grep report filters have || true suffixes. Reorder
set -e/pipefail to prevent stack leaks in mktemp failures.
v2: Add sum comparison for kernel overhead and 32-bit math corrections. Add
awk with gsub for trailing dots and brackets normalizations. Trap EXIT,
prevent race conditions and avoid hardcoded perf binary.
---
tools/perf/tests/shell/inject_aslr.sh | 423 ++++++++++++++++++++++++++
1 file changed, 423 insertions(+)
create mode 100755 tools/perf/tests/shell/inject_aslr.sh
diff --git a/tools/perf/tests/shell/inject_aslr.sh b/tools/perf/tests/shell/inject_aslr.sh
new file mode 100755
index 000000000000..fa7dd3b4c411
--- /dev/null
+++ b/tools/perf/tests/shell/inject_aslr.sh
@@ -0,0 +1,423 @@
+#!/bin/bash
+# perf inject --aslr test
+# SPDX-License-Identifier: GPL-2.0
+
+set -e
+set -o pipefail
+
+shelldir=$(dirname "$0")
+# shellcheck source=lib/perf_has_symbol.sh
+. "${shelldir}"/lib/perf_has_symbol.sh
+
+sym="noploop"
+
+skip_test_missing_symbol ${sym}
+
+# Create global temp directory
+temp_dir=$(mktemp -d /tmp/perf-test-aslr.XXXXXXXXXX)
+data="${temp_dir}/perf.data"
+data2="${temp_dir}/perf.data2"
+
+prog="perf test -w noploop"
+[ "$(uname -m)" = "s390x" ] && prog="$prog 3"
+err=0
+
+
+
+cleanup() {
+ # Check if temp_dir is set and looks sane before removing
+ if [[ "${temp_dir}" =~ ^/tmp/perf-test-aslr\. ]]; then
+ rm -rf "${temp_dir}"
+ fi
+}
+
+trap_cleanup() {
+ cleanup
+ exit 1
+}
+
+trap cleanup EXIT
+trap trap_cleanup TERM INT
+
+get_noploop_addr() {
+ local file=$1
+ perf script -i "$file" | awk '
+ BEGIN { found=0 }
+ {
+ for (i=1; i<=NF; i++) {
+ if ($i ~ /noploop\+/) {
+ if (!found) {
+ print $(i-1)
+ found=1
+ }
+ }
+ }
+ }'
+}
+
+test_basic_aslr() {
+ echo "Test basic ASLR remapping"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.basic.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.basic.XXXXXX")
+
+ perf record -e task-clock:u -o "${data}" ${prog}
+ perf inject -v --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Basic ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Basic ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Basic ASLR test [Success]"
+ fi
+}
+
+test_pipe_aslr() {
+ echo "Test pipe mode ASLR remapping"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.pipe.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.pipe.XXXXXX")
+
+ # Use tee to save the original pipe data for comparison
+ perf record -e task-clock:u -o - ${prog} | tee "${data}" | perf inject --aslr -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Pipe ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Pipe ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Pipe ASLR test [Success]"
+ fi
+}
+
+test_callchain_aslr() {
+ echo "Test Callchain ASLR remapping"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.callchain.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.callchain.XXXXXX")
+
+ perf record -g -e task-clock:u -o "${data}" ${prog}
+ perf inject --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Callchain ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Callchain ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ # Extract callchain addresses (indented lines starting with hex addresses)
+ orig_callchain=$(perf script -i "${data}" | awk '/^[[:space:]]+[0-9a-f]+/ {print $1}')
+ new_callchain=$(perf script -i "${data2}" | awk '/^[[:space:]]+[0-9a-f]+/ {print $1}')
+
+ if [ -z "$orig_callchain" ]; then
+ echo "Callchain ASLR test [Failed - no callchain samples in original file]"
+ err=1
+ elif [ -z "$new_callchain" ]; then
+ echo "Callchain ASLR test [Failed - callchain data was dropped]"
+ err=1
+ elif [ "$orig_callchain" = "$new_callchain" ]; then
+ echo "Callchain ASLR test [Failed - callchain addresses were not remapped]"
+ err=1
+ else
+ echo "Callchain ASLR test [Success]"
+ fi
+ fi
+}
+
+test_report_aslr() {
+ echo "Test perf report consistency"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.report.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.report.XXXXXX")
+ local data_clean
+ data_clean=$(mktemp "${temp_dir}/perf.data.clean.XXXXXX")
+
+ perf record -e task-clock:u -o "${data}" ${prog}
+ # Use -b to inject build-ids and force ordered events processing in both
+ perf inject -b -i "${data}" -o "${data_clean}"
+ perf inject -v -b --aslr -i "${data}" -o "${data2}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}" || true
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ ! -s "${report1_clean}" ]; then
+ echo "Report ASLR test [Failed - no samples captured]"
+ err=1
+ elif [ -s "${diff_file}" ]; then
+ echo "Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Report ASLR test [Success]"
+ fi
+}
+
+test_pipe_report_aslr() {
+ echo "Test pipe mode perf report consistency"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.pipe_report.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.pipe_report.XXXXXX")
+ local data_clean
+ data_clean=$(mktemp "${temp_dir}/perf.data.clean.XXXXXX")
+
+ # Use tee to save the original pipe data, then process it with inject -b
+ perf record -e task-clock:u -o - ${prog} | \
+ tee "${data}" | \
+ perf inject -b --aslr -o "${data2}"
+ perf inject -b -i "${data}" -o "${data_clean}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}" || true
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ ! -s "${report1_clean}" ]; then
+ echo "Pipe Report ASLR test [Failed - no samples captured]"
+ err=1
+ elif [ -s "${diff_file}" ]; then
+ echo "Pipe Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Pipe Report ASLR test [Success]"
+ fi
+}
+
+test_dropped_samples() {
+ echo "Test dropped samples (phys-data)"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.dropped.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.dropped.XXXXXX")
+
+ # Check if --phys-data is supported by recording a short run
+ if ! perf record -e task-clock:u --phys-data -o "${data}" -- sleep 0.1 > /dev/null 2>&1; then
+ echo "Skipping dropped samples test as --phys-data is not supported"
+ return
+ fi
+
+ perf record -e task-clock:u --phys-data -o "${data}" ${prog}
+ perf inject --aslr -i "${data}" -o "${data2}"
+
+ # Verify that the original file actually contained samples!
+ orig_samples=$(perf script -i "${data}" | wc -l)
+ if [ "$orig_samples" -eq 0 ]; then
+ echo "Dropped samples test [Failed - no samples in original file]"
+ err=1
+ else
+ # Verify that samples are dropped.
+ samples_count=$(perf script -i "${data2}" | wc -l)
+
+ if [ "$samples_count" -gt 0 ]; then
+ echo "Dropped samples test [Failed - samples were not dropped]"
+ err=1
+ else
+ echo "Dropped samples test [Success]"
+ fi
+ fi
+}
+
+test_kernel_aslr() {
+ echo "Test kernel ASLR remapping"
+ local kdata
+ kdata=$(mktemp "${temp_dir}/perf.data.kernel.XXXXXX")
+ local kdata2
+ kdata2=$(mktemp "${temp_dir}/perf.data2.kernel.XXXXXX")
+ local log_file
+ log_file=$(mktemp "${temp_dir}/kernel_record.log.XXXXXX")
+
+ # Try to record kernel samples
+ if ! perf record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel ASLR test as recording failed (maybe no permissions)"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel ASLR test as kernel map could not be recorded (permissions restricted)"
+ return
+ fi
+
+ perf inject -v --aslr -i "${kdata}" -o "${kdata2}"
+
+ # Check if kernel addresses are remapped.
+ # Find the field that ends with :k: (the event name) and take the next field!
+ orig_addr=$(perf script -i "${kdata}" | awk '
+ BEGIN { found=0 }
+ {
+ for (i=1; i<NF; i++) {
+ if ($i ~ /:[k]+:?$/) {
+ if (!found) {
+ print $(i+1)
+ found=1
+ }
+ }
+ }
+ }')
+ new_addr=$(perf script -i "${kdata2}" | awk '
+ BEGIN { found=0 }
+ {
+ for (i=1; i<NF; i++) {
+ if ($i ~ /:[k]+:?$/) {
+ if (!found) {
+ print $(i+1)
+ found=1
+ }
+ }
+ }
+ }')
+
+ echo "Kernel ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Kernel ASLR test [Failed - no kernel samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Kernel ASLR test [Success]"
+ fi
+}
+
+test_kernel_report_aslr() {
+ echo "Test kernel perf report consistency"
+ local kdata
+ kdata=$(mktemp "${temp_dir}/perf.data.kernel_report.XXXXXX")
+ local kdata2
+ kdata2=$(mktemp "${temp_dir}/perf.data2.kernel_report.XXXXXX")
+ local data_clean
+ data_clean=$(mktemp "${temp_dir}/perf.data.clean.XXXXXX")
+ local log_file
+ log_file=$(mktemp "${temp_dir}/kernel_report_record.log.XXXXXX")
+
+ # Try to record kernel samples
+ if ! perf record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel report test as recording failed (maybe no permissions)"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel report test as kernel map could not be recorded (permissions restricted)"
+ return
+ fi
+
+ # Use -b to inject build-ids and force ordered events processing in both
+ perf inject -b -i "${kdata}" -o "${data_clean}"
+ perf inject -v -b --aslr -i "${kdata}" -o "${kdata2}"
+
+ local report1="${temp_dir}/report_kernel1"
+ local report2="${temp_dir}/report_kernel2"
+ local report1_clean="${temp_dir}/report_kernel1.clean"
+ local report2_clean="${temp_dir}/report_kernel2.clean"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${kdata2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' > "${report2_clean}" || true
+
+ # Normalize kernel DSOs and addresses in clean reports
+ # This allows kernel modules to be either a module or kernel.kallsyms
+ local report1_norm="${temp_dir}/report_kernel1.norm"
+ local report2_norm="${temp_dir}/report_kernel2.norm"
+
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); \
+ print}' "${report1_clean}" > "${report1_norm}"
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); \
+ print}' "${report2_clean}" > "${report2_norm}"
+
+ # Calculate sum of percentages for [kernel] samples
+ sum1=$(awk '/\[kernel\]/ {sum += $1} END {print sum}' "${report1_norm}")
+ sum2=$(awk '/\[kernel\]/ {sum += $1} END {print sum}' "${report2_norm}")
+
+ echo "Kernel report sums: sum1=$sum1, sum2=$sum2"
+
+ # Compare sums with tolerance
+ if [ -z "$sum1" ] || [ -z "$sum2" ]; then
+ echo "Kernel Report ASLR test [Failed - no kernel data]"
+ err=1
+ elif awk -v s1="$sum1" -v s2="$sum2" \
+ 'BEGIN {diff = s1 - s2; if (diff < 0) diff = -diff; if (diff < 0.05) exit 0; else exit 1}'; then
+ echo "Kernel Report ASLR test [Success]"
+ else
+ echo "Kernel Report ASLR test [Failed - sums differ too much]"
+ err=1
+ fi
+}
+
+test_basic_aslr
+test_pipe_aslr
+test_callchain_aslr
+test_report_aslr
+test_pipe_report_aslr
+test_dropped_samples
+test_kernel_aslr
+test_kernel_report_aslr
+
+cleanup
+exit $err
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (3 preceding siblings ...)
2026-05-04 3:51 ` [PATCH v3 4/4] perf test: Add inject ASLR test Ian Rogers
@ 2026-05-04 7:29 ` Ian Rogers
2026-05-04 7:29 ` [PATCH v4 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
` (4 more replies)
4 siblings, 5 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 7:29 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
This patch series introduces the new 'perf inject --aslr' feature to remap
virtual memory addresses or drop physical memory event leaks when profile
record data is shared between machines. Bundled with this feature are two
independent, critical bug fixes inside core event dispatching tools that
harden perf session analysis against dynamic crashes and callchain mapping
failures.
Core Feature: 'perf inject --aslr' (Patches 3 and 4)
Transferring perf.data files across environments introduces a potential leak
of virtual address footprints, weakening Address Space Layout Randomization
(ASLR) on the originating machine. To mitigate this, we introduce the --aslr
flag into perf inject. Unknown or unhandled events are dropped conservatively,
while handled samples and branch loops undergo systematic virtual memory offset
obfuscation.
To ensure comprehensive memory and error-path safety, the ASLR tool implements:
- Machine namespaces ('struct machines') to safely interleave host mappings and
unprivileged KVM guest virtual address mappings.
- Resolves VMA split map failures (caused by overlap fixups during map
insertions) consistently by anchoring mappings on DSO and memory
invariants.
- Guards against integer overflows in branch stack loops via
subtraction-based bounds arithmetic.
- Prevents heap buffer overflows by computing safe word limits on
userspace stacks and dynamic hardware tracing (AUX) sizes.
- Prevents key collisions/ABA lookups by correctly managing DSO
reference counts (dso__get/put).
- Cleans up error paths to avoid inconsistent hashmap mappings on
OOM failures.
- Optimizes performance by removing redundant hot-path memory
allocations.
- Cleanly advances session readers past dropped auxtrace streams
using pipe-stream I/O skip helpers.
- Scrubs breakpoint addresses (bp_addr) from output event headers
and dynamically synthesized events for pipes via a custom pipe
repipe wrapper to prevent unscrubbed address leakage.
- Remaps kernel memory maps linearly to maintain secure base
obfuscation bounds.
- Hardens guest cpumode lookups against corrupting host/guest user and
kernel mapping boundaries during sample fallback searches.
- Synchronizes ksymbol map tracking invariants using precise VMA
offset math rather than raw addresses to prevent unique base leaks
on every function symbol.
- Blocks trailing heap padding byte data leakage vectors in userspace
stacks and AUX tracking frames via targeted tail-word clearing.
Verification is reinforced in Patch 4 with a new comprehensive POSIX shell
suite ('inject_aslr.sh'), hardened against SIGPIPE signal exits with stream
consuming awk loops and robust 'set -o pipefail' assertions. The suite includes
a new dedicated scenario validating pipe stdout injection attribute stability.
Prerequisite Bug Fixes (Patches 1 and 2)
During development, two core event delegation issues were identified and
resolved to prevent crashes and data-loss during analysis:
1. perf sched: 'timehist' registers standard MMAP, COMM, EXIT, and FORK stubs,
but completely omitted registering MMAP2 callbacks. Because modern environments
output maps primarily via MMAP2 frames, this caused timehist sessions to silently
drop shared library mappings, causing dynamic callchain symbol resolutions to
fail. Patch 1 corrects this by properly registering perf_event__process_mmap2.
2. perf tool: Patch 2 fixes missing copies of schedstat callbacks inside delegated
wrapper tools (which caused segfaults on NULL stubs) and properly initializes/copies
the 'dont_split_sample_group' grouping parameters to prevent stack garbage from
triggering silent non-leader events drops during split deliver streams.
Changes since v3:
- Feature integration: Pass a dedicated 'perf_event__aslr_repipe' callback to
perf_event__synthesize_for_pipe() to scrub synthesized breakpoint attributes.
- Feature core: Loop through and scrub event evlist breakpoint attributes right
before writing file headers in __cmd_inject().
- Feature core: Linearize kernel map base obfuscation and remove redundant pgoff
delta adjustments that leaked kernel layout calculations.
- Feature core: Fix host/guest cpumode mappings in sample fallback lookups.
- Feature core: Sync ksymbol tracking keys onto VMA offset invariants.
- Feature core: Zero out trailing padding word bytes in user stacks and AUX blocks.
- Validation suite: Add 'test_pipe_out_report_aslr' validation case.
- Validation suite: Upgrade kernel report checks to strict sorted line-by-line diffs.
- Style: Wrap all commit description lines to under 75 columns and fix code formatting.
Ian Rogers (4):
perf sched: Add missing mmap2 handler in timehist
perf tool: Fix missing schedstat delegates and dont_split_sample_group
in delegate_tool
perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
perf test: Add inject ASLR test
tools/perf/builtin-inject.c | 52 +-
tools/perf/builtin-sched.c | 1 +
tools/perf/tests/shell/inject_aslr.sh | 459 ++++++++++
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1161 +++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
tools/perf/util/tool.c | 6 +
7 files changed, 1689 insertions(+), 1 deletion(-)
create mode 100755 tools/perf/tests/shell/inject_aslr.sh
create mode 100644 tools/perf/util/aslr.c
create mode 100644 tools/perf/util/aslr.h
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply [flat|nested] 15+ messages in thread* [PATCH v4 1/4] perf sched: Add missing mmap2 handler in timehist
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
@ 2026-05-04 7:29 ` Ian Rogers
2026-05-04 7:29 ` [PATCH v4 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
` (3 subsequent siblings)
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 7:29 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
perf_sched__timehist() registers event handlers for options using the
sched->tool struct. It registers handlers for MMAP, COMM, EXIT, FORK, etc.
but completely omits registering a handler for MMAP2 events.
Failing to register both MMAP and MMAP2 handlers causes modern systems
(which primarily output MMAP2 records) to silently drop VMA map mappings.
This results in uninitialized machine/thread mapping structures, making it
impossible to resolve shared library instruction pointers (IPs) to dynamic
symbols/DSOs during timehist callchain analysis.
Fix this by correctly registering perf_event__process_mmap2 in
sched->tool inside perf_sched__timehist().
Assisted-by: Gemini-CLI:Google Gemini 3
Fixes: 5bbfec0ad93c ("perf sched: Implement timehist option")
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/builtin-sched.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/tools/perf/builtin-sched.c b/tools/perf/builtin-sched.c
index 555247568e7a..241c2f808f7b 100644
--- a/tools/perf/builtin-sched.c
+++ b/tools/perf/builtin-sched.c
@@ -3299,6 +3299,7 @@ static int perf_sched__timehist(struct perf_sched *sched)
*/
sched->tool.sample = perf_timehist__process_sample;
sched->tool.mmap = perf_event__process_mmap;
+ sched->tool.mmap2 = perf_event__process_mmap2;
sched->tool.comm = perf_event__process_comm;
sched->tool.exit = perf_event__process_exit;
sched->tool.fork = perf_event__process_fork;
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v4 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-04 7:29 ` [PATCH v4 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
@ 2026-05-04 7:29 ` Ian Rogers
2026-05-04 7:29 ` [PATCH v4 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
` (2 subsequent siblings)
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 7:29 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
delegate_tool was missing the delegate overrides for schedstat_cpu
and schedstat_domain. As a result, when allocated with zalloc, these
callbacks defaulted to NULL, causing a segmentation fault crash if
any schedstat events were delivered during event processing.
Fix this by adding delegate_schedstat_cpu and delegate_schedstat_domain
via the CREATE_DELEGATE_OP2 macro, and ensuring delegate_tool__init
correctly registers them.
Additionally, delegate_tool__init completely omitted copying the
dont_split_sample_group property from the delegate. This would cause
wrapper tools to default the flag to false, which corrupts piped event
processing (e.g., in perf inject) by triggering duplicate event
deliveries on split sample values in deliver_sample_group().
Similarly, perf_tool__init() omitted the initialization of this
boolean field. On stack-allocated tools that rely on this initializer
(like intel-tpebs or __cmd_evlist), this could result in uninitialized
stack garbage evaluating to true—silently dropping non-leader event
members in deliver_sample_group().
Fix both issues by properly copying the field in delegate_tool__init
and initializing it to false in perf_tool__init.
Assisted-by: Gemini-CLI:Google Gemini 3
Fixes: 6331b2669359 ("perf tool: Add a delegate_tool that just delegates actions to another tool")
Fixes: 79bcd34e0f3d ("perf inject: Fix leader sampling inserting additional samples")
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/tool.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/tools/perf/util/tool.c b/tools/perf/util/tool.c
index 013c7839e2cf..ff2150517b75 100644
--- a/tools/perf/util/tool.c
+++ b/tools/perf/util/tool.c
@@ -285,6 +285,7 @@ void perf_tool__init(struct perf_tool *tool, bool ordered_events)
tool->no_warn = false;
tool->show_feat_hdr = SHOW_FEAT_NO_HEADER;
tool->merge_deferred_callchains = true;
+ tool->dont_split_sample_group = false;
tool->sample = process_event_sample_stub;
tool->mmap = process_event_stub;
@@ -433,6 +434,8 @@ CREATE_DELEGATE_OP2(stat_config);
CREATE_DELEGATE_OP2(stat_round);
CREATE_DELEGATE_OP2(thread_map);
CREATE_DELEGATE_OP2(time_conv);
+CREATE_DELEGATE_OP2(schedstat_cpu);
+CREATE_DELEGATE_OP2(schedstat_domain);
CREATE_DELEGATE_OP2(tracing_data);
#define CREATE_DELEGATE_OP3(name) \
@@ -470,6 +473,7 @@ void delegate_tool__init(struct delegate_tool *tool, struct perf_tool *delegate)
tool->tool.no_warn = delegate->no_warn;
tool->tool.show_feat_hdr = delegate->show_feat_hdr;
tool->tool.merge_deferred_callchains = delegate->merge_deferred_callchains;
+ tool->tool.dont_split_sample_group = delegate->dont_split_sample_group;
tool->tool.sample = delegate_sample;
tool->tool.read = delegate_read;
@@ -516,4 +520,6 @@ void delegate_tool__init(struct delegate_tool *tool, struct perf_tool *delegate)
tool->tool.bpf_metadata = delegate_bpf_metadata;
tool->tool.compressed = delegate_compressed;
tool->tool.auxtrace = delegate_auxtrace;
+ tool->tool.schedstat_cpu = delegate_schedstat_cpu;
+ tool->tool.schedstat_domain = delegate_schedstat_domain;
}
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v4 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-04 7:29 ` [PATCH v4 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-04 7:29 ` [PATCH v4 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
@ 2026-05-04 7:29 ` Ian Rogers
2026-05-04 7:29 ` [PATCH v4 4/4] perf test: Add inject ASLR test Ian Rogers
2026-05-04 8:23 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 7:29 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
If perf.data files are taken from one machine to another they make
leak virtual addresses and so weaken ASLR on the machine they are
coming from. Add a '--aslr' option for perf inject that remaps all
virtual addresses, or drops data/events, so that the virtual address
information isn't leaked.
When events are not known/handled by the tool they are dropped. This
makes the tool conservative and it should never leak ASLR information,
but it means virtual address remapping is needed for cases like
auxtrace.
To ensure comprehensive robustness and security, this tool:
- Employs guest namespace isolation by utilizing 'struct machines' to
safely interleave host and unprivileged KVM guest virtual address
mappings.
- Resolves VMA split map failures (caused by overlap fixups during map
insertions) consistently by anchoring mappings on DSO and memory
invariants.
- Guards against integer overflows in branch stack loops via
subtraction-based bounds arithmetic.
- Prevents heap buffer overflows by computing safe word limits on
userspace stacks and dynamic hardware tracing (AUX) sizes.
- Prevents key collisions/ABA lookups by correctly managing DSO
reference counts (dso__get/put).
- Cleans up error paths to avoid inconsistent hashmap mappings on
OOM failures.
- Optimizes performance by removing redundant hot-path memory
allocations.
- Cleanly advances session readers past dropped auxtrace streams
using pipe-stream I/O skip helpers.
- Scrubs breakpoint addresses (bp_addr) from output event headers
and dynamically synthesized events for pipes via a custom pipe
repipe wrapper to prevent unscrubbed address leakage.
- Remaps kernel memory maps linearly to maintain secure base
obfuscation bounds.
- Hardens guest cpumode lookups against corrupting host/guest user and
kernel mapping boundaries during sample fallback searches.
- Synchronizes ksymbol map tracking invariants using precise VMA
offset math rather than raw addresses to prevent unique base leaks
on every function symbol.
- Blocks trailing heap padding byte data leakage vectors in userspace
stacks and AUX tracking frames via targeted tail-word clearing.
Assisted-by: Gemini-CLI:Google Gemini 3
Signed-off-by: Ian Rogers <irogers@google.com>
Co-developed-by: Gabriel Marin <gmx@google.com>
Signed-off-by: Gabriel Marin <gmx@google.com>
---
v4: Scrub bp_addr from headers/pipe synthesis attributes. Remove kernel
mmap pgoff mathematical delta adjustment leaks to maintain secure
base obfuscation bounds. Harden guest space contexts mapping loops,
correct ksymbol map base invariants tracking, and plug tail-word
padding heap leakage vectors in user stacks and AUX payloads.
v3: Combine split-map fixes, guest namespaces, bounds checks, OOM rollbacks,
hot path optimization, safe dso references, and I/O stream error handling
from v3/v4 development. Drop raw auxtrace events. Fix thread reference leaks
in event handlers. Fix 32-bit truncation bug in hashmaps using u64* values.
Prevent leaking uninitialized heap memory by zeroing copy buffer. Correct
bitmask checks for branch stack flags. Avoid PMU configuration corruption.
v2: First review feedback adjustments.
---
tools/perf/builtin-inject.c | 52 +-
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1161 +++++++++++++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
4 files changed, 1223 insertions(+), 1 deletion(-)
create mode 100644 tools/perf/util/aslr.c
create mode 100644 tools/perf/util/aslr.h
diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c
index f174bc69cec4..d2167a94d4be 100644
--- a/tools/perf/builtin-inject.c
+++ b/tools/perf/builtin-inject.c
@@ -8,6 +8,7 @@
*/
#include "builtin.h"
+#include "util/aslr.h"
#include "util/color.h"
#include "util/dso.h"
#include "util/vdso.h"
@@ -123,6 +124,7 @@ struct perf_inject {
bool in_place_update_dry_run;
bool copy_kcore_dir;
bool convert_callchain;
+ bool aslr;
const char *input_name;
struct perf_data output;
u64 bytes_written;
@@ -304,6 +306,29 @@ static int perf_event__repipe(const struct perf_tool *tool,
return perf_event__repipe_synth(tool, event);
}
+/**
+ * perf_event__aslr_repipe - Wrapper to scrub synthesized pipe attributes.
+ * @tool: The original &inject.tool pointer.
+ * @event: The synthesized perf_event record.
+ *
+ * Synthesized attributes for pipes bypass the main event stream tool hooks.
+ * This wrapper intercepts them during pipe header generation to clear unprivileged
+ * breakpoint addresses (bp_addr). It forwards execution using the original tool
+ * context pointer to ensure container_of(&inject.tool) evaluation inside the
+ * downstream repipe stubs remains valid and does not cause structure corruptions.
+ */
+static int perf_event__aslr_repipe(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ if (event->header.type == PERF_RECORD_HEADER_ATTR &&
+ event->attr.attr.type == PERF_TYPE_BREAKPOINT) {
+ event->attr.attr.bp_addr = 0;
+ }
+ return perf_event__repipe(tool, event, sample, machine);
+}
+
static int perf_event__drop(const struct perf_tool *tool __maybe_unused,
union perf_event *event __maybe_unused,
struct perf_sample *sample __maybe_unused,
@@ -2458,6 +2483,15 @@ static int __cmd_inject(struct perf_inject *inject)
}
}
+ if (inject->aslr) {
+ struct evsel *evsel;
+
+ evlist__for_each_entry(session->evlist, evsel) {
+ if (evsel->core.attr.type == PERF_TYPE_BREAKPOINT)
+ evsel->core.attr.bp_addr = 0;
+ }
+ }
+
session->header.data_offset = output_data_offset;
session->header.data_size = inject->bytes_written;
perf_session__inject_header(session, session->evlist, fd, &inj_fc.fc,
@@ -2564,6 +2598,8 @@ int cmd_inject(int argc, const char **argv)
" instance has a subdir"),
OPT_BOOLEAN(0, "convert-callchain", &inject.convert_callchain,
"Generate callchains using DWARF and drop register/stack data"),
+ OPT_BOOLEAN(0, "aslr", &inject.aslr,
+ "Remap virtual memory addresses similar to ASLR"),
OPT_END()
};
const char * const inject_usage[] = {
@@ -2571,6 +2607,7 @@ int cmd_inject(int argc, const char **argv)
NULL
};
bool ordered_events;
+ struct perf_tool *tool = &inject.tool;
if (!inject.itrace_synth_opts.set) {
/* Disable eager loading of kernel symbols that adds overhead to perf inject. */
@@ -2684,12 +2721,21 @@ int cmd_inject(int argc, const char **argv)
inject.tool.schedstat_domain = perf_event__repipe_op2_synth;
inject.tool.dont_split_sample_group = true;
inject.tool.merge_deferred_callchains = false;
- inject.session = __perf_session__new(&data, &inject.tool,
+ if (inject.aslr) {
+ tool = aslr_tool__new(&inject.tool);
+ if (!tool) {
+ ret = -ENOMEM;
+ goto out_close_output;
+ }
+ }
+ inject.session = __perf_session__new(&data, tool,
/*trace_event_repipe=*/inject.output.is_pipe,
/*host_env=*/NULL);
if (IS_ERR(inject.session)) {
ret = PTR_ERR(inject.session);
+ if (inject.aslr)
+ aslr_tool__delete(tool);
goto out_close_output;
}
@@ -2717,6 +2763,8 @@ int cmd_inject(int argc, const char **argv)
ret = perf_event__synthesize_for_pipe(&inject.tool,
inject.session,
&inject.output,
+ inject.aslr ?
+ perf_event__aslr_repipe :
perf_event__repipe);
if (ret < 0)
goto out_delete;
@@ -2789,6 +2837,8 @@ int cmd_inject(int argc, const char **argv)
strlist__delete(inject.known_build_ids);
zstd_fini(&(inject.session->zstd_data));
perf_session__delete(inject.session);
+ if (inject.aslr)
+ aslr_tool__delete(tool);
out_close_output:
if (!inject.in_place_update)
perf_data__close(&inject.output);
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 70cc91d00804..65b96f3b87e2 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -6,6 +6,7 @@ perf-util-y += arm64-frame-pointer-unwind-support.o
perf-util-y += addr2line.o
perf-util-y += addr_location.o
perf-util-y += annotate.o
+perf-util-y += aslr.o
perf-util-y += blake2s.o
perf-util-y += block-info.o
perf-util-y += block-range.o
diff --git a/tools/perf/util/aslr.c b/tools/perf/util/aslr.c
new file mode 100644
index 000000000000..27b642c91eb9
--- /dev/null
+++ b/tools/perf/util/aslr.c
@@ -0,0 +1,1161 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "aslr.h"
+
+#include "addr_location.h"
+#include "debug.h"
+#include "event.h"
+#include "evsel.h"
+#include "machine.h"
+#include "map.h"
+#include "thread.h"
+#include "tool.h"
+#include "session.h"
+#include "data.h"
+#include "dso.h"
+
+#include <internal/lib.h> /* page_size */
+#include <linux/compiler.h>
+#include <linux/zalloc.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <unistd.h>
+
+static int skipn(int fd, u64 n)
+{
+ char buf[4096];
+ ssize_t ret;
+
+ while (n > 0) {
+ ret = read(fd, buf, (n < (u64)sizeof(buf) ? n : (u64)sizeof(buf)));
+ if (ret <= 0)
+ return ret;
+ n -= ret;
+ }
+
+ return 0;
+}
+
+/**
+ * struct remap_addresses_key - Key for mapping original addresses to remapped ones.
+ * @dso: Pointer to the DSO (Dynamic Shared Object) associated with the mapping.
+ * @invariant: Unique offset invariant within the VMA (Virtual Memory Area).
+ * Calculated as `start - pgoff`. This value remains constant when
+ * perf's internal `maps__fixup_overlap_and_insert` splits a map into
+ * fragmented VMA pieces due to overlapping events, allowing us to
+ * resolve split maps consistently back to the original VMA.
+ * @pid: Process ID associated with the mapping.
+ */
+struct remap_addresses_key {
+ struct dso *dso;
+ u64 invariant;
+ pid_t pid;
+};
+
+struct aslr_mapping {
+ struct list_head node;
+ u64 orig_start;
+ u64 len;
+ u64 remap_start;
+};
+
+struct aslr_tool {
+ /** @tool: The tool implemented here and a pointer to a delegate to process the data. */
+ struct delegate_tool tool;
+ /** @machines: The machines with the input, not remapped, virtual address layout. */
+ struct machines machines;
+ /** @event_copy: Buffer used to create an event to pass to the delegate. */
+ char event_copy[PERF_SAMPLE_MAX_SIZE];
+ /** @remap_addresses: mapping from remap_addresses_key to remapped address. */
+ struct hashmap remap_addresses;
+ /** @top_addresses: mapping from process to max remapped address. */
+ struct hashmap top_addresses;
+};
+
+static const pid_t kernel_pid = -1;
+
+/* Start remapping user processes from a small non-zero offset. */
+static const u64 user_space_start = 0x200000;
+static const u64 kernel_space_start = 0xffff800010000000;
+
+static size_t remap_addresses__hash(long _key, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key = (struct remap_addresses_key *)_key;
+
+ return (size_t)key->dso ^ key->invariant ^ key->pid;
+}
+
+static bool remap_addresses__equal(long _key1, long _key2, void *ctx __maybe_unused)
+{
+ struct remap_addresses_key *key1 = (struct remap_addresses_key *)_key1;
+ struct remap_addresses_key *key2 = (struct remap_addresses_key *)_key2;
+
+ return key1->dso == key2->dso &&
+ key1->invariant == key2->invariant &&
+ key1->pid == key2->pid;
+}
+
+static size_t top_addresses__hash(long key, void *ctx __maybe_unused)
+{
+ return key;
+}
+
+static bool top_addresses__equal(long key1, long key2, void *ctx __maybe_unused)
+{
+ return key1 == key2;
+}
+
+static u64 round_up_to_page_size(u64 addr)
+{
+ return (addr + page_size - 1) & ~((u64)page_size - 1);
+}
+
+static u64 aslr_tool__remap_address(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 addr)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 remap_addr = 0;
+ u8 effective_cpumode = cpumode;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&al);
+ if (!thread__find_map(aslr_thread, cpumode, addr, &al)) {
+ /*
+ * If lookup fails with specified cpumode, try fallback to the other space
+ * to be robust against bad cpumode in samples.
+ */
+ if (cpumode == PERF_RECORD_MISC_KERNEL)
+ effective_cpumode = PERF_RECORD_MISC_USER;
+ else if (cpumode == PERF_RECORD_MISC_USER)
+ effective_cpumode = PERF_RECORD_MISC_KERNEL;
+ else if (cpumode == PERF_RECORD_MISC_GUEST_KERNEL)
+ effective_cpumode = PERF_RECORD_MISC_GUEST_USER;
+ else if (cpumode == PERF_RECORD_MISC_GUEST_USER)
+ effective_cpumode = PERF_RECORD_MISC_GUEST_KERNEL;
+
+ if (!thread__find_map(aslr_thread, effective_cpumode, addr, &al)) {
+ addr_location__exit(&al);
+ return 0; /* No mmap. */
+ }
+ }
+
+ key.dso = map__dso(al.map);
+ key.invariant = map__start(al.map) - map__pgoff(al.map);
+ key.pid = effective_cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+
+ if (hashmap__find(&aslr->remap_addresses, &key, &remapped_invariant_ptr)) {
+ remap_addr = *remapped_invariant_ptr + map__pgoff(al.map) +
+ (addr - map__start(al.map));
+ } else {
+ pr_debug("Cannot find a remapped entry for address %lx in mapping %lx(%lx) for pid=%d\n",
+ addr, map__start(al.map), map__size(al.map), key.pid);
+ }
+
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_mapping(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode,
+ u64 start, u64 len, u64 pgoff)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 *max_addr_ptr = NULL;
+ bool is_contiguous = false;
+ bool first_mapping = false;
+ bool key_found = false;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&al);
+ if (thread__find_map(aslr_thread, cpumode, start, &al))
+ key.dso = map__dso(al.map);
+ else
+ key.dso = NULL;
+
+ key.invariant = start - pgoff;
+ key.pid = cpumode == PERF_RECORD_MISC_KERNEL ? kernel_pid : aslr_thread->pid_;
+
+ if (hashmap__find(&aslr->remap_addresses, &key, &remapped_invariant_ptr)) {
+ remap_addr = *remapped_invariant_ptr + pgoff;
+ key_found = true;
+ } else {
+ struct addr_location prev_al;
+
+ addr_location__init(&prev_al);
+ if (thread__find_map(aslr_thread, cpumode, start - 1, &prev_al)) {
+ if (map__start(prev_al.map) + map__size(prev_al.map) == start) {
+ is_contiguous = true;
+ } else {
+ pr_debug("Previous mmap [%lx, %lx] overlaps current map [%lx, %lx]\n",
+ map__start(prev_al.map),
+ map__start(prev_al.map) + map__size(prev_al.map),
+ start, start+len);
+ }
+ }
+ addr_location__exit(&prev_al);
+
+ if (!hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ first_mapping = true;
+ remap_addr = (cpumode == PERF_RECORD_MISC_KERNEL ?
+ kernel_space_start : user_space_start);
+ } else {
+ remap_addr = *max_addr_ptr;
+ }
+
+ remap_addr = round_up_to_page_size(remap_addr);
+ if (!is_contiguous && !first_mapping)
+ remap_addr += page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+ u64 *new_val = malloc(sizeof(u64));
+
+ if (!new_key || !new_val) {
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_key = key;
+ new_key->dso = dso__get(key.dso);
+ *new_val = remap_addr - pgoff;
+
+ if (hashmap__add(&aslr->remap_addresses, new_key, new_val) != 0) {
+ dso__put(new_key->dso);
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ }
+ }
+
+ /* Update top_addresses */
+ {
+ u64 *new_max = malloc(sizeof(u64));
+ u64 *old_val = NULL;
+ int err;
+
+ if (!new_max) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ if (!key_found) {
+ hashmap__delete(&aslr->remap_addresses, &key,
+ &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ }
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_max = remap_addr + len;
+
+ if (hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ if (*max_addr_ptr > *new_max)
+ *new_max = *max_addr_ptr;
+ }
+
+ err = hashmap__insert(&aslr->top_addresses, key.pid, new_max,
+ (first_mapping && !key_found) ?
+ HASHMAP_ADD : HASHMAP_UPDATE,
+ NULL, &old_val);
+ if (err) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ free(new_max);
+ if (!key_found) {
+ hashmap__delete(&aslr->remap_addresses, &key,
+ &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ }
+ addr_location__exit(&al);
+ return 0;
+ }
+ free(old_val);
+ }
+
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+static u64 aslr_tool__remap_ksymbol(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u64 addr, u32 len)
+{
+ struct addr_location al;
+ struct remap_addresses_key key;
+ u64 remap_addr = 0;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 *max_addr_ptr = NULL;
+ bool first_mapping = false;
+
+ if (!aslr_thread)
+ return 0; /* No thread. */
+
+ addr_location__init(&al);
+ if (thread__find_map(aslr_thread, PERF_RECORD_MISC_KERNEL, addr, &al)) {
+ key.dso = map__dso(al.map);
+ key.invariant = map__start(al.map) - map__pgoff(al.map);
+ } else {
+ key.dso = NULL;
+ key.invariant = addr; /* pgoff is 0 for ksymbols */
+ }
+ key.pid = aslr_thread->pid_;
+
+ if (hashmap__find(&aslr->remap_addresses, &key, &remapped_invariant_ptr)) {
+ if (al.map)
+ remap_addr = *remapped_invariant_ptr + map__pgoff(al.map) +
+ (addr - map__start(al.map));
+ else
+ remap_addr = *remapped_invariant_ptr;
+ addr_location__exit(&al);
+ return remap_addr;
+ }
+
+ if (!hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ first_mapping = true;
+ remap_addr = kernel_space_start;
+ } else {
+ remap_addr = *max_addr_ptr;
+ }
+
+ remap_addr = round_up_to_page_size(remap_addr) + page_size;
+
+ {
+ struct remap_addresses_key *new_key = malloc(sizeof(*new_key));
+ u64 *new_val = malloc(sizeof(u64));
+
+ if (!new_key || !new_val) {
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_key = key;
+ new_key->dso = dso__get(key.dso);
+ if (al.map)
+ *new_val = remap_addr - (addr - map__start(al.map)) - map__pgoff(al.map);
+ else
+ *new_val = remap_addr;
+
+ if (hashmap__add(&aslr->remap_addresses, new_key, new_val) < 0) {
+ dso__put(new_key->dso);
+ free(new_key);
+ free(new_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ }
+
+ {
+ u64 *new_max = malloc(sizeof(u64));
+ u64 *old_val = NULL;
+ int err;
+
+ if (!new_max) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ hashmap__delete(&aslr->remap_addresses, &key, &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ addr_location__exit(&al);
+ return 0;
+ }
+ *new_max = remap_addr + len;
+
+ if (hashmap__find(&aslr->top_addresses, key.pid, &max_addr_ptr)) {
+ if (*max_addr_ptr > *new_max)
+ *new_max = *max_addr_ptr;
+ }
+
+ err = hashmap__insert(&aslr->top_addresses, key.pid, new_max,
+ first_mapping ?
+ HASHMAP_ADD : HASHMAP_UPDATE,
+ NULL, &old_val);
+ if (err) {
+ struct remap_addresses_key *old_key = NULL;
+ u64 *old_val_remap = NULL;
+
+ free(new_max);
+ hashmap__delete(&aslr->remap_addresses, &key, &old_key, &old_val_remap);
+ if (old_key)
+ dso__put(old_key->dso);
+ free(old_key);
+ free(old_val_remap);
+ addr_location__exit(&al);
+ return 0;
+ }
+ free(old_val);
+ }
+
+ addr_location__exit(&al);
+ return remap_addr;
+}
+
+
+static int aslr_tool__process_mmap(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ u8 cpumode;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(aslr_machine, event->mmap.pid, event->mmap.tid);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->mmap, &event->mmap, event->mmap.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap.start,
+ event->mmap.len,
+ event->mmap.pgoff);
+ err = delegate->mmap(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_mmap2(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ u8 cpumode;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_mmap2(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(aslr_machine, event->mmap2.pid, event->mmap2.tid);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->mmap2, &event->mmap2, event->mmap2.header.size);
+ /* Remaps the mmap.start. */
+ new_event->mmap2.start = aslr_tool__remap_mapping(aslr, thread, cpumode,
+ event->mmap2.start,
+ event->mmap2.len,
+ event->mmap2.pgoff);
+ err = delegate->mmap2(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_comm(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_comm(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ return delegate->comm(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_fork(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_fork(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ return delegate->fork(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_exit(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_exit(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ return delegate->exit(delegate, event, sample, machine);
+}
+
+static int aslr_tool__process_text_poke(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ u8 cpumode;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = event->header.misc & PERF_RECORD_MISC_CPUMODE_MASK;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ thread = machine__findnew_thread(aslr_machine, sample->pid, sample->tid);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->text_poke, &event->text_poke, event->text_poke.header.size);
+ new_event->text_poke.addr = aslr_tool__remap_address(aslr, thread, cpumode,
+ event->text_poke.addr);
+
+ err = delegate->text_poke(delegate, new_event, sample, machine);
+
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_ksymbol(const struct perf_tool *tool,
+ union perf_event *event,
+ struct perf_sample *sample,
+ struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ int err;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ /* Create the thread, map, etc. in the ASLR before virtual address space. */
+ err = perf_event__process_ksymbol(tool, event, sample, aslr_machine);
+ if (err)
+ return err;
+
+ thread = machine__findnew_thread(aslr_machine, kernel_pid, 0);
+ if (!thread)
+ return -ENOMEM;
+ memcpy(&new_event->ksymbol, &event->ksymbol, event->ksymbol.header.size);
+ /* Remaps the ksymbol.start */
+ new_event->ksymbol.addr = aslr_tool__remap_ksymbol(aslr, thread,
+ event->ksymbol.addr, event->ksymbol.len);
+
+ err = delegate->ksymbol(delegate, new_event, sample, machine);
+ thread__put(thread);
+ return err;
+}
+
+static int aslr_tool__process_sample(const struct perf_tool *tool, union perf_event *event,
+ struct perf_sample *sample,
+ struct evsel *evsel, struct machine *machine)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ int ret;
+ u64 sample_type;
+ struct thread *thread;
+ struct machine *aslr_machine;
+ __u64 max_i;
+ __u64 max_j;
+ union perf_event *new_event;
+ struct perf_sample new_sample;
+ __u64 *in_array, *out_array;
+ u8 cpumode;
+ u64 addr;
+ size_t i;
+ size_t j;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ ret = -EFAULT;
+ sample_type = evsel->core.attr.sample_type;
+ max_i = (event->header.size - sizeof(struct perf_event_header)) / sizeof(__u64);
+ max_j = (PERF_SAMPLE_MAX_SIZE - sizeof(struct perf_event_header)) / sizeof(__u64);
+ new_event = (union perf_event *)aslr->event_copy;
+ cpumode = sample->cpumode;
+ i = 0;
+ j = 0;
+
+ aslr_machine = machines__findnew(&aslr->machines, machine->pid);
+ if (!aslr_machine)
+ return -ENOMEM;
+
+ thread = machine__findnew_thread(aslr_machine, sample->pid, sample->tid);
+
+ if (!thread)
+ return -ENOMEM;
+
+ if (max_i > PERF_SAMPLE_MAX_SIZE / sizeof(u64))
+ goto out_put;
+
+
+
+ new_event->sample.header = event->sample.header;
+
+ in_array = &event->sample.array[0];
+ out_array = &new_event->sample.array[0];
+
+#define CHECK_BOUNDS(required_i, required_j) \
+ do { \
+ if (i + (required_i) > max_i || j + (required_j) > max_j) { \
+ ret = -EFAULT; \
+ goto out_put; \
+ } \
+ } while (0)
+
+#define COPY_U64() \
+ do { \
+ CHECK_BOUNDS(1, 1); \
+ out_array[j++] = in_array[i++]; \
+ } while (0)
+
+#define REMAP_U64(addr_field) \
+ do { \
+ CHECK_BOUNDS(1, 1); \
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode, addr_field); \
+ i++; \
+ } while (0)
+
+ if (sample_type & PERF_SAMPLE_IDENTIFIER)
+ COPY_U64(); /* id */
+ if (sample_type & PERF_SAMPLE_IP)
+ REMAP_U64(sample->ip);
+ if (sample_type & PERF_SAMPLE_TID)
+ COPY_U64(); /* pid, tid */
+ if (sample_type & PERF_SAMPLE_TIME)
+ COPY_U64(); /* time */
+ if (sample_type & PERF_SAMPLE_ADDR)
+ REMAP_U64(sample->addr);
+ if (sample_type & PERF_SAMPLE_ID)
+ COPY_U64(); /* id */
+ if (sample_type & PERF_SAMPLE_STREAM_ID)
+ COPY_U64(); /* stream_id */
+ if (sample_type & PERF_SAMPLE_CPU)
+ COPY_U64(); /* cpu, res */
+ if (sample_type & PERF_SAMPLE_PERIOD)
+ COPY_U64(); /* period */
+ if (sample_type & PERF_SAMPLE_READ) {
+ if ((evsel->core.attr.read_format & PERF_FORMAT_GROUP) == 0) {
+ COPY_U64(); /* value */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ COPY_U64(); /* time_enabled */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ COPY_U64(); /* time_running */
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ COPY_U64(); /* id */
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ COPY_U64(); /* lost */
+ } else {
+ u64 nr;
+
+ CHECK_BOUNDS(1, 1);
+ nr = out_array[j++] = in_array[i++];
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_ENABLED)
+ COPY_U64(); /* time_enabled */
+ if (evsel->core.attr.read_format & PERF_FORMAT_TOTAL_TIME_RUNNING)
+ COPY_U64(); /* time_running */
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ COPY_U64(); /* value */
+ if (evsel->core.attr.read_format & PERF_FORMAT_ID)
+ COPY_U64(); /* id */
+ if (evsel->core.attr.read_format & PERF_FORMAT_LOST)
+ COPY_U64(); /* lost */
+ }
+ }
+ }
+ if (sample_type & PERF_SAMPLE_CALLCHAIN) {
+ u64 nr;
+
+ CHECK_BOUNDS(1, 1);
+ nr = out_array[j++] = in_array[i++];
+
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ CHECK_BOUNDS(1, 1);
+ addr = in_array[i++];
+ if (addr >= PERF_CONTEXT_MAX) {
+ out_array[j++] = addr;
+ switch (addr) {
+ case PERF_CONTEXT_HV:
+ cpumode = PERF_RECORD_MISC_HYPERVISOR;
+ break;
+ case PERF_CONTEXT_KERNEL:
+ cpumode = PERF_RECORD_MISC_KERNEL;
+ break;
+ case PERF_CONTEXT_USER:
+ cpumode = PERF_RECORD_MISC_USER;
+ break;
+ case PERF_CONTEXT_GUEST:
+ cpumode = PERF_RECORD_MISC_GUEST_KERNEL;
+ break;
+ case PERF_CONTEXT_GUEST_KERNEL:
+ cpumode = PERF_RECORD_MISC_GUEST_KERNEL;
+ break;
+ case PERF_CONTEXT_GUEST_USER:
+ cpumode = PERF_RECORD_MISC_GUEST_USER;
+ break;
+ case PERF_CONTEXT_USER_DEFERRED:
+ /*
+ * Immediately followed by a 64-bit
+ * stitching cookie. Skip/Copy it!
+ */
+ CHECK_BOUNDS(1, 1);
+ out_array[j++] = in_array[i++];
+ cntr++;
+ break;
+ default:
+ pr_debug("invalid callchain context: %"PRIx64"\n", addr);
+ ret = 0;
+ goto out_put;
+ }
+ continue;
+ }
+ out_array[j++] = aslr_tool__remap_address(aslr, thread, cpumode, addr);
+ }
+ }
+ if (sample_type & PERF_SAMPLE_RAW) {
+ size_t bytes = sizeof(u32) + sample->raw_size;
+ size_t u64_words = (bytes + 7) / 8;
+
+ if (i + u64_words > max_i || j + u64_words > max_j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], bytes);
+ i += u64_words;
+ j += u64_words;
+ /*
+ * TODO: certain raw samples can be remapped, such as
+ * tracepoints by examining their fields.
+ */
+ pr_debug("Dropping raw samples as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_BRANCH_STACK) {
+ u64 nr;
+
+ CHECK_BOUNDS(1, 1);
+ nr = out_array[j++] = in_array[i++];
+
+ if (evsel->core.attr.branch_sample_type & PERF_SAMPLE_BRANCH_HW_INDEX)
+ COPY_U64(); /* hw_idx */
+
+ if (nr > (ULLONG_MAX / 3)) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ if (nr * 3 > max_i - i || nr * 3 > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ for (u64 cntr = 0; cntr < nr; cntr++) {
+ out_array[j++] = aslr_tool__remap_address(aslr, thread,
+ sample->cpumode,
+ in_array[i++]); /* from */
+ out_array[j++] = aslr_tool__remap_address(aslr, thread,
+ sample->cpumode,
+ in_array[i++]); /* to */
+ out_array[j++] = in_array[i++]; /* flags */
+ }
+ if (evsel->core.attr.branch_sample_type & PERF_SAMPLE_BRANCH_COUNTERS) {
+ if (nr > max_i - i || nr > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ /* TODO: confirm branch counters don't leak ASLR information. */
+ pr_debug("Dropping sample branch counters as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ }
+ if (sample_type & PERF_SAMPLE_REGS_USER) {
+ u64 abi;
+
+ COPY_U64(); /* abi */
+ abi = out_array[j-1];
+ if (abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_user);
+
+ if (nr > max_i - i || nr > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping regs user sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_STACK_USER) {
+ u64 size;
+
+ CHECK_BOUNDS(1, 1);
+ size = out_array[j++] = in_array[i++];
+ if (size > 0) {
+ size_t u64_words = size / 8 + (size % 8 ? 1 : 0);
+
+ if (u64_words > max_i - i || u64_words > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], size);
+ if (size % 8) {
+ size_t pad = 8 - (size % 8);
+
+ memset(((char *)&out_array[j]) + size, 0, pad);
+ }
+ i += u64_words;
+ j += u64_words;
+
+ COPY_U64(); /* dyn_size */
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping stack user sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_WEIGHT_TYPE)
+ COPY_U64(); /* perf_sample_weight */
+ if (sample_type & PERF_SAMPLE_DATA_SRC)
+ COPY_U64(); /* data_src */
+ if (sample_type & PERF_SAMPLE_TRANSACTION)
+ COPY_U64(); /* transaction */
+ if (sample_type & PERF_SAMPLE_REGS_INTR) {
+ u64 abi;
+
+ COPY_U64(); /* abi */
+ abi = out_array[j-1];
+ if (abi != PERF_SAMPLE_REGS_ABI_NONE) {
+ u64 nr = hweight64(evsel->core.attr.sample_regs_intr);
+
+ if (nr > max_i - i || nr > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
+ i += nr;
+ j += nr;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping interrupt register sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_PHYS_ADDR) {
+ COPY_U64(); /* phys_addr */
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping physical address sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+ if (sample_type & PERF_SAMPLE_CGROUP)
+ COPY_U64(); /* cgroup */
+ if (sample_type & PERF_SAMPLE_DATA_PAGE_SIZE)
+ COPY_U64(); /* data_page_size */
+ if (sample_type & PERF_SAMPLE_CODE_PAGE_SIZE)
+ COPY_U64(); /* code_page_size */
+
+ if (sample_type & PERF_SAMPLE_AUX) {
+ u64 size;
+
+ CHECK_BOUNDS(1, 1);
+ size = out_array[j++] = in_array[i++];
+ if (size > 0) {
+ size_t u64_words = size / 8 + (size % 8 ? 1 : 0);
+
+ if (u64_words > max_i - i || u64_words > max_j - j) {
+ ret = -EFAULT;
+ goto out_put;
+ }
+ memcpy(&out_array[j], &in_array[i], size);
+ if (size % 8) {
+ size_t pad = 8 - (size % 8);
+
+ memset(((char *)&out_array[j]) + size, 0, pad);
+ }
+ i += u64_words;
+ j += u64_words;
+ }
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping aux sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+
+ if (evsel__is_offcpu_event(evsel)) {
+ /* TODO: can this be less conservative? */
+ pr_debug("Dropping off-CPU sample as possible ASLR leak\n");
+ ret = 0;
+ goto out_put;
+ }
+
+ new_event->sample.header.size = sizeof(struct perf_event_header) + j * sizeof(u64);
+
+ perf_sample__init(&new_sample, /*all=*/ true);
+ ret = evsel__parse_sample(evsel, new_event, &new_sample);
+ if (ret) {
+ perf_sample__exit(&new_sample);
+ goto out_put;
+ }
+
+ ret = delegate->sample(delegate, new_event, &new_sample, evsel, machine);
+ perf_sample__exit(&new_sample);
+
+out_put:
+ thread__put(thread);
+ return ret;
+}
+
+#undef CHECK_BOUNDS
+#undef COPY_U64
+#undef REMAP_U64
+
+
+static int aslr_tool__process_attr(const struct perf_tool *tool,
+ union perf_event *event,
+ struct evlist **pevlist)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct perf_tool *delegate;
+ union perf_event *new_event;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+ delegate = aslr->tool.delegate;
+ new_event = (union perf_event *)aslr->event_copy;
+
+ memcpy(&new_event->attr, &event->attr, event->attr.header.size);
+ if (new_event->attr.attr.type == PERF_TYPE_BREAKPOINT)
+ new_event->attr.attr.bp_addr = 0; /* Conservatively remove addresses. */
+
+ return delegate->attr(delegate, new_event, pevlist);
+}
+
+static s64 aslr_tool__process_auxtrace(const struct perf_tool *tool __maybe_unused,
+ struct perf_session *session,
+ union perf_event *event)
+{
+ if (perf_data__is_pipe(session->data)) {
+ int err = skipn(perf_data__fd(session->data), event->auxtrace.size);
+
+ if (err < 0)
+ return err;
+ }
+ return event->auxtrace.size;
+}
+
+static int aslr_tool__process_auxtrace_info(const struct perf_tool *tool __maybe_unused,
+ struct perf_session *session __maybe_unused,
+ union perf_event *event __maybe_unused)
+{
+ return 0;
+}
+
+static int aslr_tool__process_auxtrace_error(const struct perf_tool *tool __maybe_unused,
+ struct perf_session *session __maybe_unused,
+ union perf_event *event __maybe_unused)
+{
+ return 0;
+}
+
+static void aslr_tool__init(struct aslr_tool *aslr, struct perf_tool *delegate)
+{
+ delegate_tool__init(&aslr->tool, delegate);
+ aslr->tool.tool.ordered_events = true;
+
+ machines__init(&aslr->machines);
+
+ hashmap__init(&aslr->remap_addresses,
+ remap_addresses__hash, remap_addresses__equal,
+ /*ctx=*/NULL);
+ hashmap__init(&aslr->top_addresses,
+ top_addresses__hash, top_addresses__equal,
+ /*ctx=*/NULL);
+
+ aslr->tool.tool.sample = aslr_tool__process_sample;
+ /* read - reads a counter, okay to delegate. */
+ aslr->tool.tool.mmap = aslr_tool__process_mmap;
+ aslr->tool.tool.mmap2 = aslr_tool__process_mmap2;
+ aslr->tool.tool.comm = aslr_tool__process_comm;
+ aslr->tool.tool.fork = aslr_tool__process_fork;
+ aslr->tool.tool.exit = aslr_tool__process_exit;
+ /* namesspaces, cgroup, lost, lost_sample, aux, */
+ /* itrace_start, aux_output_hw_id, context_switch, throttle, unthrottle */
+ /* - no virtual addresses. */
+ aslr->tool.tool.ksymbol = aslr_tool__process_ksymbol;
+ /* bpf - no virtual address. */
+ aslr->tool.tool.text_poke = aslr_tool__process_text_poke;
+ aslr->tool.tool.attr = aslr_tool__process_attr;
+ /* event_update, tracing_data, finished_round, build_id, id_index, */
+ /* event_update, tracing_data, finished_round, build_id, id_index, */
+ /* auxtrace_info, auxtrace_error, time_conv, thread_map, cpu_map, */
+ /* stat_config, stat, feature, finished_init, bpf_metadata, compressed, */
+ /* auxtrace - no virtual addresses. */
+ aslr->tool.tool.auxtrace = aslr_tool__process_auxtrace;
+ aslr->tool.tool.auxtrace_info = aslr_tool__process_auxtrace_info;
+ aslr->tool.tool.auxtrace_error = aslr_tool__process_auxtrace_error;
+}
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate)
+{
+ struct aslr_tool *aslr = zalloc(sizeof(*aslr));
+
+ if (!aslr)
+ return NULL;
+
+ aslr_tool__init(aslr, delegate);
+ return &aslr->tool.tool;
+}
+
+void aslr_tool__delete(struct perf_tool *tool)
+{
+ struct delegate_tool *del_tool;
+ struct aslr_tool *aslr;
+ struct hashmap_entry *cur;
+ size_t bkt;
+
+ if (!tool)
+ return;
+
+ del_tool = container_of(tool, struct delegate_tool, tool);
+ aslr = container_of(del_tool, struct aslr_tool, tool);
+
+ hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
+ struct remap_addresses_key *key = (struct remap_addresses_key *)cur->pkey;
+
+ if (key)
+ dso__put(key->dso);
+ zfree(&cur->pkey);
+ zfree(&cur->pvalue);
+ }
+ hashmap__for_each_entry(&aslr->top_addresses, cur, bkt) {
+ zfree(&cur->pvalue);
+ }
+
+ hashmap__clear(&aslr->remap_addresses);
+ hashmap__clear(&aslr->top_addresses);
+ machines__exit(&aslr->machines);
+ free(aslr);
+}
diff --git a/tools/perf/util/aslr.h b/tools/perf/util/aslr.h
new file mode 100644
index 000000000000..ea984d82681f
--- /dev/null
+++ b/tools/perf/util/aslr.h
@@ -0,0 +1,10 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PERF_ASLR_H
+#define __PERF_ASLR_H
+
+struct perf_tool;
+
+struct perf_tool *aslr_tool__new(struct perf_tool *delegate);
+void aslr_tool__delete(struct perf_tool *aslr);
+
+#endif /* __PERF_ASLR_H */
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* [PATCH v4 4/4] perf test: Add inject ASLR test
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (2 preceding siblings ...)
2026-05-04 7:29 ` [PATCH v4 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-05-04 7:29 ` Ian Rogers
2026-05-04 8:23 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 7:29 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz, Ian Rogers
Add a new shell test `inject_aslr.sh` to verify the `perf inject --aslr`
feature. The test covers:
- Basic address remapping for user space samples.
- Pipe mode coverage for `perf record` piped into `perf inject --aslr`.
- Callchain address remapping.
- Consistency of `perf report` output before and after injection.
- Pipe mode report consistency.
- Dropping of samples that leak ASLR info (physical addresses).
- Kernel address remapping (skipping gracefully if permissions restrict
recording the kernel map).
- Kernel report consistency with address normalization.
The test suite is hardened with:
- Global 'set -o pipefail' pipeline checks to catch failures in perf
script.
- Safe awk processing loop closures that consume whole streams to avoid
SIGPIPE signal aborts.
- False success assertions to verify callchain data isn't dynamically
dropped.
- Graceful error paths on empty sample records.
- Multi-arch support (32-bit and 64-bit address normalizations).
- Adds a new 'test_pipe_out_report_aslr' pipeline validation scenario
testing raw 'perf inject --aslr -o -' pipe stdout generation to verify
attribute repipe wrapper stability.
Assisted-by: Gemini-CLI:Google Gemini 3
Signed-off-by: Ian Rogers <irogers@google.com>
---
v4: Reorder set -e/pipefail to prevent temp file leakage in root directory on
unprivileged record failures when run as root. Ensure grep report filters
have || true suffixes to avoid aborts under pipefail. Add comprehensive
pipe stdout injection attributes validation case.
v3: Harden script with pipefail, SIGPIPE awk pipeline fixes, callchain empty
data asserts, baseline sample verification, and grep report abort
protections. Reorder set -e/pipefail to prevent stack leaks in mktemp
failures.
v2: Add sum comparison for kernel overhead and 32-bit math corrections. Add
awk with gsub for trailing dots and brackets normalizations. Trap EXIT,
prevent race conditions and avoid hardcoded perf binary.
---
tools/perf/tests/shell/inject_aslr.sh | 459 ++++++++++++++++++++++++++
1 file changed, 459 insertions(+)
create mode 100755 tools/perf/tests/shell/inject_aslr.sh
diff --git a/tools/perf/tests/shell/inject_aslr.sh b/tools/perf/tests/shell/inject_aslr.sh
new file mode 100755
index 000000000000..80ec9bf3daf8
--- /dev/null
+++ b/tools/perf/tests/shell/inject_aslr.sh
@@ -0,0 +1,459 @@
+#!/bin/bash
+# perf inject --aslr test
+# SPDX-License-Identifier: GPL-2.0
+
+set -e
+set -o pipefail
+
+shelldir=$(dirname "$0")
+# shellcheck source=lib/perf_has_symbol.sh
+. "${shelldir}"/lib/perf_has_symbol.sh
+
+sym="noploop"
+
+skip_test_missing_symbol ${sym}
+
+# Create global temp directory
+temp_dir=$(mktemp -d /tmp/perf-test-aslr.XXXXXXXXXX)
+data="${temp_dir}/perf.data"
+data2="${temp_dir}/perf.data2"
+
+prog="perf test -w noploop"
+[ "$(uname -m)" = "s390x" ] && prog="$prog 3"
+err=0
+
+
+
+cleanup() {
+ # Check if temp_dir is set and looks sane before removing
+ if [[ "${temp_dir}" =~ ^/tmp/perf-test-aslr\. ]]; then
+ rm -rf "${temp_dir}"
+ fi
+}
+
+trap_cleanup() {
+ cleanup
+ exit 1
+}
+
+trap cleanup EXIT
+trap trap_cleanup TERM INT
+
+get_noploop_addr() {
+ local file=$1
+ perf script -i "$file" | awk '
+ BEGIN { found=0 }
+ {
+ for (i=1; i<=NF; i++) {
+ if ($i ~ /noploop\+/) {
+ if (!found) {
+ print $(i-1)
+ found=1
+ }
+ }
+ }
+ }'
+}
+
+test_basic_aslr() {
+ echo "Test basic ASLR remapping"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.basic.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.basic.XXXXXX")
+
+ perf record -e task-clock:u -o "${data}" ${prog}
+ perf inject -v --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Basic ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Basic ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Basic ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Basic ASLR test [Success]"
+ fi
+}
+
+test_pipe_aslr() {
+ echo "Test pipe mode ASLR remapping"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.pipe.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.pipe.XXXXXX")
+
+ # Use tee to save the original pipe data for comparison
+ perf record -e task-clock:u -o - ${prog} | tee "${data}" | perf inject --aslr -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Pipe ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Pipe ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Pipe ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Pipe ASLR test [Success]"
+ fi
+}
+
+test_callchain_aslr() {
+ echo "Test Callchain ASLR remapping"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.callchain.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.callchain.XXXXXX")
+
+ perf record -g -e task-clock:u -o "${data}" ${prog}
+ perf inject --aslr -i "${data}" -o "${data2}"
+
+ orig_addr=$(get_noploop_addr "${data}")
+ new_addr=$(get_noploop_addr "${data2}")
+
+ echo "Callchain ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Callchain ASLR test [Failed - no noploop samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Callchain ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ # Extract callchain addresses (indented lines starting with hex addresses)
+ orig_callchain=$(perf script -i "${data}" | awk '/^[[:space:]]+[0-9a-f]+/ {print $1}')
+ new_callchain=$(perf script -i "${data2}" | awk '/^[[:space:]]+[0-9a-f]+/ {print $1}')
+
+ if [ -z "$orig_callchain" ]; then
+ echo "Callchain ASLR test [Failed - no callchain samples in original file]"
+ err=1
+ elif [ -z "$new_callchain" ]; then
+ echo "Callchain ASLR test [Failed - callchain data was dropped]"
+ err=1
+ elif [ "$orig_callchain" = "$new_callchain" ]; then
+ echo "Callchain ASLR test [Failed - callchain addresses were not remapped]"
+ err=1
+ else
+ echo "Callchain ASLR test [Success]"
+ fi
+ fi
+}
+
+test_report_aslr() {
+ echo "Test perf report consistency"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.report.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.report.XXXXXX")
+ local data_clean
+ data_clean=$(mktemp "${temp_dir}/perf.data.clean.XXXXXX")
+
+ perf record -e task-clock:u -o "${data}" ${prog}
+ # Use -b to inject build-ids and force ordered events processing in both
+ perf inject -b -i "${data}" -o "${data_clean}"
+ perf inject -v -b --aslr -i "${data}" -o "${data2}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}" || true
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ ! -s "${report1_clean}" ]; then
+ echo "Report ASLR test [Failed - no samples captured]"
+ err=1
+ elif [ -s "${diff_file}" ]; then
+ echo "Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Report ASLR test [Success]"
+ fi
+}
+
+test_pipe_report_aslr() {
+ echo "Test pipe mode perf report consistency"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.pipe_report.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.pipe_report.XXXXXX")
+ local data_clean
+ data_clean=$(mktemp "${temp_dir}/perf.data.clean.XXXXXX")
+
+ # Use tee to save the original pipe data, then process it with inject -b
+ perf record -e task-clock:u -o - ${prog} | \
+ tee "${data}" | \
+ perf inject -b --aslr -o "${data2}"
+ perf inject -b -i "${data}" -o "${data_clean}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${data2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}" || true
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ ! -s "${report1_clean}" ]; then
+ echo "Pipe Report ASLR test [Failed - no samples captured]"
+ err=1
+ elif [ -s "${diff_file}" ]; then
+ echo "Pipe Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Pipe Report ASLR test [Success]"
+ fi
+}
+
+test_pipe_out_report_aslr() {
+ echo "Test pipe output mode perf report consistency"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.pipe_out_report.XXXXXX")
+ local data_clean
+ data_clean=$(mktemp "${temp_dir}/perf.data.clean.XXXXXX")
+
+ perf record -e task-clock:u -o "${data}" ${prog}
+ perf inject -b -i "${data}" -o "${data_clean}"
+
+ local report1="${temp_dir}/report1"
+ local report2="${temp_dir}/report2"
+ local report1_clean="${temp_dir}/report1.clean"
+ local report2_clean="${temp_dir}/report2.clean"
+ local diff_file="${temp_dir}/diff"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf inject -b --aslr -i "${data}" -o - | perf report -i - --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' | sort > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' | sort > "${report2_clean}" || true
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ ! -s "${report1_clean}" ]; then
+ echo "Pipe Output Report ASLR test [Failed - no samples captured]"
+ err=1
+ elif [ -s "${diff_file}" ]; then
+ echo "Pipe Output Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Pipe Output Report ASLR test [Success]"
+ fi
+}
+
+test_dropped_samples() {
+ echo "Test dropped samples (phys-data)"
+ local data
+ data=$(mktemp "${temp_dir}/perf.data.dropped.XXXXXX")
+ local data2
+ data2=$(mktemp "${temp_dir}/perf.data2.dropped.XXXXXX")
+
+ # Check if --phys-data is supported by recording a short run
+ if ! perf record -e task-clock:u --phys-data -o "${data}" -- sleep 0.1 > /dev/null 2>&1; then
+ echo "Skipping dropped samples test as --phys-data is not supported"
+ return
+ fi
+
+ perf record -e task-clock:u --phys-data -o "${data}" ${prog}
+ perf inject --aslr -i "${data}" -o "${data2}"
+
+ # Verify that the original file actually contained samples!
+ orig_samples=$(perf script -i "${data}" | wc -l)
+ if [ "$orig_samples" -eq 0 ]; then
+ echo "Dropped samples test [Failed - no samples in original file]"
+ err=1
+ else
+ # Verify that samples are dropped.
+ samples_count=$(perf script -i "${data2}" | wc -l)
+
+ if [ "$samples_count" -gt 0 ]; then
+ echo "Dropped samples test [Failed - samples were not dropped]"
+ err=1
+ else
+ echo "Dropped samples test [Success]"
+ fi
+ fi
+}
+
+test_kernel_aslr() {
+ echo "Test kernel ASLR remapping"
+ local kdata
+ kdata=$(mktemp "${temp_dir}/perf.data.kernel.XXXXXX")
+ local kdata2
+ kdata2=$(mktemp "${temp_dir}/perf.data2.kernel.XXXXXX")
+ local log_file
+ log_file=$(mktemp "${temp_dir}/kernel_record.log.XXXXXX")
+
+ # Try to record kernel samples
+ if ! perf record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel ASLR test as recording failed (maybe no permissions)"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel ASLR test as kernel map could not be recorded (permissions restricted)"
+ return
+ fi
+
+ perf inject -v --aslr -i "${kdata}" -o "${kdata2}"
+
+ # Check if kernel addresses are remapped.
+ # Find the field that ends with :k: (the event name) and take the next field!
+ orig_addr=$(perf script -i "${kdata}" | awk '
+ BEGIN { found=0 }
+ {
+ for (i=1; i<NF; i++) {
+ if ($i ~ /:[k]+:?$/) {
+ if (!found) {
+ print $(i+1)
+ found=1
+ }
+ }
+ }
+ }')
+ new_addr=$(perf script -i "${kdata2}" | awk '
+ BEGIN { found=0 }
+ {
+ for (i=1; i<NF; i++) {
+ if ($i ~ /:[k]+:?$/) {
+ if (!found) {
+ print $(i+1)
+ found=1
+ }
+ }
+ }
+ }')
+
+ echo "Kernel ASLR: orig_addr=$orig_addr, new_addr=$new_addr"
+
+ if [ -z "$orig_addr" ]; then
+ echo "Kernel ASLR test [Failed - no kernel samples in original file]"
+ err=1
+ elif [ -z "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - could not find remapped address]"
+ err=1
+ elif [ "$orig_addr" = "$new_addr" ]; then
+ echo "Kernel ASLR test [Failed - addresses are not remapped]"
+ err=1
+ else
+ echo "Kernel ASLR test [Success]"
+ fi
+}
+
+test_kernel_report_aslr() {
+ echo "Test kernel perf report consistency"
+ local kdata
+ kdata=$(mktemp "${temp_dir}/perf.data.kernel_report.XXXXXX")
+ local kdata2
+ kdata2=$(mktemp "${temp_dir}/perf.data2.kernel_report.XXXXXX")
+ local data_clean
+ data_clean=$(mktemp "${temp_dir}/perf.data.clean.XXXXXX")
+ local log_file
+ log_file=$(mktemp "${temp_dir}/kernel_report_record.log.XXXXXX")
+
+ # Try to record kernel samples
+ if ! perf record -e task-clock:k -o "${kdata}" ${prog} > "${log_file}" 2>&1; then
+ echo "Skipping kernel report test as recording failed (maybe no permissions)"
+ return
+ fi
+
+ # Check for warning about kernel map restriction
+ if grep -q "Couldn't record kernel reference relocation symbol" "${log_file}"; then
+ echo "Skipping kernel report test as kernel map could not be recorded (permissions restricted)"
+ return
+ fi
+
+ # Use -b to inject build-ids and force ordered events processing in both
+ perf inject -b -i "${kdata}" -o "${data_clean}"
+ perf inject -v -b --aslr -i "${kdata}" -o "${kdata2}"
+
+ local report1="${temp_dir}/report_kernel1"
+ local report2="${temp_dir}/report_kernel2"
+ local report1_clean="${temp_dir}/report_kernel1.clean"
+ local report2_clean="${temp_dir}/report_kernel2.clean"
+
+ perf report -i "${data_clean}" --stdio > "${report1}"
+ perf report -i "${kdata2}" --stdio > "${report2}"
+
+ # Strip headers and compare lines with percentages
+ grep '%' "${report1}" | grep -v '^#' > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' > "${report2_clean}" || true
+
+ # Normalize kernel DSOs and addresses in clean reports
+ # This allows kernel modules to be either a module or kernel.kallsyms
+ local report1_norm="${temp_dir}/report_kernel1.norm"
+ local report2_norm="${temp_dir}/report_kernel2.norm"
+ local diff_file="${temp_dir}/diff_kernel"
+
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); \
+ print}' "${report1_clean}" | sort > "${report1_norm}"
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); \
+ print}' "${report2_clean}" | sort > "${report2_norm}"
+
+ diff -u -w "${report1_norm}" "${report2_norm}" > "${diff_file}" || true
+
+ if [ ! -s "${report1_norm}" ]; then
+ echo "Kernel Report ASLR test [Failed - no samples captured]"
+ err=1
+ elif [ -s "${diff_file}" ]; then
+ echo "Kernel Report ASLR test [Failed - reports differ]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ else
+ echo "Kernel Report ASLR test [Success]"
+ fi
+}
+
+test_basic_aslr
+test_pipe_aslr
+test_callchain_aslr
+test_report_aslr
+test_pipe_report_aslr
+test_pipe_out_report_aslr
+test_dropped_samples
+test_kernel_aslr
+test_kernel_report_aslr
+
+cleanup
+exit $err
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 15+ messages in thread* Re: [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (3 preceding siblings ...)
2026-05-04 7:29 ` [PATCH v4 4/4] perf test: Add inject ASLR test Ian Rogers
@ 2026-05-04 8:23 ` Ian Rogers
4 siblings, 0 replies; 15+ messages in thread
From: Ian Rogers @ 2026-05-04 8:23 UTC (permalink / raw)
To: acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz
I found a regression when testing as root, so please ignore.
Thanks,
Ian
On Mon, May 4, 2026 at 12:29 AM Ian Rogers <irogers@google.com> wrote:
>
> This patch series introduces the new 'perf inject --aslr' feature to remap
> virtual memory addresses or drop physical memory event leaks when profile
> record data is shared between machines. Bundled with this feature are two
> independent, critical bug fixes inside core event dispatching tools that
> harden perf session analysis against dynamic crashes and callchain mapping
> failures.
>
> Core Feature: 'perf inject --aslr' (Patches 3 and 4)
>
> Transferring perf.data files across environments introduces a potential leak
> of virtual address footprints, weakening Address Space Layout Randomization
> (ASLR) on the originating machine. To mitigate this, we introduce the --aslr
> flag into perf inject. Unknown or unhandled events are dropped conservatively,
> while handled samples and branch loops undergo systematic virtual memory offset
> obfuscation.
>
> To ensure comprehensive memory and error-path safety, the ASLR tool implements:
> - Machine namespaces ('struct machines') to safely interleave host mappings and
> unprivileged KVM guest virtual address mappings.
> - Resolves VMA split map failures (caused by overlap fixups during map
> insertions) consistently by anchoring mappings on DSO and memory
> invariants.
> - Guards against integer overflows in branch stack loops via
> subtraction-based bounds arithmetic.
> - Prevents heap buffer overflows by computing safe word limits on
> userspace stacks and dynamic hardware tracing (AUX) sizes.
> - Prevents key collisions/ABA lookups by correctly managing DSO
> reference counts (dso__get/put).
> - Cleans up error paths to avoid inconsistent hashmap mappings on
> OOM failures.
> - Optimizes performance by removing redundant hot-path memory
> allocations.
> - Cleanly advances session readers past dropped auxtrace streams
> using pipe-stream I/O skip helpers.
> - Scrubs breakpoint addresses (bp_addr) from output event headers
> and dynamically synthesized events for pipes via a custom pipe
> repipe wrapper to prevent unscrubbed address leakage.
> - Remaps kernel memory maps linearly to maintain secure base
> obfuscation bounds.
> - Hardens guest cpumode lookups against corrupting host/guest user and
> kernel mapping boundaries during sample fallback searches.
> - Synchronizes ksymbol map tracking invariants using precise VMA
> offset math rather than raw addresses to prevent unique base leaks
> on every function symbol.
> - Blocks trailing heap padding byte data leakage vectors in userspace
> stacks and AUX tracking frames via targeted tail-word clearing.
>
> Verification is reinforced in Patch 4 with a new comprehensive POSIX shell
> suite ('inject_aslr.sh'), hardened against SIGPIPE signal exits with stream
> consuming awk loops and robust 'set -o pipefail' assertions. The suite includes
> a new dedicated scenario validating pipe stdout injection attribute stability.
>
> Prerequisite Bug Fixes (Patches 1 and 2)
>
> During development, two core event delegation issues were identified and
> resolved to prevent crashes and data-loss during analysis:
>
> 1. perf sched: 'timehist' registers standard MMAP, COMM, EXIT, and FORK stubs,
> but completely omitted registering MMAP2 callbacks. Because modern environments
> output maps primarily via MMAP2 frames, this caused timehist sessions to silently
> drop shared library mappings, causing dynamic callchain symbol resolutions to
> fail. Patch 1 corrects this by properly registering perf_event__process_mmap2.
>
> 2. perf tool: Patch 2 fixes missing copies of schedstat callbacks inside delegated
> wrapper tools (which caused segfaults on NULL stubs) and properly initializes/copies
> the 'dont_split_sample_group' grouping parameters to prevent stack garbage from
> triggering silent non-leader events drops during split deliver streams.
>
> Changes since v3:
> - Feature integration: Pass a dedicated 'perf_event__aslr_repipe' callback to
> perf_event__synthesize_for_pipe() to scrub synthesized breakpoint attributes.
> - Feature core: Loop through and scrub event evlist breakpoint attributes right
> before writing file headers in __cmd_inject().
> - Feature core: Linearize kernel map base obfuscation and remove redundant pgoff
> delta adjustments that leaked kernel layout calculations.
> - Feature core: Fix host/guest cpumode mappings in sample fallback lookups.
> - Feature core: Sync ksymbol tracking keys onto VMA offset invariants.
> - Feature core: Zero out trailing padding word bytes in user stacks and AUX blocks.
> - Validation suite: Add 'test_pipe_out_report_aslr' validation case.
> - Validation suite: Upgrade kernel report checks to strict sorted line-by-line diffs.
> - Style: Wrap all commit description lines to under 75 columns and fix code formatting.
>
> Ian Rogers (4):
> perf sched: Add missing mmap2 handler in timehist
> perf tool: Fix missing schedstat delegates and dont_split_sample_group
> in delegate_tool
> perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
> perf test: Add inject ASLR test
>
> tools/perf/builtin-inject.c | 52 +-
> tools/perf/builtin-sched.c | 1 +
> tools/perf/tests/shell/inject_aslr.sh | 459 ++++++++++
> tools/perf/util/Build | 1 +
> tools/perf/util/aslr.c | 1161 +++++++++++++++++++++++++
> tools/perf/util/aslr.h | 10 +
> tools/perf/util/tool.c | 6 +
> 7 files changed, 1689 insertions(+), 1 deletion(-)
> create mode 100755 tools/perf/tests/shell/inject_aslr.sh
> create mode 100644 tools/perf/util/aslr.c
> create mode 100644 tools/perf/util/aslr.h
>
> --
> 2.54.0.545.g6539524ca2-goog
>
^ permalink raw reply [flat|nested] 15+ messages in thread
end of thread, other threads:[~2026-05-04 8:23 UTC | newest]
Thread overview: 15+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-24 22:05 [PATCH v1 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-04-24 22:05 ` [PATCH v1 2/2] perf test: Add inject ASLR test Ian Rogers
2026-04-25 2:05 ` [PATCH v2 1/2] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-04-25 2:05 ` [PATCH v2 2/2] perf test: Add inject ASLR test Ian Rogers
2026-05-04 3:51 ` [PATCH v3 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-04 3:51 ` [PATCH v3 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-04 3:51 ` [PATCH v3 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
2026-05-04 3:51 ` [PATCH v3 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-05-04 3:51 ` [PATCH v3 4/4] perf test: Add inject ASLR test Ian Rogers
2026-05-04 7:29 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-04 7:29 ` [PATCH v4 1/4] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-04 7:29 ` [PATCH v4 2/4] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
2026-05-04 7:29 ` [PATCH v4 3/4] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-05-04 7:29 ` [PATCH v4 4/4] perf test: Add inject ASLR test Ian Rogers
2026-05-04 8:23 ` [PATCH v4 0/4] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox