* [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes
[not found] <20260504072937.2103453-1-irogers@google.com>
@ 2026-05-06 0:45 ` Ian Rogers
2026-05-06 0:45 ` [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist Ian Rogers
` (5 more replies)
0 siblings, 6 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 0:45 UTC (permalink / raw)
To: irogers, acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz
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 three
independent, critical bug fixes inside core event dispatching and map tracking
tools that harden perf session analysis against dynamic crashes and callchain
mapping failures.
Core Feature: 'perf inject --aslr' (Patches 4 and 5)
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.
The ASLR tracking tool virtualizes process and machine namespaces using
'struct machines' to safely isolate host mappings from unprivileged KVM guest
address spaces. Memory space layouts are tracked globally per process context to
ensure linear, continuous space allocations across successive mapping runs.
To remain strictly conservative and guarantee security, the tool scrubs
breakpoint addresses (bp_addr) from all synthesized stream headers, and drops
unsupported complex payloads (such as user register stacks, raw tracepoints,
and hardware AUX tracing frames) to completely eliminate accidental address
leakage vectors.
Verification is reinforced in Patch 5 with a 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 dedicated scenario validating raw 'perf inject -o -' pipe stdout generation
attribute stability.
Prerequisite Bug Fixes (Patches 1, 2, and 3)
During development, three core event delegation and map indexing 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.
3. perf symbols: Patch 3 resolves a deep structural map tracking desynchronization bug
inside symbol-elf.c by re-engineering the map removal sequence order to run
strictly BEFORE in-place virtual address mutations, preventing absolute binary
searches (bsearch) from failing on misaligned cache array slots.
Changes since v4:
- Core Bug Fix: Introduce a new prerequisite standalone fix patch (Patch 3) that
re-engineers map tracking removal sequence order inside symbol-elf.c to prevent
corrupting binary search index arrays during in-place address mutations.
- Feature Core: Refactor aslr_tool__delete to cleanly clear host/guest maps and
structures via machines__destroy_kernel_maps() to cure all destructor leaks.
- Feature Core: Integrate the 'first_kernel_mapping' state guard to protect
kernel module file offsets (pgoff) from corruption, preventing dynamic
symbolization resolutions dropouts.
- Feature Integration: Move breakpoint address (bp_addr) cleaning to the core
session memory initialization startup level, natively securing both files and
pipes while completely stripping away redundant runtime wrapper layers.
- Validation Suite: Harden grep-v filters with || true operators to protect pipelines
from crashing under set -o pipefail on empty inputs.
- Style: Prune out and streamline commit log text clutter into concise high-level
architectural summary overviews.
Ian Rogers (5):
perf sched: Add missing mmap2 handler in timehist
perf tool: Fix missing schedstat delegates and dont_split_sample_group
in delegate_tool
perf symbols: Fix map removal sequence inside
dso__process_kernel_symbol()
perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
perf test: Add inject ASLR test
tools/perf/builtin-inject.c | 31 +-
tools/perf/builtin-sched.c | 1 +
tools/perf/tests/shell/inject_aslr.sh | 459 ++++++++++
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1220 +++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
tools/perf/util/symbol-elf.c | 21 +-
tools/perf/util/tool.c | 6 +
8 files changed, 1743 insertions(+), 6 deletions(-)
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] 27+ messages in thread
* [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist
2026-05-06 0:45 ` [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
@ 2026-05-06 0:45 ` Ian Rogers
2026-05-06 13:22 ` Arnaldo Carvalho de Melo
2026-05-06 0:45 ` [PATCH v5 2/5] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
` (4 subsequent siblings)
5 siblings, 1 reply; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 0:45 UTC (permalink / raw)
To: irogers, acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz
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] 27+ messages in thread
* [PATCH v5 2/5] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool
2026-05-06 0:45 ` [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-06 0:45 ` [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist Ian Rogers
@ 2026-05-06 0:45 ` Ian Rogers
2026-05-06 0:45 ` [PATCH v5 3/5] perf symbols: Fix map removal sequence inside dso__process_kernel_symbol() Ian Rogers
` (3 subsequent siblings)
5 siblings, 0 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 0:45 UTC (permalink / raw)
To: irogers, acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz
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] 27+ messages in thread
* [PATCH v5 3/5] perf symbols: Fix map removal sequence inside dso__process_kernel_symbol()
2026-05-06 0:45 ` [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-06 0:45 ` [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-06 0:45 ` [PATCH v5 2/5] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
@ 2026-05-06 0:45 ` Ian Rogers
2026-05-06 0:45 ` [PATCH v5 4/5] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
` (2 subsequent siblings)
5 siblings, 0 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 0:45 UTC (permalink / raw)
To: irogers, acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz
When parsing vmlinux ELF binary symbols, dso__process_kernel_symbol()
mutates the map's start address key fields in place before executing
maps__remove(). This forces maps__by_address_index() to look up the
new mutated address range via strict binary search inside an array
interval that was ordered using the old unmutated boundaries, leading
to a bsearch() mismatch failure and leaking maps index errors.
Fix this natively by executing maps__remove() before mutating the
map fields in place, ensuring binary search maps queries always locate
and extract target elements flawlessly.
Fixes: 39b12f781271 ("perf tools: Make it possible to read object code from vmlinux")
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/symbol-elf.c | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
diff --git a/tools/perf/util/symbol-elf.c b/tools/perf/util/symbol-elf.c
index 7afa8a117139..f31d481a8627 100644
--- a/tools/perf/util/symbol-elf.c
+++ b/tools/perf/util/symbol-elf.c
@@ -1372,20 +1372,31 @@ static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
*/
if (*remap_kernel && dso__kernel(dso) && !kmodule) {
*remap_kernel = false;
- map__set_start(map, shdr->sh_addr + ref_reloc(kmap));
- map__set_end(map, map__start(map) + shdr->sh_size);
- map__set_pgoff(map, shdr->sh_offset);
- map__set_mapping_type(map, MAPPING_TYPE__DSO);
- /* Ensure maps are correctly ordered */
+ /*
+ * If the map is tracking inside the kmaps cache list array, we
+ * MUST remove it before mutating its virtual address key fields
+ * in place. Otherwise, downstream binary search lookups (bsearch)
+ * will search for mutated keys inside an array sorted under old
+ * invariants, causing indexing desynchronization faults.
+ */
if (kmaps) {
int err;
struct map *tmp = map__get(map);
maps__remove(kmaps, map);
+ map__set_start(map, shdr->sh_addr + ref_reloc(kmap));
+ map__set_end(map, map__start(map) + shdr->sh_size);
+ map__set_pgoff(map, shdr->sh_offset);
+ map__set_mapping_type(map, MAPPING_TYPE__DSO);
err = maps__insert(kmaps, map);
map__put(tmp);
if (err)
return err;
+ } else {
+ map__set_start(map, shdr->sh_addr + ref_reloc(kmap));
+ map__set_end(map, map__start(map) + shdr->sh_size);
+ map__set_pgoff(map, shdr->sh_offset);
+ map__set_mapping_type(map, MAPPING_TYPE__DSO);
}
}
--
2.54.0.545.g6539524ca2-goog
^ permalink raw reply related [flat|nested] 27+ messages in thread
* [PATCH v5 4/5] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-05-06 0:45 ` [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (2 preceding siblings ...)
2026-05-06 0:45 ` [PATCH v5 3/5] perf symbols: Fix map removal sequence inside dso__process_kernel_symbol() Ian Rogers
@ 2026-05-06 0:45 ` Ian Rogers
2026-05-06 18:52 ` Namhyung Kim
2026-05-06 0:45 ` [PATCH v5 5/5] perf test: Add inject ASLR test Ian Rogers
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
5 siblings, 1 reply; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 0:45 UTC (permalink / raw)
To: irogers, acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, peterz
If perf.data files are taken from one machine to another they may
leak virtual addresses and so weaken ASLR on the machine they are
coming from. Add an 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.
The ASLR tracking tool virtualizes process and machine namespaces using
'struct machines' to safely isolate host mappings from unprivileged KVM guest
address spaces. Memory layouts are tracked globally per process context to
ensure linear, continuous space allocations across successive mapping runs.
To remain strictly conservative and guarantee security, the tool scrubs
breakpoint addresses (bp_addr) from all synthesized stream headers, and drops
unsupported complex payloads (such as user register stacks, raw tracepoints,
and hardware AUX tracing frames) to completely eliminate accidental address
leakage vectors.
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>
---
v5: Fix memory leaks inside aslr_tool__delete destructor by calling standard
machines__destroy_kernel_maps() to cleanly free host/guest maps and guest
machine structures. Introduce the precise 'first_kernel_mapping' tracking
guard inside aslr.c to rewrite the core kernel pgoff virtual address while
safely protecting module file offsets from corruption. Harden skipn()
pipe I/O stream reader loops against EINTR interruption errors. Clean up
breakpoint address (bp_addr) memory scrubbing by executing the scrubbing loop
directly at core session initialization startup level, natively securing both
file headers and streaming pipe channels while removing redundant runtime
tool wrapper interception hooks layers.
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 | 31 +-
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1220 +++++++++++++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
4 files changed, 1261 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..8fe479cb4152 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,8 @@ static int perf_event__repipe(const struct perf_tool *tool,
return perf_event__repipe_synth(tool, event);
}
+
+
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 +2462,8 @@ static int __cmd_inject(struct perf_inject *inject)
}
}
+
+
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 +2570,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 +2579,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,18 +2693,36 @@ 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;
}
if (zstd_init(&(inject.session->zstd_data), 0) < 0)
pr_warning("Decompression initialization failed.\n");
+ if (inject.aslr) {
+ struct evsel *evsel;
+
+ evlist__for_each_entry(inject.session->evlist, evsel) {
+ if (evsel->core.attr.type == PERF_TYPE_BREAKPOINT)
+ evsel->core.attr.bp_addr = 0;
+ }
+ }
+
/* Save original section info before feature bits change */
ret = save_section_info(&inject);
if (ret)
@@ -2789,6 +2816,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..effdcbec0db0
--- /dev/null
+++ b/tools/perf/util/aslr.c
@@ -0,0 +1,1220 @@
+// 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) {
+ if (errno == EINTR)
+ continue;
+ return ret;
+ }
+ if (ret == 0)
+ return 0;
+ 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 RC_CHK_EQUAL(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 {
+ if (effective_cpumode == PERF_RECORD_MISC_KERNEL) {
+ struct hashmap_entry *cur;
+ size_t bkt;
+
+ hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
+ struct remap_addresses_key *k;
+ u64 *v;
+
+ k = (struct remap_addresses_key *)cur->pkey;
+ if (k->pid == kernel_pid &&
+ k->invariant == key.invariant) {
+ v = (u64 *)cur->pvalue;
+ remap_addr = *v + map__pgoff(al.map) +
+ (addr - map__start(al.map));
+ break;
+ }
+ }
+ }
+ if (remap_addr == 0) {
+ 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 addr_location prev_al;
+ struct remap_addresses_key key;
+ struct remap_addresses_key *new_key = NULL;
+ struct remap_addresses_key *old_key = NULL;
+ u64 remap_addr = 0;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 *max_addr_ptr = NULL;
+ u64 *new_val = NULL;
+ u64 *new_max = NULL;
+ u64 *old_val = NULL;
+ u64 *old_val_remap = NULL;
+ bool is_contiguous = false;
+ bool first_mapping = false;
+ bool key_found = false;
+ int err;
+
+ 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);
+ key.invariant = map__start(al.map) - map__pgoff(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 + (al.map ? map__pgoff(al.map) : pgoff);
+ key_found = true;
+ } else {
+ 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;
+
+ new_key = malloc(sizeof(*new_key));
+ 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 - (al.map ? map__pgoff(al.map) : 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 */
+ new_max = malloc(sizeof(u64));
+ old_val = NULL;
+
+ if (!new_max) {
+ old_key = NULL;
+ 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) {
+ old_key = NULL;
+ 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;
+ struct hashmap_entry *cur;
+ struct remap_addresses_key *new_key = NULL;
+ struct remap_addresses_key *old_key = NULL;
+ struct remap_addresses_key *k;
+ size_t bkt;
+ u64 remap_addr = 0;
+ u64 *remapped_invariant_ptr = NULL;
+ u64 *max_addr_ptr = NULL;
+ u64 *new_val = NULL;
+ u64 *new_max = NULL;
+ u64 *old_val = NULL;
+ u64 *old_val_remap = NULL;
+ u64 *v;
+ bool first_mapping = false;
+ int err;
+
+ 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;
+ }
+
+ hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
+ k = (struct remap_addresses_key *)cur->pkey;
+ if (k->pid == kernel_pid && k->invariant == key.invariant) {
+ v = (u64 *)cur->pvalue;
+
+ if (al.map)
+ remap_addr = *v + map__pgoff(al.map) +
+ (addr - map__start(al.map));
+ else
+ remap_addr = *v;
+ 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;
+
+ new_key = malloc(sizeof(*new_key));
+ 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;
+ }
+
+ new_max = malloc(sizeof(u64));
+ old_val = NULL;
+
+ if (!new_max) {
+ old_key = NULL;
+ 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) {
+ old_key = NULL;
+ 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) {
+ new_event->mmap.pgoff = 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) {
+ new_event->mmap2.pgoff = 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;
+
+ 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->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__destroy_kernel_maps(&aslr->machines);
+ 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] 27+ messages in thread
* [PATCH v5 5/5] perf test: Add inject ASLR test
2026-05-06 0:45 ` [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (3 preceding siblings ...)
2026-05-06 0:45 ` [PATCH v5 4/5] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-05-06 0:45 ` Ian Rogers
2026-05-07 15:58 ` James Clark
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
5 siblings, 1 reply; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 0:45 UTC (permalink / raw)
To: irogers, acme, gmx, namhyung
Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users,
mingo, 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.
The test suite is hardened with global 'set -o pipefail' assertions to catch
pipeline failures, stream-consuming awk processors to handle SIGPIPE signals,
and a dedicated pipe output scenario validating raw 'perf inject -o -' stdout
streams.
Assisted-by: Gemini-CLI:Google Gemini 3
Signed-off-by: Ian Rogers <irogers@google.com>
---
v5: Harden test suite verification pipelines by upgrading report checks to
strict sorted line-by-line diff comparisons to accommodate remapped pointer
shifts. Append || true fallback operators to grep-v filtering pipelines to
prevent the shell test from spuriously aborting under set -o pipefail on
empty inputs, ensuring graceful failure checks trigger correctly.
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..cdc3aa94de63
--- /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"
+
+ grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report1_clean}" | \
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report1_norm}" || true
+ grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report2_clean}" | \
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report2_norm}" || true
+
+ 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] 27+ messages in thread
* Re: [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist
2026-05-06 0:45 ` [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist Ian Rogers
@ 2026-05-06 13:22 ` Arnaldo Carvalho de Melo
2026-05-06 16:16 ` Ian Rogers
0 siblings, 1 reply; 27+ messages in thread
From: Arnaldo Carvalho de Melo @ 2026-05-06 13:22 UTC (permalink / raw)
To: Ian Rogers
Cc: gmx, namhyung, adrian.hunter, james.clark, jolsa, linux-kernel,
linux-perf-users, mingo, peterz
On Tue, May 05, 2026 at 05:45:42PM -0700, Ian Rogers wrote:
> 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")
I'm picking patches 1 and 2 as they're simple and passed sashiko review,
but:
⬢ [acme@toolbx perf-tools-next2]$ git show 5bbfec0ad93c
fatal: ambiguous argument '5bbfec0ad93c': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
⬢ [acme@toolbx perf-tools-next2]$
The right one is:
Fixes: 49394a2a24c78ce0 ("perf sched timehist: Introduce timehist command")
I checked and at the time, mmap2 was already present and it missed
adding this handler.
I fixed it, in case I'm missing something, lemme know,
- Arnaldo
> 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 [flat|nested] 27+ messages in thread
* Re: [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist
2026-05-06 13:22 ` Arnaldo Carvalho de Melo
@ 2026-05-06 16:16 ` Ian Rogers
0 siblings, 0 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 16:16 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo
Cc: gmx, namhyung, adrian.hunter, james.clark, jolsa, linux-kernel,
linux-perf-users, mingo, peterz
On Wed, May 6, 2026 at 6:22 AM Arnaldo Carvalho de Melo <acme@kernel.org> wrote:
>
> On Tue, May 05, 2026 at 05:45:42PM -0700, Ian Rogers wrote:
> > 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")
>
> I'm picking patches 1 and 2 as they're simple and passed sashiko review,
> but:
>
> ⬢ [acme@toolbx perf-tools-next2]$ git show 5bbfec0ad93c
> fatal: ambiguous argument '5bbfec0ad93c': unknown revision or path not in the working tree.
> Use '--' to separate paths from revisions, like this:
> 'git <command> [<revision>...] -- [<file>...]'
> ⬢ [acme@toolbx perf-tools-next2]$
>
> The right one is:
>
> Fixes: 49394a2a24c78ce0 ("perf sched timehist: Introduce timehist command")
>
> I checked and at the time, mmap2 was already present and it missed
> adding this handler.
>
> I fixed it, in case I'm missing something, lemme know,
Sounds good. I reworked patch3 because the remove/insert pattern was
potentially racy. Now, with the write lock held it mutates the map
entry in place and then marks the backing maps as being unsorted.
Sashiko reviews made me aware of about four instances of this
potential bug. I've also tweaked the ASLR code to make it more robust
against colliding host/guest VM PIDs. I'm running sashiko locally and
will post the next version when satisfied. For some reason the
external Sashiko seems to be catching different issues :-/
Thanks,
Ian
> - Arnaldo
>
> > 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 [flat|nested] 27+ messages in thread
* Re: [PATCH v5 4/5] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-05-06 0:45 ` [PATCH v5 4/5] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-05-06 18:52 ` Namhyung Kim
2026-05-06 20:01 ` Ian Rogers
0 siblings, 1 reply; 27+ messages in thread
From: Namhyung Kim @ 2026-05-06 18:52 UTC (permalink / raw)
To: Ian Rogers
Cc: acme, gmx, adrian.hunter, james.clark, jolsa, linux-kernel,
linux-perf-users, mingo, peterz
On Tue, May 05, 2026 at 05:45:45PM -0700, Ian Rogers wrote:
> If perf.data files are taken from one machine to another they may
> leak virtual addresses and so weaken ASLR on the machine they are
> coming from. Add an 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.
>
> The ASLR tracking tool virtualizes process and machine namespaces using
> 'struct machines' to safely isolate host mappings from unprivileged KVM guest
> address spaces. Memory layouts are tracked globally per process context to
> ensure linear, continuous space allocations across successive mapping runs.
>
> To remain strictly conservative and guarantee security, the tool scrubs
> breakpoint addresses (bp_addr) from all synthesized stream headers, and drops
> unsupported complex payloads (such as user register stacks, raw tracepoints,
> and hardware AUX tracing frames) to completely eliminate accidental address
> leakage vectors.
>
> 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>
> ---
> v5: Fix memory leaks inside aslr_tool__delete destructor by calling standard
> machines__destroy_kernel_maps() to cleanly free host/guest maps and guest
> machine structures. Introduce the precise 'first_kernel_mapping' tracking
> guard inside aslr.c to rewrite the core kernel pgoff virtual address while
> safely protecting module file offsets from corruption. Harden skipn()
> pipe I/O stream reader loops against EINTR interruption errors. Clean up
> breakpoint address (bp_addr) memory scrubbing by executing the scrubbing loop
> directly at core session initialization startup level, natively securing both
> file headers and streaming pipe channels while removing redundant runtime
> tool wrapper interception hooks layers.
> 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 | 31 +-
> tools/perf/util/Build | 1 +
> tools/perf/util/aslr.c | 1220 +++++++++++++++++++++++++++++++++++
> tools/perf/util/aslr.h | 10 +
> 4 files changed, 1261 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..8fe479cb4152 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,8 @@ static int perf_event__repipe(const struct perf_tool *tool,
> return perf_event__repipe_synth(tool, event);
> }
>
> +
> +
> 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 +2462,8 @@ static int __cmd_inject(struct perf_inject *inject)
> }
> }
>
> +
> +
Unnessary whitespace changes here and the above.
> 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 +2570,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 +2579,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,18 +2693,36 @@ 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;
> }
>
> if (zstd_init(&(inject.session->zstd_data), 0) < 0)
> pr_warning("Decompression initialization failed.\n");
>
> + if (inject.aslr) {
> + struct evsel *evsel;
> +
> + evlist__for_each_entry(inject.session->evlist, evsel) {
> + if (evsel->core.attr.type == PERF_TYPE_BREAKPOINT)
> + evsel->core.attr.bp_addr = 0;
> + }
> + }
> +
> /* Save original section info before feature bits change */
> ret = save_section_info(&inject);
> if (ret)
> @@ -2789,6 +2816,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..effdcbec0db0
> --- /dev/null
> +++ b/tools/perf/util/aslr.c
> @@ -0,0 +1,1220 @@
> +// 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) {
> + if (errno == EINTR)
> + continue;
> + return ret;
> + }
> + if (ret == 0)
> + return 0;
> + 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.
I'm curious if it's guaranteed to be unique within a process.
> + * @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 RC_CHK_EQUAL(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 {
> + if (effective_cpumode == PERF_RECORD_MISC_KERNEL) {
> + struct hashmap_entry *cur;
> + size_t bkt;
> +
> + hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
> + struct remap_addresses_key *k;
> + u64 *v;
> +
> + k = (struct remap_addresses_key *)cur->pkey;
> + if (k->pid == kernel_pid &&
> + k->invariant == key.invariant) {
> + v = (u64 *)cur->pvalue;
> + remap_addr = *v + map__pgoff(al.map) +
> + (addr - map__start(al.map));
> + break;
> + }
> + }
> + }
> + if (remap_addr == 0) {
> + 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 addr_location prev_al;
> + struct remap_addresses_key key;
> + struct remap_addresses_key *new_key = NULL;
> + struct remap_addresses_key *old_key = NULL;
> + u64 remap_addr = 0;
> + u64 *remapped_invariant_ptr = NULL;
> + u64 *max_addr_ptr = NULL;
> + u64 *new_val = NULL;
> + u64 *new_max = NULL;
> + u64 *old_val = NULL;
> + u64 *old_val_remap = NULL;
> + bool is_contiguous = false;
> + bool first_mapping = false;
> + bool key_found = false;
> + int err;
> +
> + 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);
> + key.invariant = map__start(al.map) - map__pgoff(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 + (al.map ? map__pgoff(al.map) : pgoff);
> + key_found = true;
> + } else {
> + 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;
> +
> + new_key = malloc(sizeof(*new_key));
> + new_val = malloc(sizeof(u64));
I think the value of hashmap can be passed as value if it's u64.. well
on 64-bit systems.
> +
> + 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 - (al.map ? map__pgoff(al.map) : 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 */
> + new_max = malloc(sizeof(u64));
> + old_val = NULL;
> +
> + if (!new_max) {
> + old_key = NULL;
> + 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) {
> + old_key = NULL;
> + 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)
Any chance you can share the code with the above function?
> +{
> + struct addr_location al;
> + struct remap_addresses_key key;
> + struct hashmap_entry *cur;
> + struct remap_addresses_key *new_key = NULL;
> + struct remap_addresses_key *old_key = NULL;
> + struct remap_addresses_key *k;
> + size_t bkt;
> + u64 remap_addr = 0;
> + u64 *remapped_invariant_ptr = NULL;
> + u64 *max_addr_ptr = NULL;
> + u64 *new_val = NULL;
> + u64 *new_max = NULL;
> + u64 *old_val = NULL;
> + u64 *old_val_remap = NULL;
> + u64 *v;
> + bool first_mapping = false;
> + int err;
> +
> + 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;
> + }
> +
> + hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
> + k = (struct remap_addresses_key *)cur->pkey;
> + if (k->pid == kernel_pid && k->invariant == key.invariant) {
> + v = (u64 *)cur->pvalue;
> +
> + if (al.map)
> + remap_addr = *v + map__pgoff(al.map) +
> + (addr - map__start(al.map));
> + else
> + remap_addr = *v;
> + 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;
> +
> + new_key = malloc(sizeof(*new_key));
> + 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;
> + }
> +
> + new_max = malloc(sizeof(u64));
> + old_val = NULL;
> +
> + if (!new_max) {
> + old_key = NULL;
> + 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) {
> + old_key = NULL;
> + 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) {
> + new_event->mmap.pgoff = 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) {
> + new_event->mmap2.pgoff = 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;
> +
> + 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;
> +
> +
> +
Excessive blank lines.
> + 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;
Is this mean you drop samples if it contains registers?
> + }
> + 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;
> + }
Can you use perf_event__synthesize_sample()?
> +
> + if (evsel__is_offcpu_event(evsel)) {
> + /* TODO: can this be less conservative? */
> + pr_debug("Dropping off-CPU sample as possible ASLR leak\n");
Why not remap the address?
Thanks,
Namhyung
> + 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__destroy_kernel_maps(&aslr->machines);
> + 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 [flat|nested] 27+ messages in thread
* Re: [PATCH v5 4/5] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-05-06 18:52 ` Namhyung Kim
@ 2026-05-06 20:01 ` Ian Rogers
0 siblings, 0 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-06 20:01 UTC (permalink / raw)
To: Namhyung Kim
Cc: acme, gmx, adrian.hunter, james.clark, jolsa, linux-kernel,
linux-perf-users, mingo, peterz
On Wed, May 6, 2026 at 11:52 AM Namhyung Kim <namhyung@kernel.org> wrote:
>
> On Tue, May 05, 2026 at 05:45:45PM -0700, Ian Rogers wrote:
> > If perf.data files are taken from one machine to another they may
> > leak virtual addresses and so weaken ASLR on the machine they are
> > coming from. Add an 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.
> >
> > The ASLR tracking tool virtualizes process and machine namespaces using
> > 'struct machines' to safely isolate host mappings from unprivileged KVM guest
> > address spaces. Memory layouts are tracked globally per process context to
> > ensure linear, continuous space allocations across successive mapping runs.
> >
> > To remain strictly conservative and guarantee security, the tool scrubs
> > breakpoint addresses (bp_addr) from all synthesized stream headers, and drops
> > unsupported complex payloads (such as user register stacks, raw tracepoints,
> > and hardware AUX tracing frames) to completely eliminate accidental address
> > leakage vectors.
> >
> > 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>
> > ---
> > v5: Fix memory leaks inside aslr_tool__delete destructor by calling standard
> > machines__destroy_kernel_maps() to cleanly free host/guest maps and guest
> > machine structures. Introduce the precise 'first_kernel_mapping' tracking
> > guard inside aslr.c to rewrite the core kernel pgoff virtual address while
> > safely protecting module file offsets from corruption. Harden skipn()
> > pipe I/O stream reader loops against EINTR interruption errors. Clean up
> > breakpoint address (bp_addr) memory scrubbing by executing the scrubbing loop
> > directly at core session initialization startup level, natively securing both
> > file headers and streaming pipe channels while removing redundant runtime
> > tool wrapper interception hooks layers.
> > 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 | 31 +-
> > tools/perf/util/Build | 1 +
> > tools/perf/util/aslr.c | 1220 +++++++++++++++++++++++++++++++++++
> > tools/perf/util/aslr.h | 10 +
> > 4 files changed, 1261 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..8fe479cb4152 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,8 @@ static int perf_event__repipe(const struct perf_tool *tool,
> > return perf_event__repipe_synth(tool, event);
> > }
> >
> > +
> > +
> > 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 +2462,8 @@ static int __cmd_inject(struct perf_inject *inject)
> > }
> > }
> >
> > +
> > +
>
> Unnessary whitespace changes here and the above.
Ack.
> > 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 +2570,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 +2579,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,18 +2693,36 @@ 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;
> > }
> >
> > if (zstd_init(&(inject.session->zstd_data), 0) < 0)
> > pr_warning("Decompression initialization failed.\n");
> >
> > + if (inject.aslr) {
> > + struct evsel *evsel;
> > +
> > + evlist__for_each_entry(inject.session->evlist, evsel) {
> > + if (evsel->core.attr.type == PERF_TYPE_BREAKPOINT)
> > + evsel->core.attr.bp_addr = 0;
> > + }
> > + }
> > +
> > /* Save original section info before feature bits change */
> > ret = save_section_info(&inject);
> > if (ret)
> > @@ -2789,6 +2816,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..effdcbec0db0
> > --- /dev/null
> > +++ b/tools/perf/util/aslr.c
> > @@ -0,0 +1,1220 @@
> > +// 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) {
> > + if (errno == EINTR)
> > + continue;
> > + return ret;
> > + }
> > + if (ret == 0)
> > + return 0;
> > + 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.
>
> I'm curious if it's guaranteed to be unique within a process.
You could have two DSOs at different addresses, and when the `address
- pgoff` is computed, they might yield the same invariant value.
However, in that case the DSOs differ, so the keys don't match. If it
were the same DSO, either the address or the pgoff would differ making
it impossible to have the same invariant value.
>
> > + * @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 RC_CHK_EQUAL(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 {
> > + if (effective_cpumode == PERF_RECORD_MISC_KERNEL) {
> > + struct hashmap_entry *cur;
> > + size_t bkt;
> > +
> > + hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
> > + struct remap_addresses_key *k;
> > + u64 *v;
> > +
> > + k = (struct remap_addresses_key *)cur->pkey;
> > + if (k->pid == kernel_pid &&
> > + k->invariant == key.invariant) {
> > + v = (u64 *)cur->pvalue;
> > + remap_addr = *v + map__pgoff(al.map) +
> > + (addr - map__start(al.map));
> > + break;
> > + }
> > + }
> > + }
> > + if (remap_addr == 0) {
> > + 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 addr_location prev_al;
> > + struct remap_addresses_key key;
> > + struct remap_addresses_key *new_key = NULL;
> > + struct remap_addresses_key *old_key = NULL;
> > + u64 remap_addr = 0;
> > + u64 *remapped_invariant_ptr = NULL;
> > + u64 *max_addr_ptr = NULL;
> > + u64 *new_val = NULL;
> > + u64 *new_max = NULL;
> > + u64 *old_val = NULL;
> > + u64 *old_val_remap = NULL;
> > + bool is_contiguous = false;
> > + bool first_mapping = false;
> > + bool key_found = false;
> > + int err;
> > +
> > + 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);
> > + key.invariant = map__start(al.map) - map__pgoff(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 + (al.map ? map__pgoff(al.map) : pgoff);
> > + key_found = true;
> > + } else {
> > + 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;
> > +
> > + new_key = malloc(sizeof(*new_key));
> > + new_val = malloc(sizeof(u64));
>
> I think the value of hashmap can be passed as value if it's u64.. well
> on 64-bit systems.
Yeah, sashiko was complaining about 32-bit builds. I think we should
diverge our hashmap implementation as using long for the key rather
than s64 just causes this kind of silliness for us. I think we can
also add generic support avoid errptrs, etc. I don't have hope of BPF
people being sensible as in this series:
https://lore.kernel.org/linux-perf-users/20260322005823.981079-1-irogers@google.com/
>
> > +
> > + 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 - (al.map ? map__pgoff(al.map) : 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 */
> > + new_max = malloc(sizeof(u64));
> > + old_val = NULL;
> > +
> > + if (!new_max) {
> > + old_key = NULL;
> > + 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) {
> > + old_key = NULL;
> > + 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)
>
> Any chance you can share the code with the above function?
I'll check.
> > +{
> > + struct addr_location al;
> > + struct remap_addresses_key key;
> > + struct hashmap_entry *cur;
> > + struct remap_addresses_key *new_key = NULL;
> > + struct remap_addresses_key *old_key = NULL;
> > + struct remap_addresses_key *k;
> > + size_t bkt;
> > + u64 remap_addr = 0;
> > + u64 *remapped_invariant_ptr = NULL;
> > + u64 *max_addr_ptr = NULL;
> > + u64 *new_val = NULL;
> > + u64 *new_max = NULL;
> > + u64 *old_val = NULL;
> > + u64 *old_val_remap = NULL;
> > + u64 *v;
> > + bool first_mapping = false;
> > + int err;
> > +
> > + 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;
> > + }
> > +
> > + hashmap__for_each_entry(&aslr->remap_addresses, cur, bkt) {
> > + k = (struct remap_addresses_key *)cur->pkey;
> > + if (k->pid == kernel_pid && k->invariant == key.invariant) {
> > + v = (u64 *)cur->pvalue;
> > +
> > + if (al.map)
> > + remap_addr = *v + map__pgoff(al.map) +
> > + (addr - map__start(al.map));
> > + else
> > + remap_addr = *v;
> > + 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;
> > +
> > + new_key = malloc(sizeof(*new_key));
> > + 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;
> > + }
> > +
> > + new_max = malloc(sizeof(u64));
> > + old_val = NULL;
> > +
> > + if (!new_max) {
> > + old_key = NULL;
> > + 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) {
> > + old_key = NULL;
> > + 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) {
> > + new_event->mmap.pgoff = 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) {
> > + new_event->mmap2.pgoff = 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;
> > +
> > + 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;
> > +
> > +
> > +
>
> Excessive blank lines.
Ack.
> > + 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;
>
> Is this mean you drop samples if it contains registers?
Yep, as noted by the TODO.
>
> > + }
> > + 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;
> > + }
>
> Can you use perf_event__synthesize_sample()?
I'd prefer not to as I think working field by field is worthwhile in
convincing ourselves of a lack of ASLR leaks. Punting this into
perf_sample and synthesis opens up gaps for bugs to be introduced.
> > +
> > + if (evsel__is_offcpu_event(evsel)) {
> > + /* TODO: can this be less conservative? */
> > + pr_debug("Dropping off-CPU sample as possible ASLR leak\n");
>
> Why not remap the address?
No reason, but support wasn't a first priority, hence the TODO.
Thanks,
Ian
> Thanks,
> Namhyung
>
> > + 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__destroy_kernel_maps(&aslr->machines);
> > + 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 [flat|nested] 27+ messages in thread
* Re: [PATCH v5 5/5] perf test: Add inject ASLR test
2026-05-06 0:45 ` [PATCH v5 5/5] perf test: Add inject ASLR test Ian Rogers
@ 2026-05-07 15:58 ` James Clark
2026-05-07 16:17 ` Ian Rogers
0 siblings, 1 reply; 27+ messages in thread
From: James Clark @ 2026-05-07 15:58 UTC (permalink / raw)
To: Ian Rogers
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz, acme, gmx, namhyung
On 06/05/2026 1:45 am, Ian Rogers wrote:
> 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' assertions to catch
> pipeline failures, stream-consuming awk processors to handle SIGPIPE signals,
> and a dedicated pipe output scenario validating raw 'perf inject -o -' stdout
> streams.
>
> Assisted-by: Gemini-CLI:Google Gemini 3
> Signed-off-by: Ian Rogers <irogers@google.com>
> ---
> v5: Harden test suite verification pipelines by upgrading report checks to
> strict sorted line-by-line diff comparisons to accommodate remapped pointer
> shifts. Append || true fallback operators to grep-v filtering pipelines to
> prevent the shell test from spuriously aborting under set -o pipefail on
> empty inputs, ensuring graceful failure checks trigger correctly.
> 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..cdc3aa94de63
> --- /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]"
Hi Ian,
This test fails on Arm. I believe it's because on Arm we request the
link register to be sampled with frame pointer unwinds. Then the aslr
tool drops all the samples because it sees that user regs were sampled:
/* TODO: can this be less conservative? */
pr_debug("Dropping regs user sample as possible ASLR leak\n");
ret = 0;
goto out_put;
I think maybe that comment is onto something. Perhaps the user regs can
be zeroed instead of dropping the sample. Then the frame pointer unwind
will still work on Arm and the aslr test will pass. We just won't be
able to use the link register to add the leaf frame caller, but that's
not a big deal.
James
> + 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"
> +
> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report1_clean}" | \
> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report1_norm}" || true
> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report2_clean}" | \
> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report2_norm}" || true
> +
> + 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
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 5/5] perf test: Add inject ASLR test
2026-05-07 15:58 ` James Clark
@ 2026-05-07 16:17 ` Ian Rogers
2026-05-08 10:42 ` James Clark
0 siblings, 1 reply; 27+ messages in thread
From: Ian Rogers @ 2026-05-07 16:17 UTC (permalink / raw)
To: James Clark
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz, acme, gmx, namhyung
On Thu, May 7, 2026 at 8:58 AM James Clark <james.clark@linaro.org> wrote:
>
>
>
> On 06/05/2026 1:45 am, Ian Rogers wrote:
> > 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' assertions to catch
> > pipeline failures, stream-consuming awk processors to handle SIGPIPE signals,
> > and a dedicated pipe output scenario validating raw 'perf inject -o -' stdout
> > streams.
> >
> > Assisted-by: Gemini-CLI:Google Gemini 3
> > Signed-off-by: Ian Rogers <irogers@google.com>
> > ---
> > v5: Harden test suite verification pipelines by upgrading report checks to
> > strict sorted line-by-line diff comparisons to accommodate remapped pointer
> > shifts. Append || true fallback operators to grep-v filtering pipelines to
> > prevent the shell test from spuriously aborting under set -o pipefail on
> > empty inputs, ensuring graceful failure checks trigger correctly.
> > 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..cdc3aa94de63
> > --- /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]"
>
> Hi Ian,
>
> This test fails on Arm. I believe it's because on Arm we request the
> link register to be sampled with frame pointer unwinds. Then the aslr
> tool drops all the samples because it sees that user regs were sampled:
>
> /* TODO: can this be less conservative? */
> pr_debug("Dropping regs user sample as possible ASLR leak\n");
> ret = 0;
> goto out_put;
>
> I think maybe that comment is onto something. Perhaps the user regs can
> be zeroed instead of dropping the sample. Then the frame pointer unwind
> will still work on Arm and the aslr test will pass. We just won't be
> able to use the link register to add the leaf frame caller, but that's
> not a big deal.
Thanks James. I'm working on a new version of the patches, but I'm
having delays getting the AI to approve the changes.
ARM does what? Ah, I knew this and also it didn't really register. I'm
wondering now if we can put the machinery behind "EM_HOST ==
EM_AARCH64":
https://lore.kernel.org/all/20211217154521.80603-2-german.gomez@arm.com/
as it seems a mechanism that would benefit other architectures such as
ARM32 :-) And I have my mission to make tools/perf/arch disappear as
much as is humanly possible.
I also imagine the problem the link register solves for perf happens
for BPF, so perhaps this ability shouldn't be encouraged.
I think rather than zeroing the register values it would be better to
just remove them from the output events. I'll try to add that support
as having this test break on ARM isn't desirable.
Thanks,
Ian
> James
>
> > + 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"
> > +
> > + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report1_clean}" | \
> > + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report1_norm}" || true
> > + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report2_clean}" | \
> > + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report2_norm}" || true
> > +
> > + 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
>
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes
2026-05-06 0:45 ` [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (4 preceding siblings ...)
2026-05-06 0:45 ` [PATCH v5 5/5] perf test: Add inject ASLR test Ian Rogers
@ 2026-05-08 8:27 ` Ian Rogers
2026-05-08 8:27 ` [PATCH v6 1/6] perf sched: Add missing mmap2 handler in timehist Ian Rogers
` (5 more replies)
5 siblings, 6 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-08 8:27 UTC (permalink / raw)
To: irogers, acme, gmx, james.clark, namhyung
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz
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 three independent, critical bug fixes inside core event
dispatching and map tracking tools that harden perf session analysis
against dynamic crashes, concurrent lookup data races, and callchain
mapping failures.
Core Feature: 'perf inject --aslr' (Patches 4, 5, and 6)
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.
Events carrying virtual memory layouts are conservatively
remap-processed or dropped, while zero-address-risk lifecycle metadata
records (such as namespaces, cgroups, and BPF program info) are
intentionally delegated to preserve comprehensive downstream trace
tool analysis compatibility.
The ASLR tracking tool virtualizes process and machine namespaces
using 'struct machines' to safely isolate host mappings from
unprivileged KVM guest address spaces. Memory space layouts are
tracked globally per process context to ensure linear, continuous
space allocations across successive mapping runs. The topological
invariant coordinate dso + invariant (start - pgoff) is tracked to
uniquely index binary section frameworks, providing complete collision
safety against separate overlapping shared-invariant libraries while
remaining perfectly immune to boundary shifts or split fragmentations.
To remain strictly conservative and guarantee security, the tool
scrubs breakpoint addresses (bp_addr) from all synthesized stream
headers, completely drops PERF_RECORD_TEXT_POKE events to prevent
absolute immediate pointer operands leaks, and drops unsupported
complex payloads (such as user register stacks, raw tracepoints, and
hardware AUX tracing frames).
Verification is reinforced in Patch 5 with a 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 utilizes a highly dense, system-call intensive
VFS byte block loop workload (dd count=500) to guarantee deterministic
hardware timer interrupts sampling streams inside kernel privilege
states.
Prerequisite Bug Fixes (Patches 1, 2, and 3)
During development, three core event delegation and map indexing
issues were identified and resolved to prevent crashes, live-locks,
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.
3. perf symbols: Patch 3 replaces old remove-reinsert map boundary
update cycles with a high-performance, thread-safe transactional
framework maps__mutate_mapping() that enforces write semaphore lock
closures around all in-place virtual address mutations and sorting
invalidations, completely closing concurrent lookup race condition
windows. It explicitly executes DWARF address space cache
invalidation (libdw__invalidate_dwfl()) to keep debugger unwinding
frames perfectly synchronized.
Changes since v5:
- Core Concurrency Fix (Patch 3): Refactor map address boundary
mutations across ELF loaders, proc kallsyms parsers, and dynamic
module managers to utilize a thread-safe, synchronized transactional
framework maps__mutate_mapping() that encapsulates mutations and
sorting invalidations under write lock closures, eliminating
concurrent lookup race condition windows. Cites intention-revealing
callbacks names (remap_kernel_cb).
- Feature Exclusivity (Patch 4): Inject strict command-line validation
checks enforcing mutual exclusivity between --aslr and
--convert-callchain to prevent silent trace unwind failures since
ASLR stack dropping conflicts directly with DWARF parsing needs.
- KASLR Hardening (Patch 4): Secure mmap.pgoff unconditionally for all
host and guest kernel text mapping regions to prevent unredacted
active KASLR base deltas leakage.
- TEXT_POKE Drops (Patch 4): Conservatively drop PERF_RECORD_TEXT_POKE
events completely via a local static drop stub to prevent unredacted
absolute 64-bit kernel virtual pointer immediate operands leakage.
- Parsing Invariants (Patch 4): Inject explicit array-end bounds
validation check blocks before consuming trailing
PERF_CONTEXT_USER_DEFERRED callchain cookies to completely eliminate
out-of-bounds reads and parser desynchronization faults.
- Commit Records Alignment (Patch 4): Precisely clarify commit
descriptions to reflect that zero-address metadata events are
intentionally delegated to protect downstream trace tool processing
backward compatibility.
- Telemetry Stabilization (Patch 5): Upgrade kernel space tracking
workloads to utilize a dedicated system-call intensive VFS byte
block loop workload (dd count=500) instead of purely userspace-bound
tight loops, guaranteeing high-density kernel privilege state
sampling streams and eliminating intermittent execution flakiness
dropouts.
- Profile Retention Optimizer (Patch 6): Refactor sample processor to
dynamically strip out ONLY register dump words out of sample
payloads while shrinking output header sizes, overwriting ABI words
to NONE, and scrubbing attributes up front. This completely rescues
trace profiles from complete sample drop starvation, which happened
by default on ARM64.
Ian Rogers (6):
perf sched: Add missing mmap2 handler in timehist
perf tool: Missing delegate_tool schedstat delegates and
dont_split_sample_group
perf maps: Add maps__mutate_mapping
perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
perf test: Add inject ASLR test
perf aslr: Strip sample registers
tools/perf/builtin-inject.c | 47 +-
tools/perf/builtin-sched.c | 1 +
tools/perf/tests/shell/inject_aslr.sh | 511 ++++++++++++
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1035 +++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
tools/perf/util/machine.c | 32 +-
tools/perf/util/maps.c | 26 +
tools/perf/util/maps.h | 2 +
tools/perf/util/symbol-elf.c | 41 +-
tools/perf/util/symbol.c | 17 +-
tools/perf/util/tool.c | 6 +
12 files changed, 1697 insertions(+), 32 deletions(-)
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.563.g4f69b47b94-goog
^ permalink raw reply [flat|nested] 27+ messages in thread
* [PATCH v6 1/6] perf sched: Add missing mmap2 handler in timehist
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
@ 2026-05-08 8:27 ` Ian Rogers
2026-05-08 8:27 ` [PATCH v6 2/6] perf tool: Missing delegate_tool schedstat delegates and dont_split_sample_group Ian Rogers
` (4 subsequent siblings)
5 siblings, 0 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-08 8:27 UTC (permalink / raw)
To: irogers, acme, gmx, james.clark, namhyung
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz
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: 49394a2a24c78ce0 ("perf sched timehist: Introduce timehist command")
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.563.g4f69b47b94-goog
^ permalink raw reply related [flat|nested] 27+ messages in thread
* [PATCH v6 2/6] perf tool: Missing delegate_tool schedstat delegates and dont_split_sample_group
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-08 8:27 ` [PATCH v6 1/6] perf sched: Add missing mmap2 handler in timehist Ian Rogers
@ 2026-05-08 8:27 ` Ian Rogers
2026-05-08 8:27 ` [PATCH v6 3/6] perf maps: Add maps__mutate_mapping Ian Rogers
` (3 subsequent siblings)
5 siblings, 0 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-08 8:27 UTC (permalink / raw)
To: irogers, acme, gmx, james.clark, namhyung
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz
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.563.g4f69b47b94-goog
^ permalink raw reply related [flat|nested] 27+ messages in thread
* [PATCH v6 3/6] perf maps: Add maps__mutate_mapping
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-08 8:27 ` [PATCH v6 1/6] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-08 8:27 ` [PATCH v6 2/6] perf tool: Missing delegate_tool schedstat delegates and dont_split_sample_group Ian Rogers
@ 2026-05-08 8:27 ` Ian Rogers
2026-05-08 10:57 ` James Clark
2026-05-11 7:07 ` Namhyung Kim
2026-05-08 8:27 ` [PATCH v6 4/6] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
` (2 subsequent siblings)
5 siblings, 2 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-08 8:27 UTC (permalink / raw)
To: irogers, acme, gmx, james.clark, namhyung
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz
During kernel ELF symbol parsing (dso__process_kernel_symbol), proc
kallsyms image loading (dso__load_kernel_sym,
dso__load_guest_kernel_sym), and dynamic kernel memory map alignment
updates (machine__update_kernel_mmap), the loader directly modifies
live virtual address boundary keys fields on map objects. If these
boundaries are mutated while the map pointer actively resides inside
the parent maps cache array list (kmaps) outside of any lock closure,
an unsafe concurrent window is exposed where parallel worker lookup
threads (e.g., inside perf top) can mistakenly assume the cache
remains sorted based on stale parameters, executing binary search
queries (bsearch) across an unsorted range and triggering lookups
failure.
Fix this by introducing a thread-safe, atomic transactional framework
routine maps__mutate_mapping() that explicitly acquires the parent
maps write semaphore lock, executes an incoming mutation callback
block to perform the field updates under full lock protection, and
invalidates the sorted tracking flags prior to releasing the write
lock. This guarantees absolute atomic synchronization invariants,
completely closing the concurrent lookup race window. The adjacent
module alignment pass inside machine__create_kernel_maps() is safely
preserved as a high-performance lockless pass, as its invocation
lifecycle bounds remain strictly single-threaded by contract during
session initialization construction. There is a potential for self
deadlock if maps__mutate_mapping is called with the lock held, such as
with maps__for_each_map but this problem also existed with the
previous remove and insert approaches.
Fixes: 39b12f781271 ("perf tools: Make it possible to read object code from vmlinux")
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/machine.c | 32 +++++++++++++++++-----------
tools/perf/util/maps.c | 26 +++++++++++++++++++++++
tools/perf/util/maps.h | 2 ++
tools/perf/util/symbol-elf.c | 41 +++++++++++++++++++++++-------------
tools/perf/util/symbol.c | 17 +++++++++++----
5 files changed, 87 insertions(+), 31 deletions(-)
diff --git a/tools/perf/util/machine.c b/tools/perf/util/machine.c
index e76f8c86e62a..8d4452c70cb5 100644
--- a/tools/perf/util/machine.c
+++ b/tools/perf/util/machine.c
@@ -1522,22 +1522,30 @@ static void machine__set_kernel_mmap(struct machine *machine,
map__set_end(machine->vmlinux_map, ~0ULL);
}
-static int machine__update_kernel_mmap(struct machine *machine,
- u64 start, u64 end)
+struct kernel_mmap_mutation_ctx {
+ u64 start;
+ u64 end;
+};
+
+static int kernel_mmap_mutate_cb(struct map *map, void *data)
{
- struct map *orig, *updated;
- int err;
+ struct kernel_mmap_mutation_ctx *ctx = data;
- orig = machine->vmlinux_map;
- updated = map__get(orig);
+ map__set_start(map, ctx->start);
+ map__set_end(map, ctx->end);
+ if (ctx->start == 0 && ctx->end == 0)
+ map__set_end(map, ~0ULL);
+ return 0;
+}
- machine->vmlinux_map = updated;
- maps__remove(machine__kernel_maps(machine), orig);
- machine__set_kernel_mmap(machine, start, end);
- err = maps__insert(machine__kernel_maps(machine), updated);
- map__put(orig);
+static int machine__update_kernel_mmap(struct machine *machine,
+ u64 start, u64 end)
+{
+ struct kernel_mmap_mutation_ctx ctx = { .start = start, .end = end };
- return err;
+ return maps__mutate_mapping(machine__kernel_maps(machine),
+ machine->vmlinux_map,
+ kernel_mmap_mutate_cb, &ctx);
}
int machine__create_kernel_maps(struct machine *machine)
diff --git a/tools/perf/util/maps.c b/tools/perf/util/maps.c
index 81a97ac34077..91345a773aa2 100644
--- a/tools/perf/util/maps.c
+++ b/tools/perf/util/maps.c
@@ -575,6 +575,32 @@ void maps__remove(struct maps *maps, struct map *map)
#endif
}
+int maps__mutate_mapping(struct maps *maps, struct map *map,
+ int (*mutate_cb)(struct map *map, void *data), void *data)
+{
+ int err = 0;
+
+ if (maps)
+ down_write(maps__lock(maps));
+
+ err = mutate_cb(map, data);
+
+ if (maps) {
+ RC_CHK_ACCESS(maps)->maps_by_address_sorted = false;
+ RC_CHK_ACCESS(maps)->maps_by_name_sorted = false;
+ }
+
+ if (maps)
+ up_write(maps__lock(maps));
+
+#ifdef HAVE_LIBDW_SUPPORT
+ if (maps)
+ libdw__invalidate_dwfl(maps, maps__libdw_addr_space_dwfl(maps));
+#endif
+
+ return err;
+}
+
bool maps__empty(struct maps *maps)
{
bool res;
diff --git a/tools/perf/util/maps.h b/tools/perf/util/maps.h
index 20c52084ba9e..de74ccbb8a12 100644
--- a/tools/perf/util/maps.h
+++ b/tools/perf/util/maps.h
@@ -61,6 +61,8 @@ size_t maps__fprintf(struct maps *maps, FILE *fp);
int maps__insert(struct maps *maps, struct map *map);
void maps__remove(struct maps *maps, struct map *map);
+int maps__mutate_mapping(struct maps *maps, struct map *map,
+ int (*mutate_cb)(struct map *map, void *data), void *data);
struct map *maps__find(struct maps *maps, u64 addr);
struct symbol *maps__find_symbol(struct maps *maps, u64 addr, struct map **mapp);
diff --git a/tools/perf/util/symbol-elf.c b/tools/perf/util/symbol-elf.c
index 7afa8a117139..dc4ab58857b3 100644
--- a/tools/perf/util/symbol-elf.c
+++ b/tools/perf/util/symbol-elf.c
@@ -1341,6 +1341,24 @@ static u64 ref_reloc(struct kmap *kmap)
void __weak arch__sym_update(struct symbol *s __maybe_unused,
GElf_Sym *sym __maybe_unused) { }
+struct remap_kernel_ctx {
+ u64 sh_addr;
+ u64 sh_size;
+ u64 sh_offset;
+ struct kmap *kmap;
+};
+
+static int remap_kernel_cb(struct map *map, void *data)
+{
+ struct remap_kernel_ctx *ctx = data;
+
+ map__set_start(map, ctx->sh_addr + ref_reloc(ctx->kmap));
+ map__set_end(map, map__start(map) + ctx->sh_size);
+ map__set_pgoff(map, ctx->sh_offset);
+ map__set_mapping_type(map, MAPPING_TYPE__DSO);
+ return 0;
+}
+
static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
GElf_Sym *sym, GElf_Shdr *shdr,
struct maps *kmaps, struct kmap *kmap,
@@ -1371,22 +1389,15 @@ static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
* map to the kernel dso.
*/
if (*remap_kernel && dso__kernel(dso) && !kmodule) {
+ struct remap_kernel_ctx ctx = {
+ .sh_addr = shdr->sh_addr,
+ .sh_size = shdr->sh_size,
+ .sh_offset = shdr->sh_offset,
+ .kmap = kmap
+ };
+
*remap_kernel = false;
- map__set_start(map, shdr->sh_addr + ref_reloc(kmap));
- map__set_end(map, map__start(map) + shdr->sh_size);
- map__set_pgoff(map, shdr->sh_offset);
- map__set_mapping_type(map, MAPPING_TYPE__DSO);
- /* Ensure maps are correctly ordered */
- if (kmaps) {
- int err;
- struct map *tmp = map__get(map);
-
- maps__remove(kmaps, map);
- err = maps__insert(kmaps, map);
- map__put(tmp);
- if (err)
- return err;
- }
+ maps__mutate_mapping(kmaps, map, remap_kernel_cb, &ctx);
}
/*
diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
index fcaeeddbbb6b..09b93e844887 100644
--- a/tools/perf/util/symbol.c
+++ b/tools/perf/util/symbol.c
@@ -48,6 +48,13 @@
#include <symbol/kallsyms.h>
#include <sys/utsname.h>
+static int map_fixup_cb(struct map *map, void *data __maybe_unused)
+{
+ map__fixup_start(map);
+ map__fixup_end(map);
+ return 0;
+}
+
static int dso__load_kernel_sym(struct dso *dso, struct map *map);
static int dso__load_guest_kernel_sym(struct dso *dso, struct map *map);
static bool symbol__is_idle(const char *name);
@@ -2121,10 +2128,11 @@ static int dso__load_kernel_sym(struct dso *dso, struct map *map)
free(kallsyms_allocated_filename);
if (err > 0 && !dso__is_kcore(dso)) {
+ struct maps *kmaps = map__kmaps(map);
+
dso__set_binary_type(dso, DSO_BINARY_TYPE__KALLSYMS);
dso__set_long_name(dso, DSO__NAME_KALLSYMS, false);
- map__fixup_start(map);
- map__fixup_end(map);
+ maps__mutate_mapping(kmaps, map, map_fixup_cb, NULL);
}
return err;
@@ -2164,10 +2172,11 @@ static int dso__load_guest_kernel_sym(struct dso *dso, struct map *map)
if (err > 0)
pr_debug("Using %s for symbols\n", kallsyms_filename);
if (err > 0 && !dso__is_kcore(dso)) {
+ struct maps *kmaps = map__kmaps(map);
+
dso__set_binary_type(dso, DSO_BINARY_TYPE__GUEST_KALLSYMS);
dso__set_long_name(dso, machine->mmap_name, false);
- map__fixup_start(map);
- map__fixup_end(map);
+ maps__mutate_mapping(kmaps, map, map_fixup_cb, NULL);
}
return err;
--
2.54.0.563.g4f69b47b94-goog
^ permalink raw reply related [flat|nested] 27+ messages in thread
* [PATCH v6 4/6] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (2 preceding siblings ...)
2026-05-08 8:27 ` [PATCH v6 3/6] perf maps: Add maps__mutate_mapping Ian Rogers
@ 2026-05-08 8:27 ` Ian Rogers
2026-05-11 7:32 ` Namhyung Kim
2026-05-08 8:27 ` [PATCH v6 5/6] perf test: Add inject ASLR test Ian Rogers
2026-05-08 8:27 ` [PATCH v6 6/6] perf aslr: Strip sample registers Ian Rogers
5 siblings, 1 reply; 27+ messages in thread
From: Ian Rogers @ 2026-05-08 8:27 UTC (permalink / raw)
To: irogers, acme, gmx, james.clark, namhyung
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz
If perf.data files are taken from one machine to another they may
leak virtual addresses and so weaken ASLR on the machine they are
coming from. Add an aslr option for perf inject that remaps all
virtual addresses, or drops data/events, so that the virtual address
information isn't leaked.
Events carrying virtual memory layouts are conservatively remap-processed
or dropped, while zero-address-risk lifecycle metadata records (such as
namespaces, cgroups, and BPF program info) are intentionally delegated
to preserve comprehensive downstream trace tool analysis compatibility.
The ASLR tracking tool virtualizes process and machine namespaces using
'struct machines' to safely isolate host mappings from unprivileged KVM guest
address spaces. Memory space layouts are tracked globally per process context to
ensure linear, continuous space allocations across successive mapping runs.
To remain strictly conservative and guarantee security, the tool scrubs
breakpoint addresses (bp_addr) from all synthesized stream headers, completely
drops PERF_RECORD_TEXT_POKE events to prevent absolute immediate pointer
operands leaks, and drops unsupported complex payloads (such as user register
stacks, raw tracepoints, and hardware AUX tracing frames).
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>
---
v6: Enforce strict command-line validation mutual exclusivity between
--aslr and --convert-callchain to prevent silent unwind failures.
Secure mmap.pgoff unconditionally for all host and guest kernel text
mapping regions to completely prevent active KASLR load deltas leakage.
Conservatively drop PERF_RECORD_TEXT_POKE events completely via a local
static drop stub to prevent absolute 64-bit kernel virtual pointer immediate
operands leaks. Inject explicit array-end bounds validation check blocks
before consuming trailing PERF_CONTEXT_USER_DEFERRED callchain cookies
to eliminate out-of-bounds reads and parser desynchronization faults.
Simplify ASLR mapping remap logic. Ensure that encountering a
PERF_CONTEXT_USER_DEFERRED context marker explicitly updates cpumode.
v5: Add machine to remap addresss key so that it is guest/host
safe. Add 'first_kernel_mapping' tracking guard inside aslr.c to
rewrite the core kernel pgoff virtual address while safely
protecting module file offsets from corruption. Clean up
breakpoint address (bp_addr) memory scrubbing by executing the
scrubbing loop directly at core session initialization startup
level, natively securing both file headers and streaming pipe
channels while removing redundant runtime tool wrapper
interception hooks layers.
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 | 36 +-
tools/perf/util/Build | 1 +
tools/perf/util/aslr.c | 1036 +++++++++++++++++++++++++++++++++++
tools/perf/util/aslr.h | 10 +
4 files changed, 1082 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 6ab20df358c4..51dcf248b653 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,8 @@ static int perf_event__repipe(const struct perf_tool *tool,
return perf_event__repipe_synth(tool, event);
}
+
+
static int perf_event__drop(const struct perf_tool *tool __maybe_unused,
union perf_event *event __maybe_unused,
struct perf_sample *sample __maybe_unused,
@@ -2459,6 +2463,8 @@ static int __cmd_inject(struct perf_inject *inject)
}
}
+
+
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,
@@ -2565,6 +2571,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[] = {
@@ -2572,6 +2580,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. */
@@ -2592,6 +2601,11 @@ int cmd_inject(int argc, const char **argv)
if (argc)
usage_with_options(inject_usage, options);
+ if (inject.aslr && inject.convert_callchain) {
+ pr_err("Error: --aslr and --convert-callchain are mutually exclusive features.\n");
+ return -EINVAL;
+ }
+
if (inject.strip && !inject.itrace_synth_opts.set) {
pr_err("--strip option requires --itrace option\n");
return -1;
@@ -2685,18 +2699,36 @@ 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;
}
if (zstd_init(&(inject.session->zstd_data), 0) < 0)
pr_warning("Decompression initialization failed.\n");
+ if (inject.aslr) {
+ struct evsel *evsel;
+
+ evlist__for_each_entry(inject.session->evlist, evsel) {
+ if (evsel->core.attr.type == PERF_TYPE_BREAKPOINT)
+ evsel->core.attr.bp_addr = 0;
+ }
+ }
+
/* Save original section info before feature bits change */
ret = save_section_info(&inject);
if (ret)
@@ -2790,6 +2822,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..09b7f2f8fb85
--- /dev/null
+++ b/tools/perf/util/aslr.c
@@ -0,0 +1,1036 @@
+// 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>
+
+/**
+ * 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 machine *machine;
+ 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] __aligned(8);
+ /** @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->machine ^ (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->machine == key2->machine &&
+ RC_CHK_EQUAL(key1->dso, key2->dso) &&
+ key1->invariant == key2->invariant &&
+ key1->pid == key2->pid;
+}
+
+struct top_addresses_key {
+ struct machine *machine;
+ pid_t pid;
+};
+
+static size_t top_addresses__hash(long _key, void *ctx __maybe_unused)
+{
+ struct top_addresses_key *key = (struct top_addresses_key *)_key;
+
+ return (size_t)key->machine ^ key->pid;
+}
+
+static bool top_addresses__equal(long _key1, long _key2, void *ctx __maybe_unused)
+{
+ struct top_addresses_key *key1 = (struct top_addresses_key *)_key1;
+ struct top_addresses_key *key2 = (struct top_addresses_key *)_key2;
+
+ return key1->machine == key2->machine && key1->pid == key2->pid;
+}
+
+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.machine = maps__machine(aslr_thread->maps);
+ 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__findnew_mapping(struct aslr_tool *aslr,
+ struct thread *aslr_thread,
+ u8 cpumode, u64 start,
+ u64 len, u64 pgoff)
+{
+ /* Address location for dso lookup. */
+ struct addr_location al;
+ /* Original ASLR address based key for the remap table. */
+ struct remap_addresses_key remap_key;
+ /* The address in the ASLR sanitized address space less pg_off. */
+ u64 *remapped_invariant_ptr;
+ /* Key for the maximum address in a process. */
+ struct top_addresses_key top_addr_key;
+ /* Value in top address table. */
+ u64 *pmax = NULL;
+ /* Address in ASLR sanitized address space. */
+ u64 remap_addr;
+ /* Potentially allocated remap table key. */
+ struct remap_addresses_key *new_remap_key = NULL;
+ /*
+ * Potentially allocated remap table key.
+ * TODO: Avoid allocation necessary for perf 32-bit binary support.
+ */
+ u64 *new_remap_val = NULL;
+ int err;
+
+ if (!aslr_thread)
+ return 0;
+
+ /* The key to look up an incoming address to the outgoing value. */
+ addr_location__init(&al);
+ remap_key.machine = maps__machine(aslr_thread->maps);
+ remap_key.pid = (cpumode == PERF_RECORD_MISC_KERNEL) ? kernel_pid : aslr_thread->pid_;
+ if (thread__find_map(aslr_thread, cpumode, start, &al)) {
+ remap_key.dso = map__dso(al.map);
+ remap_key.invariant = map__start(al.map) - map__pgoff(al.map);
+ } else {
+ remap_key.dso = NULL;
+ remap_key.invariant = start - pgoff;
+ }
+
+ /* The key to look up top allocated address. */
+ top_addr_key.machine = remap_key.machine;
+ top_addr_key.pid = remap_key.pid;
+
+ if (hashmap__find(&aslr->remap_addresses, &remap_key, &remapped_invariant_ptr)) {
+ /* Mmap already exists. */
+ u64 calculated_max;
+
+ remap_addr = *remapped_invariant_ptr + (al.map ? map__pgoff(al.map) : pgoff);
+ calculated_max = remap_addr + len;
+
+ /* See if top mapping was expanded. */
+ if (hashmap__find(&aslr->top_addresses, &top_addr_key, &pmax)) {
+ if (calculated_max > *pmax)
+ *pmax = calculated_max;
+ }
+ addr_location__exit(&al);
+ return remap_addr;
+ }
+ /* No mmap, create an entry from the top address. */
+ if (hashmap__find(&aslr->top_addresses, &top_addr_key, &pmax)) {
+ /* Current max allocated mmap address within the process. */
+ remap_addr = *pmax;
+
+ /* Give 1 page gap from current max page. */
+ remap_addr = round_up_to_page_size(remap_addr);
+ remap_addr += page_size;
+ if (remap_addr + len > *pmax)
+ *pmax = remap_addr + len;
+ } else {
+ /* First address of the process, allocate key and first top address. */
+ struct top_addresses_key *tk;
+
+ remap_addr = (cpumode == PERF_RECORD_MISC_KERNEL) ?
+ kernel_space_start : user_space_start;
+ remap_addr = round_up_to_page_size(remap_addr);
+
+ tk = malloc(sizeof(*tk));
+ pmax = malloc(sizeof(u64));
+ if (!tk || !pmax) {
+ err = -ENOMEM;
+ } else {
+ *tk = top_addr_key;
+ *pmax = remap_addr + len;
+ err = hashmap__insert(&aslr->top_addresses, tk, pmax, HASHMAP_ADD, NULL, NULL);
+ }
+ if (err) {
+ errno = -err;
+ pr_err("Failure to add ASLR process top address %m\n");
+ free(tk);
+ free(pmax);
+ addr_location__exit(&al);
+ return 0;
+ }
+ }
+ /* Create rmeapping entry. */
+ new_remap_key = malloc(sizeof(*new_remap_key));
+ new_remap_val = malloc(sizeof(u64));
+ if (!new_remap_key || !new_remap_val) {
+ err = -ENOMEM;
+ } else {
+ *new_remap_key = remap_key;
+ new_remap_key->dso = dso__get(remap_key.dso);
+ if (cpumode == PERF_RECORD_MISC_KERNEL) {
+ if (al.map)
+ *new_remap_val = remap_addr - (start - map__start(al.map)) - map__pgoff(al.map);
+ else
+ *new_remap_val = remap_addr;
+ } else {
+ *new_remap_val = remap_addr - (al.map ? map__pgoff(al.map) : pgoff);
+ }
+ err = hashmap__add(&aslr->remap_addresses, new_remap_key, new_remap_val);
+ if (err)
+ dso__put(new_remap_key->dso);
+ }
+ if (err) {
+ errno = -err;
+ pr_err("Failure to add ASLR remapping %m\n");
+ free(new_remap_key);
+ free(new_remap_val);
+ addr_location__exit(&al);
+ return 0;
+ }
+ 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__findnew_mapping(aslr, thread, cpumode,
+ event->mmap.start,
+ event->mmap.len,
+ event->mmap.pgoff);
+ if (cpumode == PERF_RECORD_MISC_KERNEL || cpumode == PERF_RECORD_MISC_GUEST_KERNEL)
+ new_event->mmap.pgoff = new_event->mmap.start;
+ 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__findnew_mapping(aslr, thread, cpumode,
+ event->mmap2.start,
+ event->mmap2.len,
+ event->mmap2.pgoff);
+ if (cpumode == PERF_RECORD_MISC_KERNEL || cpumode == PERF_RECORD_MISC_GUEST_KERNEL)
+ new_event->mmap2.pgoff = new_event->mmap2.start;
+ 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 __maybe_unused,
+ union perf_event *event __maybe_unused,
+ struct perf_sample *sample __maybe_unused,
+ struct machine *machine __maybe_unused)
+{
+ /* Drop in case the instruction encodes an ASLR revealing address. */
+ return 0;
+}
+
+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;
+
+ 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__findnew_mapping(aslr, thread,
+ PERF_RECORD_MISC_KERNEL,
+ event->ksymbol.addr,
+ event->ksymbol.len,
+ /*pgoff=*/0);
+
+ 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:
+ if (cntr + 1 >= nr) {
+ pr_debug("Truncated callchain deferred cookie context\n");
+ ret = 0;
+ goto out_put;
+ }
+ /*
+ * Immediately followed by a 64-bit
+ * stitching cookie. Skip/Copy it!
+ */
+ CHECK_BOUNDS(1, 1);
+ out_array[j++] = in_array[i++];
+ cntr++;
+ cpumode = PERF_RECORD_MISC_USER;
+ 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 int skipn(int fd, off_t n)
+{
+ char buf[4096];
+ ssize_t ret;
+
+ while (n > 0) {
+ ret = read(fd, buf, min(n, (off_t)sizeof(buf)));
+ if (ret <= 0)
+ return ret;
+ n -= ret;
+ }
+
+ return 0;
+}
+
+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)) {
+ /* Copy behavior of the stub by reading all pipe 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->pkey);
+ zfree(&cur->pvalue);
+ }
+
+ hashmap__clear(&aslr->remap_addresses);
+ hashmap__clear(&aslr->top_addresses);
+ machines__destroy_kernel_maps(&aslr->machines);
+ 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.563.g4f69b47b94-goog
^ permalink raw reply related [flat|nested] 27+ messages in thread
* [PATCH v6 5/6] perf test: Add inject ASLR test
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (3 preceding siblings ...)
2026-05-08 8:27 ` [PATCH v6 4/6] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-05-08 8:27 ` Ian Rogers
2026-05-08 13:29 ` James Clark
2026-05-11 7:34 ` Namhyung Kim
2026-05-08 8:27 ` [PATCH v6 6/6] perf aslr: Strip sample registers Ian Rogers
5 siblings, 2 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-08 8:27 UTC (permalink / raw)
To: irogers, acme, gmx, james.clark, namhyung
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
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 (utilizing a dedicated kernel-intensive VFS dd workload
to guarantee continuous timer interrupts sampling flow inside kernel privilege states).
- Kernel report consistency with address normalization.
The test suite is hardened with global 'set -o pipefail' assertions to catch
pipeline failures, stream-consuming awk processors to handle SIGPIPE signals,
and a dedicated pipe output scenario validating raw 'perf inject -o -' stdout
streams.
Assisted-by: Gemini-CLI:Google Gemini 3
Signed-off-by: Ian Rogers <irogers@google.com>
---
v6: Refactor kernel-space sampling test cases to utilize a dedicated
system-call intensive VFS dd workload (kprog) instead of purely
userspace-bound tight loops, guaranteeing high-density kernel
privilege state sampling streams and eliminating intermittent
execution flakiness dropouts.
v5: Harden test suite verification pipelines by upgrading report
checks to strict sorted line-by-line diff comparisons to
accommodate remapped pointer shifts. Append || true fallback
operators to grep-v filtering pipelines to prevent the shell test
from spuriously aborting under set -o pipefail on empty inputs,
ensuring graceful failure checks trigger correctly.
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 | 460 ++++++++++++++++++++++++++
1 file changed, 460 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..6363a0f69d2b
--- /dev/null
+++ b/tools/perf/tests/shell/inject_aslr.sh
@@ -0,0 +1,460 @@
+#!/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
+kprog="dd if=/dev/zero of=/dev/null bs=1M count=500"
+
+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}" ${kprog} > "${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}" ${kprog} > "${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"
+
+ grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report1_clean}" | \
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | \
+ sort > "${report1_norm}" || true
+ grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report2_clean}" | \
+ awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | \
+ sort > "${report2_norm}" || true
+
+ 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.563.g4f69b47b94-goog
^ permalink raw reply related [flat|nested] 27+ messages in thread
* [PATCH v6 6/6] perf aslr: Strip sample registers
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
` (4 preceding siblings ...)
2026-05-08 8:27 ` [PATCH v6 5/6] perf test: Add inject ASLR test Ian Rogers
@ 2026-05-08 8:27 ` Ian Rogers
5 siblings, 0 replies; 27+ messages in thread
From: Ian Rogers @ 2026-05-08 8:27 UTC (permalink / raw)
To: irogers, acme, gmx, james.clark, namhyung
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz
When the ASLR tracking tool encounters sample events containing user
or interrupt register dumps (PERF_SAMPLE_REGS_USER /
PERF_SAMPLE_REGS_INTR), it previously dropped the entire sample event
conservatively to prevent absolute virtual memory pointers leakage
embedded inside raw register frames. If a trace session was recorded
with register collection flags enabled, this resulted in 100% sample
drop rates, and this happened by default for ARM64.
Refactor the ASLR tool to strip out obly the register dump payload
words from PERF_RECORD_SAMPLE event streams, automatically shrinking
the output sample header size. Incoming PERF_RECORD_ATTR events are
scrubbed up front to clear the register dump bit selection flags and
masks, and output sample ABI words are safely overwritten to
PERF_SAMPLE_REGS_ABI_NONE. This keeps downstream evsel parsers
perfectly synchronized while retaining full, comprehensive sample
profiles completely clear of secret register data frames.
Verification parity is established inside inject_aslr.sh via a
dedicated sorted report diff comparison validation case proving zero
starvation and absolute secrecy.
Assisted-by: Gemini-CLI:Google Gemini 3
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/builtin-inject.c | 11 ++++++
tools/perf/tests/shell/inject_aslr.sh | 51 +++++++++++++++++++++++++++
tools/perf/util/aslr.c | 27 +++++++-------
3 files changed, 75 insertions(+), 14 deletions(-)
diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c
index 51dcf248b653..7a17ce019657 100644
--- a/tools/perf/builtin-inject.c
+++ b/tools/perf/builtin-inject.c
@@ -2463,6 +2463,17 @@ static int __cmd_inject(struct perf_inject *inject)
}
}
+ if (inject->aslr) {
+ struct evsel *evsel;
+
+ evlist__for_each_entry(session->evlist, evsel) {
+ evsel__reset_sample_bit(evsel, REGS_USER);
+ evsel__reset_sample_bit(evsel, REGS_INTR);
+ evsel->core.attr.sample_regs_user = 0;
+ evsel->core.attr.sample_regs_intr = 0;
+ }
+ }
+
session->header.data_offset = output_data_offset;
diff --git a/tools/perf/tests/shell/inject_aslr.sh b/tools/perf/tests/shell/inject_aslr.sh
index 6363a0f69d2b..323782c3802d 100755
--- a/tools/perf/tests/shell/inject_aslr.sh
+++ b/tools/perf/tests/shell/inject_aslr.sh
@@ -446,6 +446,56 @@ test_kernel_report_aslr() {
fi
}
+test_regs_stripping() {
+ echo "Test user register stripping"
+ local rdata="${temp_dir}/perf.data.regs"
+ local rdata2="${temp_dir}/perf.data.regs.injected"
+ local rdata_clean="${temp_dir}/perf.data.regs.clean"
+
+ if ! perf record --user-regs -o "${rdata}" ${prog} > /dev/null 2>&1; then
+ echo "Skipping user registers test as recording failed (unsupported flag/platform)"
+ return
+ fi
+
+ perf inject -b -i "${rdata}" -o "${rdata_clean}"
+ perf inject -v -b --aslr -i "${rdata}" -o "${rdata2}"
+
+ local report1="${temp_dir}/report_regs1"
+ local report2="${temp_dir}/report_regs2"
+ local report1_clean="${temp_dir}/report_regs1.clean"
+ local report2_clean="${temp_dir}/report_regs2.clean"
+ local diff_file="${temp_dir}/diff_regs"
+
+ perf report -i "${rdata_clean}" --stdio > "${report1}" 2>/dev/null || true
+ perf report -i "${rdata2}" --stdio > "${report2}" 2>/dev/null || true
+
+ grep '%' "${report1}" | grep -v '^#' | grep -v -E '0x[0-9a-f]{8,}|0000000000000000' | sort > "${report1_clean}" || true
+ grep '%' "${report2}" | grep -v '^#' | grep -v -E '0x[0-9a-f]{8,}|0000000000000000' | sort > "${report2_clean}" || true
+
+ diff -u -w "${report1_clean}" "${report2_clean}" > "${diff_file}" || true
+
+ if [ ! -s "${report1_clean}" ]; then
+ echo "User registers stripping test [Failed - profile trace starved/empty]"
+ err=1
+ return
+ elif [ -s "${diff_file}" ]; then
+ echo "User registers stripping test [Failed - report parsing differs]"
+ echo "Showing first 20 lines of diff:"
+ head -n 20 "${diff_file}"
+ err=1
+ return
+ fi
+
+ local script_dump="${temp_dir}/script_regs_dump"
+ perf script -D -i "${rdata2}" > "${script_dump}" 2>/dev/null || true
+ if grep -q "PERF_SAMPLE_REGS_USER" "${script_dump}"; then
+ echo "User registers stripping test [Failed - register dumps still present]"
+ err=1
+ else
+ echo "User registers stripping test [Success]"
+ fi
+}
+
test_basic_aslr
test_pipe_aslr
test_callchain_aslr
@@ -455,6 +505,7 @@ test_pipe_out_report_aslr
test_dropped_samples
test_kernel_aslr
test_kernel_report_aslr
+test_regs_stripping
cleanup
exit $err
diff --git a/tools/perf/util/aslr.c b/tools/perf/util/aslr.c
index 09b7f2f8fb85..e5369589a733 100644
--- a/tools/perf/util/aslr.c
+++ b/tools/perf/util/aslr.c
@@ -751,18 +751,13 @@ static int aslr_tool__process_sample(const struct perf_tool *tool, union perf_ev
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) {
+ if (nr > max_i - i) {
ret = -EFAULT;
goto out_put;
}
- memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
i += nr;
- j += nr;
+ out_array[j-1] = PERF_SAMPLE_REGS_ABI_NONE;
}
- /* 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;
@@ -806,18 +801,13 @@ static int aslr_tool__process_sample(const struct perf_tool *tool, union perf_ev
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) {
+ if (nr > max_i - i) {
ret = -EFAULT;
goto out_put;
}
- memcpy(&out_array[j], &in_array[i], nr * sizeof(u64));
i += nr;
- j += nr;
+ out_array[j-1] = PERF_SAMPLE_REGS_ABI_NONE;
}
- /* 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 */
@@ -907,6 +897,15 @@ static int aslr_tool__process_attr(const struct perf_tool *tool,
if (new_event->attr.attr.type == PERF_TYPE_BREAKPOINT)
new_event->attr.attr.bp_addr = 0; /* Conservatively remove addresses. */
+ if (new_event->attr.attr.sample_type & PERF_SAMPLE_REGS_USER) {
+ new_event->attr.attr.sample_type &= ~PERF_SAMPLE_REGS_USER;
+ new_event->attr.attr.sample_regs_user = 0;
+ }
+ if (new_event->attr.attr.sample_type & PERF_SAMPLE_REGS_INTR) {
+ new_event->attr.attr.sample_type &= ~PERF_SAMPLE_REGS_INTR;
+ new_event->attr.attr.sample_regs_intr = 0;
+ }
+
return delegate->attr(delegate, new_event, pevlist);
}
--
2.54.0.563.g4f69b47b94-goog
^ permalink raw reply related [flat|nested] 27+ messages in thread
* Re: [PATCH v5 5/5] perf test: Add inject ASLR test
2026-05-07 16:17 ` Ian Rogers
@ 2026-05-08 10:42 ` James Clark
2026-05-08 10:49 ` James Clark
0 siblings, 1 reply; 27+ messages in thread
From: James Clark @ 2026-05-08 10:42 UTC (permalink / raw)
To: Ian Rogers
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz, acme, gmx, namhyung
On 07/05/2026 5:17 pm, Ian Rogers wrote:
> On Thu, May 7, 2026 at 8:58 AM James Clark <james.clark@linaro.org> wrote:
>>
>>
>>
>> On 06/05/2026 1:45 am, Ian Rogers wrote:
>>> 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' assertions to catch
>>> pipeline failures, stream-consuming awk processors to handle SIGPIPE signals,
>>> and a dedicated pipe output scenario validating raw 'perf inject -o -' stdout
>>> streams.
>>>
>>> Assisted-by: Gemini-CLI:Google Gemini 3
>>> Signed-off-by: Ian Rogers <irogers@google.com>
>>> ---
>>> v5: Harden test suite verification pipelines by upgrading report checks to
>>> strict sorted line-by-line diff comparisons to accommodate remapped pointer
>>> shifts. Append || true fallback operators to grep-v filtering pipelines to
>>> prevent the shell test from spuriously aborting under set -o pipefail on
>>> empty inputs, ensuring graceful failure checks trigger correctly.
>>> 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..cdc3aa94de63
>>> --- /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]"
>>
>> Hi Ian,
>>
>> This test fails on Arm. I believe it's because on Arm we request the
>> link register to be sampled with frame pointer unwinds. Then the aslr
>> tool drops all the samples because it sees that user regs were sampled:
>>
>> /* TODO: can this be less conservative? */
>> pr_debug("Dropping regs user sample as possible ASLR leak\n");
>> ret = 0;
>> goto out_put;
>>
>> I think maybe that comment is onto something. Perhaps the user regs can
>> be zeroed instead of dropping the sample. Then the frame pointer unwind
>> will still work on Arm and the aslr test will pass. We just won't be
>> able to use the link register to add the leaf frame caller, but that's
>> not a big deal.
>
> Thanks James. I'm working on a new version of the patches, but I'm
> having delays getting the AI to approve the changes.
>
> ARM does what? Ah, I knew this and also it didn't really register. I'm
> wondering now if we can put the machinery behind "EM_HOST ==
> EM_AARCH64":
> https://lore.kernel.org/all/20211217154521.80603-2-german.gomez@arm.com/
> as it seems a mechanism that would benefit other architectures such as
> ARM32 :-) And I have my mission to make tools/perf/arch disappear as
> much as is humanly possible.
Yeah that makes sense, the change you sent looks good.
> I also imagine the problem the link register solves for perf happens
> for BPF, so perhaps this ability shouldn't be encouraged.
Not sure what you mean by this, do you mean adding the link register
shoudln't be encouraged, or the compiler dropping the stack frame? Or
just the weak function style?
>
> I think rather than zeroing the register values it would be better to
> just remove them from the output events. I'll try to add that support
> as having this test break on ARM isn't desirable.
>
Makes sense too. I suppose data being there but zeroed could be slightly
more confusing than just dropping the sample.
I don't know if modifying the sample type to remove
PERF_SAMPLE_REGS_USER and emitting the rest could be an option? It might
be more robust to cases when things are auto added to the sample by
Perf. For example all the aux stuff has custom setup functions that add
who knows what options to the events.
> Thanks,
> Ian
>
>> James
>>
>>> + 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"
>>> +
>>> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report1_clean}" | \
>>> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report1_norm}" || true
>>> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report2_clean}" | \
>>> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | sort > "${report2_norm}" || true
>>> +
>>> + 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
>>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v5 5/5] perf test: Add inject ASLR test
2026-05-08 10:42 ` James Clark
@ 2026-05-08 10:49 ` James Clark
0 siblings, 0 replies; 27+ messages in thread
From: James Clark @ 2026-05-08 10:49 UTC (permalink / raw)
To: Ian Rogers
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz, acme, gmx, namhyung
On 08/05/2026 11:42 am, James Clark wrote:
>
>
> On 07/05/2026 5:17 pm, Ian Rogers wrote:
>> On Thu, May 7, 2026 at 8:58 AM James Clark <james.clark@linaro.org>
>> wrote:
>>>
>>>
>>>
>>> On 06/05/2026 1:45 am, Ian Rogers wrote:
>>>> 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' assertions
>>>> to catch
>>>> pipeline failures, stream-consuming awk processors to handle SIGPIPE
>>>> signals,
>>>> and a dedicated pipe output scenario validating raw 'perf inject -o
>>>> -' stdout
>>>> streams.
>>>>
>>>> Assisted-by: Gemini-CLI:Google Gemini 3
>>>> Signed-off-by: Ian Rogers <irogers@google.com>
>>>> ---
>>>> v5: Harden test suite verification pipelines by upgrading report
>>>> checks to
>>>> strict sorted line-by-line diff comparisons to accommodate
>>>> remapped pointer
>>>> shifts. Append || true fallback operators to grep-v filtering
>>>> pipelines to
>>>> prevent the shell test from spuriously aborting under set -o
>>>> pipefail on
>>>> empty inputs, ensuring graceful failure checks trigger correctly.
>>>> 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..cdc3aa94de63
>>>> --- /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]"
>>>
>>> Hi Ian,
>>>
>>> This test fails on Arm. I believe it's because on Arm we request the
>>> link register to be sampled with frame pointer unwinds. Then the aslr
>>> tool drops all the samples because it sees that user regs were sampled:
>>>
>>> /* TODO: can this be less conservative? */
>>> pr_debug("Dropping regs user sample as possible ASLR leak\n");
>>> ret = 0;
>>> goto out_put;
>>>
>>> I think maybe that comment is onto something. Perhaps the user regs can
>>> be zeroed instead of dropping the sample. Then the frame pointer unwind
>>> will still work on Arm and the aslr test will pass. We just won't be
>>> able to use the link register to add the leaf frame caller, but that's
>>> not a big deal.
>>
>> Thanks James. I'm working on a new version of the patches, but I'm
>> having delays getting the AI to approve the changes.
>>
>> ARM does what? Ah, I knew this and also it didn't really register. I'm
>> wondering now if we can put the machinery behind "EM_HOST ==
>> EM_AARCH64":
>> https://lore.kernel.org/all/20211217154521.80603-2-german.gomez@arm.com/
>> as it seems a mechanism that would benefit other architectures such as
>> ARM32 :-) And I have my mission to make tools/perf/arch disappear as
>> much as is humanly possible.
>
> Yeah that makes sense, the change you sent looks good.
>
>> I also imagine the problem the link register solves for perf happens
>> for BPF, so perhaps this ability shouldn't be encouraged.
>
> Not sure what you mean by this, do you mean adding the link register
> shoudln't be encouraged, or the compiler dropping the stack frame? Or
> just the weak function style?
>
>>
>> I think rather than zeroing the register values it would be better to
>> just remove them from the output events. I'll try to add that support
>> as having this test break on ARM isn't desirable.
>>
>
> Makes sense too. I suppose data being there but zeroed could be slightly
> more confusing than just dropping the sample.
>
> I don't know if modifying the sample type to remove
> PERF_SAMPLE_REGS_USER and emitting the rest could be an option? It might
> be more robust to cases when things are auto added to the sample by
> Perf. For example all the aux stuff has custom setup functions that add
> who knows what options to the events.
Nevermind, I see this is what is done on V6
>
>
>> Thanks,
>> Ian
>>
>>> James
>>>
>>>> + 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"
>>>> +
>>>> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report1_clean}" | \
>>>> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/,
>>>> "[kernel]", $0); print}' | sort > "${report1_norm}" || true
>>>> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report2_clean}" | \
>>>> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/,
>>>> "[kernel]", $0); print}' | sort > "${report2_norm}" || true
>>>> +
>>>> + 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
>>>
>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v6 3/6] perf maps: Add maps__mutate_mapping
2026-05-08 8:27 ` [PATCH v6 3/6] perf maps: Add maps__mutate_mapping Ian Rogers
@ 2026-05-08 10:57 ` James Clark
2026-05-11 7:07 ` Namhyung Kim
1 sibling, 0 replies; 27+ messages in thread
From: James Clark @ 2026-05-08 10:57 UTC (permalink / raw)
To: Ian Rogers
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz, acme, gmx, namhyung
On 08/05/2026 9:27 am, Ian Rogers wrote:
> During kernel ELF symbol parsing (dso__process_kernel_symbol), proc
> kallsyms image loading (dso__load_kernel_sym,
> dso__load_guest_kernel_sym), and dynamic kernel memory map alignment
> updates (machine__update_kernel_mmap), the loader directly modifies
> live virtual address boundary keys fields on map objects. If these
> boundaries are mutated while the map pointer actively resides inside
> the parent maps cache array list (kmaps) outside of any lock closure,
> an unsafe concurrent window is exposed where parallel worker lookup
> threads (e.g., inside perf top) can mistakenly assume the cache
> remains sorted based on stale parameters, executing binary search
> queries (bsearch) across an unsorted range and triggering lookups
> failure.
>
> Fix this by introducing a thread-safe, atomic transactional framework
> routine maps__mutate_mapping() that explicitly acquires the parent
> maps write semaphore lock, executes an incoming mutation callback
> block to perform the field updates under full lock protection, and
> invalidates the sorted tracking flags prior to releasing the write
> lock. This guarantees absolute atomic synchronization invariants,
> completely closing the concurrent lookup race window. The adjacent
> module alignment pass inside machine__create_kernel_maps() is safely
> preserved as a high-performance lockless pass, as its invocation
> lifecycle bounds remain strictly single-threaded by contract during
> session initialization construction. There is a potential for self
> deadlock if maps__mutate_mapping is called with the lock held, such as
> with maps__for_each_map but this problem also existed with the
> previous remove and insert approaches.
>
> Fixes: 39b12f781271 ("perf tools: Make it possible to read object code from vmlinux")
> Signed-off-by: Ian Rogers <irogers@google.com>
> ---
> tools/perf/util/machine.c | 32 +++++++++++++++++-----------
> tools/perf/util/maps.c | 26 +++++++++++++++++++++++
> tools/perf/util/maps.h | 2 ++
> tools/perf/util/symbol-elf.c | 41 +++++++++++++++++++++++-------------
> tools/perf/util/symbol.c | 17 +++++++++++----
> 5 files changed, 87 insertions(+), 31 deletions(-)
>
> diff --git a/tools/perf/util/machine.c b/tools/perf/util/machine.c
> index e76f8c86e62a..8d4452c70cb5 100644
> --- a/tools/perf/util/machine.c
> +++ b/tools/perf/util/machine.c
> @@ -1522,22 +1522,30 @@ static void machine__set_kernel_mmap(struct machine *machine,
> map__set_end(machine->vmlinux_map, ~0ULL);
> }
>
> -static int machine__update_kernel_mmap(struct machine *machine,
> - u64 start, u64 end)
> +struct kernel_mmap_mutation_ctx {
> + u64 start;
> + u64 end;
> +};
> +
> +static int kernel_mmap_mutate_cb(struct map *map, void *data)
> {
> - struct map *orig, *updated;
> - int err;
> + struct kernel_mmap_mutation_ctx *ctx = data;
>
> - orig = machine->vmlinux_map;
> - updated = map__get(orig);
> + map__set_start(map, ctx->start);
> + map__set_end(map, ctx->end);
> + if (ctx->start == 0 && ctx->end == 0)
> + map__set_end(map, ~0ULL);
> + return 0;
> +}
>
> - machine->vmlinux_map = updated;
> - maps__remove(machine__kernel_maps(machine), orig);
> - machine__set_kernel_mmap(machine, start, end);
> - err = maps__insert(machine__kernel_maps(machine), updated);
> - map__put(orig);
> +static int machine__update_kernel_mmap(struct machine *machine,
> + u64 start, u64 end)
> +{
> + struct kernel_mmap_mutation_ctx ctx = { .start = start, .end = end };
>
> - return err;
> + return maps__mutate_mapping(machine__kernel_maps(machine),
> + machine->vmlinux_map,
> + kernel_mmap_mutate_cb, &ctx);
> }
>
> int machine__create_kernel_maps(struct machine *machine)
> diff --git a/tools/perf/util/maps.c b/tools/perf/util/maps.c
> index 81a97ac34077..91345a773aa2 100644
> --- a/tools/perf/util/maps.c
> +++ b/tools/perf/util/maps.c
> @@ -575,6 +575,32 @@ void maps__remove(struct maps *maps, struct map *map)
> #endif
> }
>
> +int maps__mutate_mapping(struct maps *maps, struct map *map,
> + int (*mutate_cb)(struct map *map, void *data), void *data)
> +{
> + int err = 0;
> +
> + if (maps)
> + down_write(maps__lock(maps));
> +
> + err = mutate_cb(map, data);
Hi Ian,
I get this error when building with LLVM=1 on Ubuntu clang version
18.1.8 (11~20.04.2):
util/maps.c:586:8: error: mutex 'maps__lock(maps)' is not held on every
path through here [-Werror,-Wthread-safety-analysis]
586 | err = mutate_cb(map, data);
| ^
util/maps.c:584:3: note: mutex acquired here
584 | down_write(maps__lock(maps));
| ^
util/maps.c:594:3: error: releasing mutex 'maps__lock(maps)' that was
not held [-Werror,-Wthread-safety-analysis]
594 | up_write(maps__lock(maps));
| ^
2 errors generated.
> +
> + if (maps) {
> + RC_CHK_ACCESS(maps)->maps_by_address_sorted = false;
> + RC_CHK_ACCESS(maps)->maps_by_name_sorted = false;
> + }
> +
> + if (maps)
> + up_write(maps__lock(maps));
> +
> +#ifdef HAVE_LIBDW_SUPPORT
> + if (maps)
> + libdw__invalidate_dwfl(maps, maps__libdw_addr_space_dwfl(maps));
> +#endif
> +
> + return err;
> +}
> +
> bool maps__empty(struct maps *maps)
> {
> bool res;
> diff --git a/tools/perf/util/maps.h b/tools/perf/util/maps.h
> index 20c52084ba9e..de74ccbb8a12 100644
> --- a/tools/perf/util/maps.h
> +++ b/tools/perf/util/maps.h
> @@ -61,6 +61,8 @@ size_t maps__fprintf(struct maps *maps, FILE *fp);
>
> int maps__insert(struct maps *maps, struct map *map);
> void maps__remove(struct maps *maps, struct map *map);
> +int maps__mutate_mapping(struct maps *maps, struct map *map,
> + int (*mutate_cb)(struct map *map, void *data), void *data);
>
> struct map *maps__find(struct maps *maps, u64 addr);
> struct symbol *maps__find_symbol(struct maps *maps, u64 addr, struct map **mapp);
> diff --git a/tools/perf/util/symbol-elf.c b/tools/perf/util/symbol-elf.c
> index 7afa8a117139..dc4ab58857b3 100644
> --- a/tools/perf/util/symbol-elf.c
> +++ b/tools/perf/util/symbol-elf.c
> @@ -1341,6 +1341,24 @@ static u64 ref_reloc(struct kmap *kmap)
> void __weak arch__sym_update(struct symbol *s __maybe_unused,
> GElf_Sym *sym __maybe_unused) { }
>
> +struct remap_kernel_ctx {
> + u64 sh_addr;
> + u64 sh_size;
> + u64 sh_offset;
> + struct kmap *kmap;
> +};
> +
> +static int remap_kernel_cb(struct map *map, void *data)
> +{
> + struct remap_kernel_ctx *ctx = data;
> +
> + map__set_start(map, ctx->sh_addr + ref_reloc(ctx->kmap));
> + map__set_end(map, map__start(map) + ctx->sh_size);
> + map__set_pgoff(map, ctx->sh_offset);
> + map__set_mapping_type(map, MAPPING_TYPE__DSO);
> + return 0;
> +}
> +
> static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
> GElf_Sym *sym, GElf_Shdr *shdr,
> struct maps *kmaps, struct kmap *kmap,
> @@ -1371,22 +1389,15 @@ static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
> * map to the kernel dso.
> */
> if (*remap_kernel && dso__kernel(dso) && !kmodule) {
> + struct remap_kernel_ctx ctx = {
> + .sh_addr = shdr->sh_addr,
> + .sh_size = shdr->sh_size,
> + .sh_offset = shdr->sh_offset,
> + .kmap = kmap
> + };
> +
> *remap_kernel = false;
> - map__set_start(map, shdr->sh_addr + ref_reloc(kmap));
> - map__set_end(map, map__start(map) + shdr->sh_size);
> - map__set_pgoff(map, shdr->sh_offset);
> - map__set_mapping_type(map, MAPPING_TYPE__DSO);
> - /* Ensure maps are correctly ordered */
> - if (kmaps) {
> - int err;
> - struct map *tmp = map__get(map);
> -
> - maps__remove(kmaps, map);
> - err = maps__insert(kmaps, map);
> - map__put(tmp);
> - if (err)
> - return err;
> - }
> + maps__mutate_mapping(kmaps, map, remap_kernel_cb, &ctx);
> }
>
> /*
> diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
> index fcaeeddbbb6b..09b93e844887 100644
> --- a/tools/perf/util/symbol.c
> +++ b/tools/perf/util/symbol.c
> @@ -48,6 +48,13 @@
> #include <symbol/kallsyms.h>
> #include <sys/utsname.h>
>
> +static int map_fixup_cb(struct map *map, void *data __maybe_unused)
> +{
> + map__fixup_start(map);
> + map__fixup_end(map);
> + return 0;
> +}
> +
> static int dso__load_kernel_sym(struct dso *dso, struct map *map);
> static int dso__load_guest_kernel_sym(struct dso *dso, struct map *map);
> static bool symbol__is_idle(const char *name);
> @@ -2121,10 +2128,11 @@ static int dso__load_kernel_sym(struct dso *dso, struct map *map)
> free(kallsyms_allocated_filename);
>
> if (err > 0 && !dso__is_kcore(dso)) {
> + struct maps *kmaps = map__kmaps(map);
> +
> dso__set_binary_type(dso, DSO_BINARY_TYPE__KALLSYMS);
> dso__set_long_name(dso, DSO__NAME_KALLSYMS, false);
> - map__fixup_start(map);
> - map__fixup_end(map);
> + maps__mutate_mapping(kmaps, map, map_fixup_cb, NULL);
> }
>
> return err;
> @@ -2164,10 +2172,11 @@ static int dso__load_guest_kernel_sym(struct dso *dso, struct map *map)
> if (err > 0)
> pr_debug("Using %s for symbols\n", kallsyms_filename);
> if (err > 0 && !dso__is_kcore(dso)) {
> + struct maps *kmaps = map__kmaps(map);
> +
> dso__set_binary_type(dso, DSO_BINARY_TYPE__GUEST_KALLSYMS);
> dso__set_long_name(dso, machine->mmap_name, false);
> - map__fixup_start(map);
> - map__fixup_end(map);
> + maps__mutate_mapping(kmaps, map, map_fixup_cb, NULL);
> }
>
> return err;
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v6 5/6] perf test: Add inject ASLR test
2026-05-08 8:27 ` [PATCH v6 5/6] perf test: Add inject ASLR test Ian Rogers
@ 2026-05-08 13:29 ` James Clark
2026-05-08 14:29 ` James Clark
2026-05-11 7:34 ` Namhyung Kim
1 sibling, 1 reply; 27+ messages in thread
From: James Clark @ 2026-05-08 13:29 UTC (permalink / raw)
To: Ian Rogers
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz, acme, gmx, namhyung
On 08/05/2026 9:27 am, Ian Rogers wrote:
> 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 (utilizing a dedicated kernel-intensive VFS dd workload
> to guarantee continuous timer interrupts sampling flow inside kernel privilege states).
> - Kernel report consistency with address normalization.
>
> The test suite is hardened with global 'set -o pipefail' assertions to catch
> pipeline failures, stream-consuming awk processors to handle SIGPIPE signals,
> and a dedicated pipe output scenario validating raw 'perf inject -o -' stdout
> streams.
>
> Assisted-by: Gemini-CLI:Google Gemini 3
> Signed-off-by: Ian Rogers <irogers@google.com>
> ---
> v6: Refactor kernel-space sampling test cases to utilize a dedicated
> system-call intensive VFS dd workload (kprog) instead of purely
> userspace-bound tight loops, guaranteeing high-density kernel
> privilege state sampling streams and eliminating intermittent
> execution flakiness dropouts.
>
Hi Ian,
V5 passed on X86, but now I get this test failing about 50% of the time
with output like:
Test user register stripping
User registers stripping test [Failed - report parsing differs]
Showing first 20 lines of diff:
--- /tmp/perf-test-aslr.ssH9urcfri/report_regs1.clean 2026-05-08
14:14:02.127298207 +0100
+++ /tmp/perf-test-aslr.ssH9urcfri/report_regs2.clean 2026-05-08
14:14:02.129298219 +0100
@@ -30,8 +30,8 @@
0.02% perf ld-linux-x86-64.so.2 [.] mmap64
0.02% perf-noploop [kernel.kallsyms] [k] kmem_cache_free
0.02% perf-noploop [kernel.kallsyms] [k] nohz_balancer_kick
- 0.02% perf-noploop [kernel.kallsyms] [k] pvclock_gtod_notify
0.02% perf-noploop [kernel.kallsyms] [k] try_to_wake_up
+ 0.02% perf-noploop [kvm] [k] pvclock_gtod_notify
0.02% perf-noploop libc.so.6 [.] __cxa_finalize
0.04% perf ld-linux-x86-64.so.2 [.] strcmp
0.05% perf libLLVM-15.so.1 [.]
llvm::StringMapImpl::LookupBucketFor(llvm::StringRef)
---- end ----
or:
Test user register stripping
User registers stripping test [Failed - report parsing differs]
Showing first 20 lines of diff:
--- /tmp/perf-test-aslr.NoDUUXtHyh/report_regs1.clean 2026-05-08
14:05:31.109246491 +0100
+++ /tmp/perf-test-aslr.NoDUUXtHyh/report_regs2.clean 2026-05-08
14:05:31.111246503 +0100
@@ -2,8 +2,8 @@
0.01% perf [kernel.kallsyms] [k]
find_mergeable_anon_vma
0.01% perf [kernel.kallsyms] [k] finish_fault
0.01% perf [kernel.kallsyms] [k]
pte_offset_map_rw_nolock
+ 0.02% perf [amdgpu] [k] amdgpu_device_rreg
0.02% perf [kernel.kallsyms] [k]
__alloc_frozen_pages_noprof
- 0.02% perf [kernel.kallsyms] [k] amdgpu_device_rreg
0.02% perf [kernel.kallsyms] [k]
__build_id_parse.isra.0
0.02% perf [kernel.kallsyms] [k] filemap_get_entry
0.02% perf [kernel.kallsyms] [k] filemap_map_pages
---- end ----
And on Arm I get a hang/infinite loop every time in "Test kernel ASLR
remapping". Looks like it could be related to the changes in V6 as I
didn't see it on V5:
#0 __read_once_size (size=4, res=0xffffe56c64a0, p=0xaaaaeaedbab8)
at linux/tools/include/linux/compiler.h:180
#1 atomic_read (v=0xaaaaeaedbab8) at
linux/tools/include/asm-generic/atomic-gcc.h:26
#2 0x0000aaaaaf65cd6c in refcount_read (r=0xaaaaeaedbab8)
at linux/tools/include/linux/refcount.h:70
#3 0x0000aaaaaf65d9dc in check_invariants (maps=0xaaaae7e3b480) at
util/maps.c:114
#4 0x0000aaaaaf65eef8 in maps__insert (maps=0xaaaae7e3b480,
map=0xaaaaec2ccf10) at util/maps.c:536
#5 0x0000aaaaaf62a028 in maps__split_kallsyms (kmaps=0xaaaae7e3b480,
dso=0xaaaae7e3f910, delta=1879048192,
initial_map=0xaaaae7e3fab0) at util/symbol.c:986
#6 0x0000aaaaaf62b550 in __dso__load_kallsyms (dso=0xaaaae7e3f910,
filename=0xaaaae7e55200 "/proc/kallsyms",
map=0xaaaae7e3fab0, no_kcore=false) at util/symbol.c:1530
#7 0x0000aaaaaf62b5bc in dso__load_kallsyms (dso=0xaaaae7e3f910,
filename=0xaaaae7e55200 "/proc/kallsyms",
map=0xaaaae7e3fab0) at util/symbol.c:1536
#8 0x0000aaaaaf62cbc0 in dso__load_kernel_sym (dso=0xaaaae7e3f910,
map=0xaaaae7e3fab0) at util/symbol.c:2125
#9 0x0000aaaaaf62bc5c in dso__load (dso=0xaaaae7e3f910,
map=0xaaaae7e3fab0) at util/symbol.c:1721
#10 0x0000aaaaaf65b98c in map__load (map=0xaaaae7e3fab0) at
util/map.c:351
#11 0x0000aaaaaf5e43cc in thread__find_map (thread=0xaaaae7e443b0,
cpumode=1 '\001', addr=18446603336494207932,
al=0xffffe56c8c28) at util/event.c:744
#12 0x0000aaaaaf5e4810 in machine__resolve (machine=0xaaaae7e3bee0,
al=0xffffe56c8c28, sample=0xffffe56c8df0)
at util/event.c:818
#13 0x0000aaaaaf41d850 in process_sample_event (tool=0xffffe56c93d0,
event=0xffffb1091ec8, sample=0xffffe56c8df0,
evsel=0xaaaae7e3b580, machine=0xaaaae7e3bee0) at
builtin-script.c:2686
#14 0x0000aaaaaf6668f4 in evlist__deliver_sample
(evlist=0xaaaae7e3c550, tool=0xffffe56c93d0, event=0xffffb1091ec8,
sample=0xffffe56c8df0, evsel=0xaaaae7e3b580,
machine=0xaaaae7e3bee0) at util/session.c:1335
#15 0x0000aaaaaf667000 in machines__deliver_event
(machines=0xaaaae7e3bee0, evlist=0xaaaae7e3c550, event=0xffffb1091ec8,
sample=0xffffe56c8df0, tool=0xffffe56c93d0, file_offset=3784,
file_path=0xaaaae7e3b540
"/tmp/perf-test-aslr.J1XB8pvpFy/perf.data2.kernel.FA0Uvd") at
util/session.c:1502
#16 0x0000aaaaaf667538 in perf_session__deliver_event
(session=0xaaaae7e3bca0, event=0xffffb1091ec8,
tool=0xffffe56c93d0, file_offset=3784,
file_path=0xaaaae7e3b540
"/tmp/perf-test-aslr.J1XB8pvpFy/perf.data2.kernel.FA0Uvd") at
util/session.c:1593
#17 0x0000aaaaaf662bbc in ordered_events__deliver_event
(oe=0xaaaae7e3c460, event=0xaaaae7e44740) at util/session.c:134
#18 0x0000aaaaaf672c98 in do_flush (oe=0xaaaae7e3c460,
show_progress=true) at util/ordered-events.c:245
#19 0x0000aaaaaf673048 in __ordered_events__flush (oe=0xaaaae7e3c460,
how=OE_FLUSH__FINAL, timestamp=0)
at util/ordered-events.c:324
#20 0x0000aaaaaf673154 in ordered_events__flush (oe=0xaaaae7e3c460,
how=OE_FLUSH__FINAL) at util/ordered-events.c:342
#21 0x0000aaaaaf669e54 in __perf_session__process_events
(session=0xaaaae7e3bca0) at util/session.c:2508
#22 0x0000aaaaaf66a790 in perf_session__process_events
(session=0xaaaae7e3bca0) at util/session.c:2675
#23 0x0000aaaaaf41f59c in __cmd_script (script=0xffffe56c93d0) at
builtin-script.c:3241
#24 0x0000aaaaaf4242b0 in cmd_script (argc=0, argv=0xffffe56cb370) at
builtin-script.c:4586
#25 0x0000aaaaaf4a03f8 in run_builtin (p=0xaaaaafa14e60
<commands+480>, argc=3, argv=0xffffe56cb370) at perf.c:348
#26 0x0000aaaaaf4a066c in handle_internal_command (argc=3,
argv=0xffffe56cb370) at perf.c:398
#27 0x0000aaaaaf4a0824 in run_argv (argcp=0xffffe56cb1ac,
argv=0xffffe56cb1a0) at perf.c:442
#28 0x0000aaaaaf4a0b4c in main (argc=3, argv=0xffffe56cb370) at
perf.c:549
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v6 5/6] perf test: Add inject ASLR test
2026-05-08 13:29 ` James Clark
@ 2026-05-08 14:29 ` James Clark
0 siblings, 0 replies; 27+ messages in thread
From: James Clark @ 2026-05-08 14:29 UTC (permalink / raw)
To: Ian Rogers
Cc: adrian.hunter, jolsa, linux-kernel, linux-perf-users, mingo,
peterz, acme, gmx, namhyung
On 08/05/2026 2:29 pm, James Clark wrote:
>
>
> On 08/05/2026 9:27 am, Ian Rogers wrote:
>> 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 (utilizing a dedicated kernel-intensive VFS
>> dd workload
>> to guarantee continuous timer interrupts sampling flow inside
>> kernel privilege states).
>> - Kernel report consistency with address normalization.
>>
>> The test suite is hardened with global 'set -o pipefail' assertions to
>> catch
>> pipeline failures, stream-consuming awk processors to handle SIGPIPE
>> signals,
>> and a dedicated pipe output scenario validating raw 'perf inject -o -'
>> stdout
>> streams.
>>
>> Assisted-by: Gemini-CLI:Google Gemini 3
>> Signed-off-by: Ian Rogers <irogers@google.com>
>> ---
>> v6: Refactor kernel-space sampling test cases to utilize a dedicated
>> system-call intensive VFS dd workload (kprog) instead of purely
>> userspace-bound tight loops, guaranteeing high-density kernel
>> privilege state sampling streams and eliminating intermittent
>> execution flakiness dropouts.
>>
>
>
> Hi Ian,
>
> V5 passed on X86, but now I get this test failing about 50% of the time
> with output like:
>
>
> Test user register stripping
> User registers stripping test [Failed - report parsing differs]
> Showing first 20 lines of diff:
> --- /tmp/perf-test-aslr.ssH9urcfri/report_regs1.clean 2026-05-08
> 14:14:02.127298207 +0100
> +++ /tmp/perf-test-aslr.ssH9urcfri/report_regs2.clean 2026-05-08
> 14:14:02.129298219 +0100
> @@ -30,8 +30,8 @@
> 0.02% perf ld-linux-x86-64.so.2 [.] mmap64
> 0.02% perf-noploop [kernel.kallsyms] [k] kmem_cache_free
> 0.02% perf-noploop [kernel.kallsyms] [k] nohz_balancer_kick
> - 0.02% perf-noploop [kernel.kallsyms] [k] pvclock_gtod_notify
> 0.02% perf-noploop [kernel.kallsyms] [k] try_to_wake_up
> + 0.02% perf-noploop [kvm] [k] pvclock_gtod_notify
> 0.02% perf-noploop libc.so.6 [.] __cxa_finalize
> 0.04% perf ld-linux-x86-64.so.2 [.] strcmp
> 0.05% perf libLLVM-15.so.1 [.]
> llvm::StringMapImpl::LookupBucketFor(llvm::StringRef)
> ---- end ----
>
> or:
>
> Test user register stripping
> User registers stripping test [Failed - report parsing differs]
> Showing first 20 lines of diff:
> --- /tmp/perf-test-aslr.NoDUUXtHyh/report_regs1.clean 2026-05-08
> 14:05:31.109246491 +0100
> +++ /tmp/perf-test-aslr.NoDUUXtHyh/report_regs2.clean 2026-05-08
> 14:05:31.111246503 +0100
> @@ -2,8 +2,8 @@
> 0.01% perf [kernel.kallsyms] [k]
> find_mergeable_anon_vma
> 0.01% perf [kernel.kallsyms] [k] finish_fault
> 0.01% perf [kernel.kallsyms] [k]
> pte_offset_map_rw_nolock
> + 0.02% perf [amdgpu] [k] amdgpu_device_rreg
> 0.02% perf [kernel.kallsyms] [k]
> __alloc_frozen_pages_noprof
> - 0.02% perf [kernel.kallsyms] [k] amdgpu_device_rreg
> 0.02% perf [kernel.kallsyms] [k]
> __build_id_parse.isra.0
> 0.02% perf [kernel.kallsyms] [k] filemap_get_entry
> 0.02% perf [kernel.kallsyms] [k] filemap_map_pages
> ---- end ----
>
>
> And on Arm I get a hang/infinite loop every time in "Test kernel ASLR
> remapping". Looks like it could be related to the changes in V6 as I
> didn't see it on V5:
After around an hour it ended up passing successfully, so not an
infinite loop, just very slow.
Then after that, "User registers stripping test" failed the same way as
on x86.
>
> #0 __read_once_size (size=4, res=0xffffe56c64a0, p=0xaaaaeaedbab8)
> at linux/tools/include/linux/compiler.h:180
> #1 atomic_read (v=0xaaaaeaedbab8) at linux/tools/include/asm-
> generic/atomic-gcc.h:26
> #2 0x0000aaaaaf65cd6c in refcount_read (r=0xaaaaeaedbab8)
> at linux/tools/include/linux/refcount.h:70
> #3 0x0000aaaaaf65d9dc in check_invariants (maps=0xaaaae7e3b480) at
> util/maps.c:114
> #4 0x0000aaaaaf65eef8 in maps__insert (maps=0xaaaae7e3b480,
> map=0xaaaaec2ccf10) at util/maps.c:536
> #5 0x0000aaaaaf62a028 in maps__split_kallsyms (kmaps=0xaaaae7e3b480,
> dso=0xaaaae7e3f910, delta=1879048192,
> initial_map=0xaaaae7e3fab0) at util/symbol.c:986
> #6 0x0000aaaaaf62b550 in __dso__load_kallsyms (dso=0xaaaae7e3f910,
> filename=0xaaaae7e55200 "/proc/kallsyms",
> map=0xaaaae7e3fab0, no_kcore=false) at util/symbol.c:1530
> #7 0x0000aaaaaf62b5bc in dso__load_kallsyms (dso=0xaaaae7e3f910,
> filename=0xaaaae7e55200 "/proc/kallsyms",
> map=0xaaaae7e3fab0) at util/symbol.c:1536
> #8 0x0000aaaaaf62cbc0 in dso__load_kernel_sym (dso=0xaaaae7e3f910,
> map=0xaaaae7e3fab0) at util/symbol.c:2125
> #9 0x0000aaaaaf62bc5c in dso__load (dso=0xaaaae7e3f910,
> map=0xaaaae7e3fab0) at util/symbol.c:1721
> #10 0x0000aaaaaf65b98c in map__load (map=0xaaaae7e3fab0) at util/
> map.c:351
> #11 0x0000aaaaaf5e43cc in thread__find_map (thread=0xaaaae7e443b0,
> cpumode=1 '\001', addr=18446603336494207932,
> al=0xffffe56c8c28) at util/event.c:744
> #12 0x0000aaaaaf5e4810 in machine__resolve (machine=0xaaaae7e3bee0,
> al=0xffffe56c8c28, sample=0xffffe56c8df0)
> at util/event.c:818
> #13 0x0000aaaaaf41d850 in process_sample_event (tool=0xffffe56c93d0,
> event=0xffffb1091ec8, sample=0xffffe56c8df0,
> evsel=0xaaaae7e3b580, machine=0xaaaae7e3bee0) at builtin-
> script.c:2686
> #14 0x0000aaaaaf6668f4 in evlist__deliver_sample
> (evlist=0xaaaae7e3c550, tool=0xffffe56c93d0, event=0xffffb1091ec8,
> sample=0xffffe56c8df0, evsel=0xaaaae7e3b580,
> machine=0xaaaae7e3bee0) at util/session.c:1335
> #15 0x0000aaaaaf667000 in machines__deliver_event
> (machines=0xaaaae7e3bee0, evlist=0xaaaae7e3c550, event=0xffffb1091ec8,
> sample=0xffffe56c8df0, tool=0xffffe56c93d0, file_offset=3784,
> file_path=0xaaaae7e3b540 "/tmp/perf-test-aslr.J1XB8pvpFy/
> perf.data2.kernel.FA0Uvd") at util/session.c:1502
> #16 0x0000aaaaaf667538 in perf_session__deliver_event
> (session=0xaaaae7e3bca0, event=0xffffb1091ec8,
> tool=0xffffe56c93d0, file_offset=3784,
> file_path=0xaaaae7e3b540 "/tmp/perf-test-aslr.J1XB8pvpFy/
> perf.data2.kernel.FA0Uvd") at util/session.c:1593
> #17 0x0000aaaaaf662bbc in ordered_events__deliver_event
> (oe=0xaaaae7e3c460, event=0xaaaae7e44740) at util/session.c:134
> #18 0x0000aaaaaf672c98 in do_flush (oe=0xaaaae7e3c460,
> show_progress=true) at util/ordered-events.c:245
> #19 0x0000aaaaaf673048 in __ordered_events__flush (oe=0xaaaae7e3c460,
> how=OE_FLUSH__FINAL, timestamp=0)
> at util/ordered-events.c:324
> #20 0x0000aaaaaf673154 in ordered_events__flush (oe=0xaaaae7e3c460,
> how=OE_FLUSH__FINAL) at util/ordered-events.c:342
> #21 0x0000aaaaaf669e54 in __perf_session__process_events
> (session=0xaaaae7e3bca0) at util/session.c:2508
> #22 0x0000aaaaaf66a790 in perf_session__process_events
> (session=0xaaaae7e3bca0) at util/session.c:2675
> #23 0x0000aaaaaf41f59c in __cmd_script (script=0xffffe56c93d0) at
> builtin-script.c:3241
> #24 0x0000aaaaaf4242b0 in cmd_script (argc=0, argv=0xffffe56cb370) at
> builtin-script.c:4586
> #25 0x0000aaaaaf4a03f8 in run_builtin (p=0xaaaaafa14e60
> <commands+480>, argc=3, argv=0xffffe56cb370) at perf.c:348
> #26 0x0000aaaaaf4a066c in handle_internal_command (argc=3,
> argv=0xffffe56cb370) at perf.c:398
> #27 0x0000aaaaaf4a0824 in run_argv (argcp=0xffffe56cb1ac,
> argv=0xffffe56cb1a0) at perf.c:442
> #28 0x0000aaaaaf4a0b4c in main (argc=3, argv=0xffffe56cb370) at
> perf.c:549
>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v6 3/6] perf maps: Add maps__mutate_mapping
2026-05-08 8:27 ` [PATCH v6 3/6] perf maps: Add maps__mutate_mapping Ian Rogers
2026-05-08 10:57 ` James Clark
@ 2026-05-11 7:07 ` Namhyung Kim
1 sibling, 0 replies; 27+ messages in thread
From: Namhyung Kim @ 2026-05-11 7:07 UTC (permalink / raw)
To: Ian Rogers
Cc: acme, gmx, james.clark, adrian.hunter, jolsa, linux-kernel,
linux-perf-users, mingo, peterz
On Fri, May 08, 2026 at 01:27:23AM -0700, Ian Rogers wrote:
> During kernel ELF symbol parsing (dso__process_kernel_symbol), proc
> kallsyms image loading (dso__load_kernel_sym,
> dso__load_guest_kernel_sym), and dynamic kernel memory map alignment
> updates (machine__update_kernel_mmap), the loader directly modifies
> live virtual address boundary keys fields on map objects. If these
> boundaries are mutated while the map pointer actively resides inside
> the parent maps cache array list (kmaps) outside of any lock closure,
> an unsafe concurrent window is exposed where parallel worker lookup
> threads (e.g., inside perf top) can mistakenly assume the cache
> remains sorted based on stale parameters, executing binary search
> queries (bsearch) across an unsorted range and triggering lookups
> failure.
>
> Fix this by introducing a thread-safe, atomic transactional framework
> routine maps__mutate_mapping() that explicitly acquires the parent
> maps write semaphore lock, executes an incoming mutation callback
> block to perform the field updates under full lock protection, and
> invalidates the sorted tracking flags prior to releasing the write
> lock. This guarantees absolute atomic synchronization invariants,
> completely closing the concurrent lookup race window. The adjacent
> module alignment pass inside machine__create_kernel_maps() is safely
> preserved as a high-performance lockless pass, as its invocation
> lifecycle bounds remain strictly single-threaded by contract during
> session initialization construction. There is a potential for self
> deadlock if maps__mutate_mapping is called with the lock held, such as
> with maps__for_each_map but this problem also existed with the
> previous remove and insert approaches.
>
> Fixes: 39b12f781271 ("perf tools: Make it possible to read object code from vmlinux")
> Signed-off-by: Ian Rogers <irogers@google.com>
> ---
> tools/perf/util/machine.c | 32 +++++++++++++++++-----------
> tools/perf/util/maps.c | 26 +++++++++++++++++++++++
> tools/perf/util/maps.h | 2 ++
> tools/perf/util/symbol-elf.c | 41 +++++++++++++++++++++++-------------
> tools/perf/util/symbol.c | 17 +++++++++++----
> 5 files changed, 87 insertions(+), 31 deletions(-)
>
> diff --git a/tools/perf/util/machine.c b/tools/perf/util/machine.c
> index e76f8c86e62a..8d4452c70cb5 100644
> --- a/tools/perf/util/machine.c
> +++ b/tools/perf/util/machine.c
> @@ -1522,22 +1522,30 @@ static void machine__set_kernel_mmap(struct machine *machine,
> map__set_end(machine->vmlinux_map, ~0ULL);
> }
>
> -static int machine__update_kernel_mmap(struct machine *machine,
> - u64 start, u64 end)
> +struct kernel_mmap_mutation_ctx {
> + u64 start;
> + u64 end;
> +};
> +
> +static int kernel_mmap_mutate_cb(struct map *map, void *data)
> {
> - struct map *orig, *updated;
> - int err;
> + struct kernel_mmap_mutation_ctx *ctx = data;
>
> - orig = machine->vmlinux_map;
> - updated = map__get(orig);
> + map__set_start(map, ctx->start);
> + map__set_end(map, ctx->end);
> + if (ctx->start == 0 && ctx->end == 0)
> + map__set_end(map, ~0ULL);
> + return 0;
> +}
>
> - machine->vmlinux_map = updated;
> - maps__remove(machine__kernel_maps(machine), orig);
> - machine__set_kernel_mmap(machine, start, end);
> - err = maps__insert(machine__kernel_maps(machine), updated);
> - map__put(orig);
> +static int machine__update_kernel_mmap(struct machine *machine,
> + u64 start, u64 end)
> +{
> + struct kernel_mmap_mutation_ctx ctx = { .start = start, .end = end };
>
> - return err;
> + return maps__mutate_mapping(machine__kernel_maps(machine),
> + machine->vmlinux_map,
> + kernel_mmap_mutate_cb, &ctx);
> }
>
> int machine__create_kernel_maps(struct machine *machine)
> diff --git a/tools/perf/util/maps.c b/tools/perf/util/maps.c
> index 81a97ac34077..91345a773aa2 100644
> --- a/tools/perf/util/maps.c
> +++ b/tools/perf/util/maps.c
> @@ -575,6 +575,32 @@ void maps__remove(struct maps *maps, struct map *map)
> #endif
> }
>
> +int maps__mutate_mapping(struct maps *maps, struct map *map,
> + int (*mutate_cb)(struct map *map, void *data), void *data)
> +{
> + int err = 0;
> +
> + if (maps)
> + down_write(maps__lock(maps));
> +
> + err = mutate_cb(map, data);
> +
> + if (maps) {
> + RC_CHK_ACCESS(maps)->maps_by_address_sorted = false;
> + RC_CHK_ACCESS(maps)->maps_by_name_sorted = false;
> + }
> +
> + if (maps)
> + up_write(maps__lock(maps));
> +
> +#ifdef HAVE_LIBDW_SUPPORT
> + if (maps)
> + libdw__invalidate_dwfl(maps, maps__libdw_addr_space_dwfl(maps));
> +#endif
> +
> + return err;
> +}
Could be simplified by checking 'maps' once. But I'm not sure if
there's a case it doesn't have the maps.
Thanks,
Namhyung
> +
> bool maps__empty(struct maps *maps)
> {
> bool res;
> diff --git a/tools/perf/util/maps.h b/tools/perf/util/maps.h
> index 20c52084ba9e..de74ccbb8a12 100644
> --- a/tools/perf/util/maps.h
> +++ b/tools/perf/util/maps.h
> @@ -61,6 +61,8 @@ size_t maps__fprintf(struct maps *maps, FILE *fp);
>
> int maps__insert(struct maps *maps, struct map *map);
> void maps__remove(struct maps *maps, struct map *map);
> +int maps__mutate_mapping(struct maps *maps, struct map *map,
> + int (*mutate_cb)(struct map *map, void *data), void *data);
>
> struct map *maps__find(struct maps *maps, u64 addr);
> struct symbol *maps__find_symbol(struct maps *maps, u64 addr, struct map **mapp);
> diff --git a/tools/perf/util/symbol-elf.c b/tools/perf/util/symbol-elf.c
> index 7afa8a117139..dc4ab58857b3 100644
> --- a/tools/perf/util/symbol-elf.c
> +++ b/tools/perf/util/symbol-elf.c
> @@ -1341,6 +1341,24 @@ static u64 ref_reloc(struct kmap *kmap)
> void __weak arch__sym_update(struct symbol *s __maybe_unused,
> GElf_Sym *sym __maybe_unused) { }
>
> +struct remap_kernel_ctx {
> + u64 sh_addr;
> + u64 sh_size;
> + u64 sh_offset;
> + struct kmap *kmap;
> +};
> +
> +static int remap_kernel_cb(struct map *map, void *data)
> +{
> + struct remap_kernel_ctx *ctx = data;
> +
> + map__set_start(map, ctx->sh_addr + ref_reloc(ctx->kmap));
> + map__set_end(map, map__start(map) + ctx->sh_size);
> + map__set_pgoff(map, ctx->sh_offset);
> + map__set_mapping_type(map, MAPPING_TYPE__DSO);
> + return 0;
> +}
> +
> static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
> GElf_Sym *sym, GElf_Shdr *shdr,
> struct maps *kmaps, struct kmap *kmap,
> @@ -1371,22 +1389,15 @@ static int dso__process_kernel_symbol(struct dso *dso, struct map *map,
> * map to the kernel dso.
> */
> if (*remap_kernel && dso__kernel(dso) && !kmodule) {
> + struct remap_kernel_ctx ctx = {
> + .sh_addr = shdr->sh_addr,
> + .sh_size = shdr->sh_size,
> + .sh_offset = shdr->sh_offset,
> + .kmap = kmap
> + };
> +
> *remap_kernel = false;
> - map__set_start(map, shdr->sh_addr + ref_reloc(kmap));
> - map__set_end(map, map__start(map) + shdr->sh_size);
> - map__set_pgoff(map, shdr->sh_offset);
> - map__set_mapping_type(map, MAPPING_TYPE__DSO);
> - /* Ensure maps are correctly ordered */
> - if (kmaps) {
> - int err;
> - struct map *tmp = map__get(map);
> -
> - maps__remove(kmaps, map);
> - err = maps__insert(kmaps, map);
> - map__put(tmp);
> - if (err)
> - return err;
> - }
> + maps__mutate_mapping(kmaps, map, remap_kernel_cb, &ctx);
> }
>
> /*
> diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
> index fcaeeddbbb6b..09b93e844887 100644
> --- a/tools/perf/util/symbol.c
> +++ b/tools/perf/util/symbol.c
> @@ -48,6 +48,13 @@
> #include <symbol/kallsyms.h>
> #include <sys/utsname.h>
>
> +static int map_fixup_cb(struct map *map, void *data __maybe_unused)
> +{
> + map__fixup_start(map);
> + map__fixup_end(map);
> + return 0;
> +}
> +
> static int dso__load_kernel_sym(struct dso *dso, struct map *map);
> static int dso__load_guest_kernel_sym(struct dso *dso, struct map *map);
> static bool symbol__is_idle(const char *name);
> @@ -2121,10 +2128,11 @@ static int dso__load_kernel_sym(struct dso *dso, struct map *map)
> free(kallsyms_allocated_filename);
>
> if (err > 0 && !dso__is_kcore(dso)) {
> + struct maps *kmaps = map__kmaps(map);
> +
> dso__set_binary_type(dso, DSO_BINARY_TYPE__KALLSYMS);
> dso__set_long_name(dso, DSO__NAME_KALLSYMS, false);
> - map__fixup_start(map);
> - map__fixup_end(map);
> + maps__mutate_mapping(kmaps, map, map_fixup_cb, NULL);
> }
>
> return err;
> @@ -2164,10 +2172,11 @@ static int dso__load_guest_kernel_sym(struct dso *dso, struct map *map)
> if (err > 0)
> pr_debug("Using %s for symbols\n", kallsyms_filename);
> if (err > 0 && !dso__is_kcore(dso)) {
> + struct maps *kmaps = map__kmaps(map);
> +
> dso__set_binary_type(dso, DSO_BINARY_TYPE__GUEST_KALLSYMS);
> dso__set_long_name(dso, machine->mmap_name, false);
> - map__fixup_start(map);
> - map__fixup_end(map);
> + maps__mutate_mapping(kmaps, map, map_fixup_cb, NULL);
> }
>
> return err;
> --
> 2.54.0.563.g4f69b47b94-goog
>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v6 4/6] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses
2026-05-08 8:27 ` [PATCH v6 4/6] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
@ 2026-05-11 7:32 ` Namhyung Kim
0 siblings, 0 replies; 27+ messages in thread
From: Namhyung Kim @ 2026-05-11 7:32 UTC (permalink / raw)
To: Ian Rogers
Cc: acme, gmx, james.clark, adrian.hunter, jolsa, linux-kernel,
linux-perf-users, mingo, peterz
On Fri, May 08, 2026 at 01:27:24AM -0700, Ian Rogers wrote:
> If perf.data files are taken from one machine to another they may
> leak virtual addresses and so weaken ASLR on the machine they are
> coming from. Add an aslr option for perf inject that remaps all
> virtual addresses, or drops data/events, so that the virtual address
> information isn't leaked.
>
> Events carrying virtual memory layouts are conservatively remap-processed
> or dropped, while zero-address-risk lifecycle metadata records (such as
> namespaces, cgroups, and BPF program info) are intentionally delegated
> to preserve comprehensive downstream trace tool analysis compatibility.
>
> The ASLR tracking tool virtualizes process and machine namespaces using
> 'struct machines' to safely isolate host mappings from unprivileged KVM guest
> address spaces. Memory space layouts are tracked globally per process context to
> ensure linear, continuous space allocations across successive mapping runs.
>
> To remain strictly conservative and guarantee security, the tool scrubs
> breakpoint addresses (bp_addr) from all synthesized stream headers, completely
> drops PERF_RECORD_TEXT_POKE events to prevent absolute immediate pointer
> operands leaks, and drops unsupported complex payloads (such as user register
> stacks, raw tracepoints, and hardware AUX tracing frames).
>
> 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>
> ---
> v6: Enforce strict command-line validation mutual exclusivity between
> --aslr and --convert-callchain to prevent silent unwind failures.
> Secure mmap.pgoff unconditionally for all host and guest kernel text
> mapping regions to completely prevent active KASLR load deltas leakage.
> Conservatively drop PERF_RECORD_TEXT_POKE events completely via a local
> static drop stub to prevent absolute 64-bit kernel virtual pointer immediate
> operands leaks. Inject explicit array-end bounds validation check blocks
> before consuming trailing PERF_CONTEXT_USER_DEFERRED callchain cookies
> to eliminate out-of-bounds reads and parser desynchronization faults.
> Simplify ASLR mapping remap logic. Ensure that encountering a
> PERF_CONTEXT_USER_DEFERRED context marker explicitly updates cpumode.
>
> v5: Add machine to remap addresss key so that it is guest/host
> safe. Add 'first_kernel_mapping' tracking guard inside aslr.c to
> rewrite the core kernel pgoff virtual address while safely
> protecting module file offsets from corruption. Clean up
> breakpoint address (bp_addr) memory scrubbing by executing the
> scrubbing loop directly at core session initialization startup
> level, natively securing both file headers and streaming pipe
> channels while removing redundant runtime tool wrapper
> interception hooks layers.
>
> 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 | 36 +-
> tools/perf/util/Build | 1 +
> tools/perf/util/aslr.c | 1036 +++++++++++++++++++++++++++++++++++
> tools/perf/util/aslr.h | 10 +
> 4 files changed, 1082 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 6ab20df358c4..51dcf248b653 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,8 @@ static int perf_event__repipe(const struct perf_tool *tool,
> return perf_event__repipe_synth(tool, event);
> }
>
> +
> +
Still have unnecessary blank lines.
> static int perf_event__drop(const struct perf_tool *tool __maybe_unused,
> union perf_event *event __maybe_unused,
> struct perf_sample *sample __maybe_unused,
> @@ -2459,6 +2463,8 @@ static int __cmd_inject(struct perf_inject *inject)
> }
> }
>
> +
> +
Ditto.
> 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,
> @@ -2565,6 +2571,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[] = {
> @@ -2572,6 +2580,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. */
> @@ -2592,6 +2601,11 @@ int cmd_inject(int argc, const char **argv)
> if (argc)
> usage_with_options(inject_usage, options);
>
> + if (inject.aslr && inject.convert_callchain) {
> + pr_err("Error: --aslr and --convert-callchain are mutually exclusive features.\n");
> + return -EINVAL;
> + }
> +
> if (inject.strip && !inject.itrace_synth_opts.set) {
> pr_err("--strip option requires --itrace option\n");
> return -1;
> @@ -2685,18 +2699,36 @@ 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;
> }
>
> if (zstd_init(&(inject.session->zstd_data), 0) < 0)
> pr_warning("Decompression initialization failed.\n");
>
> + if (inject.aslr) {
> + struct evsel *evsel;
> +
> + evlist__for_each_entry(inject.session->evlist, evsel) {
> + if (evsel->core.attr.type == PERF_TYPE_BREAKPOINT)
> + evsel->core.attr.bp_addr = 0;
> + }
> + }
> +
> /* Save original section info before feature bits change */
> ret = save_section_info(&inject);
> if (ret)
> @@ -2790,6 +2822,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..09b7f2f8fb85
> --- /dev/null
> +++ b/tools/perf/util/aslr.c
> @@ -0,0 +1,1036 @@
> +// 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>
> +
> +/**
> + * 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 machine *machine;
> + 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] __aligned(8);
> + /** @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->machine ^ (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->machine == key2->machine &&
> + RC_CHK_EQUAL(key1->dso, key2->dso) &&
> + key1->invariant == key2->invariant &&
> + key1->pid == key2->pid;
> +}
> +
> +struct top_addresses_key {
> + struct machine *machine;
> + pid_t pid;
> +};
> +
> +static size_t top_addresses__hash(long _key, void *ctx __maybe_unused)
> +{
> + struct top_addresses_key *key = (struct top_addresses_key *)_key;
> +
> + return (size_t)key->machine ^ key->pid;
> +}
> +
> +static bool top_addresses__equal(long _key1, long _key2, void *ctx __maybe_unused)
> +{
> + struct top_addresses_key *key1 = (struct top_addresses_key *)_key1;
> + struct top_addresses_key *key2 = (struct top_addresses_key *)_key2;
> +
> + return key1->machine == key2->machine && key1->pid == key2->pid;
> +}
> +
> +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.machine = maps__machine(aslr_thread->maps);
> + 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__findnew_mapping(struct aslr_tool *aslr,
> + struct thread *aslr_thread,
> + u8 cpumode, u64 start,
> + u64 len, u64 pgoff)
> +{
> + /* Address location for dso lookup. */
> + struct addr_location al;
> + /* Original ASLR address based key for the remap table. */
> + struct remap_addresses_key remap_key;
> + /* The address in the ASLR sanitized address space less pg_off. */
> + u64 *remapped_invariant_ptr;
> + /* Key for the maximum address in a process. */
> + struct top_addresses_key top_addr_key;
> + /* Value in top address table. */
> + u64 *pmax = NULL;
> + /* Address in ASLR sanitized address space. */
> + u64 remap_addr;
> + /* Potentially allocated remap table key. */
> + struct remap_addresses_key *new_remap_key = NULL;
> + /*
> + * Potentially allocated remap table key.
> + * TODO: Avoid allocation necessary for perf 32-bit binary support.
> + */
> + u64 *new_remap_val = NULL;
> + int err;
> +
> + if (!aslr_thread)
> + return 0;
> +
> + /* The key to look up an incoming address to the outgoing value. */
> + addr_location__init(&al);
> + remap_key.machine = maps__machine(aslr_thread->maps);
> + remap_key.pid = (cpumode == PERF_RECORD_MISC_KERNEL) ? kernel_pid : aslr_thread->pid_;
> + if (thread__find_map(aslr_thread, cpumode, start, &al)) {
> + remap_key.dso = map__dso(al.map);
> + remap_key.invariant = map__start(al.map) - map__pgoff(al.map);
> + } else {
> + remap_key.dso = NULL;
> + remap_key.invariant = start - pgoff;
> + }
> +
> + /* The key to look up top allocated address. */
> + top_addr_key.machine = remap_key.machine;
> + top_addr_key.pid = remap_key.pid;
> +
> + if (hashmap__find(&aslr->remap_addresses, &remap_key, &remapped_invariant_ptr)) {
> + /* Mmap already exists. */
> + u64 calculated_max;
> +
> + remap_addr = *remapped_invariant_ptr + (al.map ? map__pgoff(al.map) : pgoff);
> + calculated_max = remap_addr + len;
> +
> + /* See if top mapping was expanded. */
> + if (hashmap__find(&aslr->top_addresses, &top_addr_key, &pmax)) {
> + if (calculated_max > *pmax)
> + *pmax = calculated_max;
> + }
> + addr_location__exit(&al);
> + return remap_addr;
> + }
> + /* No mmap, create an entry from the top address. */
> + if (hashmap__find(&aslr->top_addresses, &top_addr_key, &pmax)) {
> + /* Current max allocated mmap address within the process. */
> + remap_addr = *pmax;
> +
> + /* Give 1 page gap from current max page. */
> + remap_addr = round_up_to_page_size(remap_addr);
> + remap_addr += page_size;
> + if (remap_addr + len > *pmax)
> + *pmax = remap_addr + len;
> + } else {
> + /* First address of the process, allocate key and first top address. */
> + struct top_addresses_key *tk;
> +
> + remap_addr = (cpumode == PERF_RECORD_MISC_KERNEL) ?
> + kernel_space_start : user_space_start;
> + remap_addr = round_up_to_page_size(remap_addr);
> +
> + tk = malloc(sizeof(*tk));
> + pmax = malloc(sizeof(u64));
> + if (!tk || !pmax) {
> + err = -ENOMEM;
> + } else {
> + *tk = top_addr_key;
> + *pmax = remap_addr + len;
> + err = hashmap__insert(&aslr->top_addresses, tk, pmax, HASHMAP_ADD, NULL, NULL);
> + }
> + if (err) {
> + errno = -err;
> + pr_err("Failure to add ASLR process top address %m\n");
> + free(tk);
> + free(pmax);
> + addr_location__exit(&al);
> + return 0;
> + }
> + }
> + /* Create rmeapping entry. */
> + new_remap_key = malloc(sizeof(*new_remap_key));
> + new_remap_val = malloc(sizeof(u64));
> + if (!new_remap_key || !new_remap_val) {
> + err = -ENOMEM;
> + } else {
> + *new_remap_key = remap_key;
> + new_remap_key->dso = dso__get(remap_key.dso);
> + if (cpumode == PERF_RECORD_MISC_KERNEL) {
> + if (al.map)
> + *new_remap_val = remap_addr - (start - map__start(al.map)) - map__pgoff(al.map);
A too long line.
> + else
> + *new_remap_val = remap_addr;
> + } else {
> + *new_remap_val = remap_addr - (al.map ? map__pgoff(al.map) : pgoff);
> + }
> + err = hashmap__add(&aslr->remap_addresses, new_remap_key, new_remap_val);
> + if (err)
> + dso__put(new_remap_key->dso);
> + }
> + if (err) {
> + errno = -err;
> + pr_err("Failure to add ASLR remapping %m\n");
> + free(new_remap_key);
> + free(new_remap_val);
> + addr_location__exit(&al);
> + return 0;
> + }
> + 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__findnew_mapping(aslr, thread, cpumode,
> + event->mmap.start,
> + event->mmap.len,
> + event->mmap.pgoff);
> + if (cpumode == PERF_RECORD_MISC_KERNEL || cpumode == PERF_RECORD_MISC_GUEST_KERNEL)
> + new_event->mmap.pgoff = new_event->mmap.start;
> + 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__findnew_mapping(aslr, thread, cpumode,
> + event->mmap2.start,
> + event->mmap2.len,
> + event->mmap2.pgoff);
> + if (cpumode == PERF_RECORD_MISC_KERNEL || cpumode == PERF_RECORD_MISC_GUEST_KERNEL)
> + new_event->mmap2.pgoff = new_event->mmap2.start;
> + 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 __maybe_unused,
> + union perf_event *event __maybe_unused,
> + struct perf_sample *sample __maybe_unused,
> + struct machine *machine __maybe_unused)
> +{
> + /* Drop in case the instruction encodes an ASLR revealing address. */
> + return 0;
> +}
> +
> +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;
> +
> + 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__findnew_mapping(aslr, thread,
> + PERF_RECORD_MISC_KERNEL,
> + event->ksymbol.addr,
> + event->ksymbol.len,
> + /*pgoff=*/0);
> +
> + 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;
> +
> +
> +
Here as well.
> + 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)
> +
I'm still not sure if it's a good idea to expose all the details of the
sample layout here. It needs to be in sync with evsel__parse_sample()
for any future changes.
> + 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:
> + if (cntr + 1 >= nr) {
> + pr_debug("Truncated callchain deferred cookie context\n");
> + ret = 0;
> + goto out_put;
> + }
> + /*
> + * Immediately followed by a 64-bit
> + * stitching cookie. Skip/Copy it!
> + */
> + CHECK_BOUNDS(1, 1);
> + out_array[j++] = in_array[i++];
> + cntr++;
> + cpumode = PERF_RECORD_MISC_USER;
> + 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;
There's not much point to do it when we drop all samples as the sample
type flags will be the same for an evsel. Maybe better to check if it
has unsupported flags earlier.
> + }
> + 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 int skipn(int fd, off_t n)
> +{
> + char buf[4096];
> + ssize_t ret;
> +
> + while (n > 0) {
> + ret = read(fd, buf, min(n, (off_t)sizeof(buf)));
> + if (ret <= 0)
> + return ret;
> + n -= ret;
> + }
> +
> + return 0;
> +}
> +
> +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)) {
> + /* Copy behavior of the stub by reading all pipe 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, */
The same line appears twice.
> + /* auxtrace_info, auxtrace_error, time_conv, thread_map, cpu_map, */
> + /* stat_config, stat, feature, finished_init, bpf_metadata, compressed, */
> + /* auxtrace - no virtual addresses. */
The auxtrace related ones are listed but handled differently?
Thanks,
Namhyung
> + 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->pkey);
> + zfree(&cur->pvalue);
> + }
> +
> + hashmap__clear(&aslr->remap_addresses);
> + hashmap__clear(&aslr->top_addresses);
> + machines__destroy_kernel_maps(&aslr->machines);
> + 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.563.g4f69b47b94-goog
>
^ permalink raw reply [flat|nested] 27+ messages in thread
* Re: [PATCH v6 5/6] perf test: Add inject ASLR test
2026-05-08 8:27 ` [PATCH v6 5/6] perf test: Add inject ASLR test Ian Rogers
2026-05-08 13:29 ` James Clark
@ 2026-05-11 7:34 ` Namhyung Kim
1 sibling, 0 replies; 27+ messages in thread
From: Namhyung Kim @ 2026-05-11 7:34 UTC (permalink / raw)
To: Ian Rogers
Cc: acme, gmx, james.clark, adrian.hunter, jolsa, linux-kernel,
linux-perf-users, mingo, peterz
On Fri, May 08, 2026 at 01:27:25AM -0700, Ian Rogers wrote:
> 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 (utilizing a dedicated kernel-intensive VFS dd workload
> to guarantee continuous timer interrupts sampling flow inside kernel privilege states).
> - Kernel report consistency with address normalization.
>
> The test suite is hardened with global 'set -o pipefail' assertions to catch
> pipeline failures, stream-consuming awk processors to handle SIGPIPE signals,
> and a dedicated pipe output scenario validating raw 'perf inject -o -' stdout
> streams.
>
> Assisted-by: Gemini-CLI:Google Gemini 3
> Signed-off-by: Ian Rogers <irogers@google.com>
> ---
> v6: Refactor kernel-space sampling test cases to utilize a dedicated
> system-call intensive VFS dd workload (kprog) instead of purely
> userspace-bound tight loops, guaranteeing high-density kernel
> privilege state sampling streams and eliminating intermittent
> execution flakiness dropouts.
>
> v5: Harden test suite verification pipelines by upgrading report
> checks to strict sorted line-by-line diff comparisons to
> accommodate remapped pointer shifts. Append || true fallback
> operators to grep-v filtering pipelines to prevent the shell test
> from spuriously aborting under set -o pipefail on empty inputs,
> ensuring graceful failure checks trigger correctly.
>
> 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 | 460 ++++++++++++++++++++++++++
> 1 file changed, 460 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..6363a0f69d2b
> --- /dev/null
> +++ b/tools/perf/tests/shell/inject_aslr.sh
> @@ -0,0 +1,460 @@
> +#!/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
> +kprog="dd if=/dev/zero of=/dev/null bs=1M count=500"
> +
> +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")
Why not use the globally defined data and data2 here and below?
Thanks,
Namhyung
> +
> + 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}" ${kprog} > "${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}" ${kprog} > "${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"
> +
> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report1_clean}" | \
> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | \
> + sort > "${report1_norm}" || true
> + grep -v -E '0x[0-9a-f]{8,}|0000000000000000' "${report2_clean}" | \
> + awk '{gsub(/\[[a-zA-Z0-9_.-]{2,}\](\.[a-zA-Z0-9_]+)?/, "[kernel]", $0); print}' | \
> + sort > "${report2_norm}" || true
> +
> + 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.563.g4f69b47b94-goog
>
^ permalink raw reply [flat|nested] 27+ messages in thread
end of thread, other threads:[~2026-05-11 7:34 UTC | newest]
Thread overview: 27+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
[not found] <20260504072937.2103453-1-irogers@google.com>
2026-05-06 0:45 ` [PATCH v5 0/5] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-06 0:45 ` [PATCH v5 1/5] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-06 13:22 ` Arnaldo Carvalho de Melo
2026-05-06 16:16 ` Ian Rogers
2026-05-06 0:45 ` [PATCH v5 2/5] perf tool: Fix missing schedstat delegates and dont_split_sample_group in delegate_tool Ian Rogers
2026-05-06 0:45 ` [PATCH v5 3/5] perf symbols: Fix map removal sequence inside dso__process_kernel_symbol() Ian Rogers
2026-05-06 0:45 ` [PATCH v5 4/5] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-05-06 18:52 ` Namhyung Kim
2026-05-06 20:01 ` Ian Rogers
2026-05-06 0:45 ` [PATCH v5 5/5] perf test: Add inject ASLR test Ian Rogers
2026-05-07 15:58 ` James Clark
2026-05-07 16:17 ` Ian Rogers
2026-05-08 10:42 ` James Clark
2026-05-08 10:49 ` James Clark
2026-05-08 8:27 ` [PATCH v6 0/6] perf tools: Add inject --aslr feature and prerequisite robustness fixes Ian Rogers
2026-05-08 8:27 ` [PATCH v6 1/6] perf sched: Add missing mmap2 handler in timehist Ian Rogers
2026-05-08 8:27 ` [PATCH v6 2/6] perf tool: Missing delegate_tool schedstat delegates and dont_split_sample_group Ian Rogers
2026-05-08 8:27 ` [PATCH v6 3/6] perf maps: Add maps__mutate_mapping Ian Rogers
2026-05-08 10:57 ` James Clark
2026-05-11 7:07 ` Namhyung Kim
2026-05-08 8:27 ` [PATCH v6 4/6] perf inject/aslr: Add aslr tool to remap/obfuscate virtual addresses Ian Rogers
2026-05-11 7:32 ` Namhyung Kim
2026-05-08 8:27 ` [PATCH v6 5/6] perf test: Add inject ASLR test Ian Rogers
2026-05-08 13:29 ` James Clark
2026-05-08 14:29 ` James Clark
2026-05-11 7:34 ` Namhyung Kim
2026-05-08 8:27 ` [PATCH v6 6/6] perf aslr: Strip sample registers Ian Rogers
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox