* [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive
@ 2026-09-17 6:42 Ian Rogers
2026-09-17 6:42 ` [PATCH v1 01/13] perf trace: Start BPF summary before starting workload Ian Rogers
` (13 more replies)
0 siblings, 14 replies; 91+ messages in thread
From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw)
To: Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter,
James Clark, linux-perf-users, linux-kernel, Ian Rogers
perf trace's BPF augmentation attaches to raw_syscalls:sys_enter and
raw_syscalls:sys_exit system wide, and used the program return value to
decide whether a syscall was interesting. Returning 0 from a
BPF_PROG_TYPE_TRACEPOINT program makes perf_trace_run_bpf_submit() drop
the event for every listener on that tracepoint, not just for the perf
trace that installed the program. Any concurrent perf trace, perf record
or ftrace session watching raw_syscalls therefore lost events, which is
one of the reasons so many of the perf trace and perf probe shell tests
had to be marked (exclusive) and run on their own.
Patches 1 to 5 fix perf trace. They stop the return value being used as
a filter and do the filtering in BPF maps instead, fix argument handling
for the __data_loc internal tracepoint fields that syscalls:sys_enter_*
gained in 6.19, stop the sys_exit program array tail calling a sys_enter
augmenter, and replace the userspace PERF_RECORD_FORK/PERF_RECORD_EXIT
bookkeeping with BTF-typed raw tracepoint programs on
sched_process_{fork,exit,exec}. A task is then registered before its
first syscall and evicted in do_exit(), rather than whenever userspace
next drains the ring buffer.
Patches 6 to 13 deal with the tests. Several collided with each other
through global state rather than through perf trace: fixed probe names,
clear_all_probes() disabling tracing events globally, and perf trace's
hardcoded "probe:vfs_getname*" wildcard pinning probes belonging to
other tests. With those scoped to a pid they can drop (exclusive) and
run in parallel again.
Tested on x86_64. The trace and probe tests pass under 'perf test -r3',
which runs the repeats concurrently. Every patch builds individually,
and the series also builds with BUILD_BPF_SKEL=0.
Ian Rogers (13):
perf trace: Start BPF summary before starting workload
perf trace: Skip internal tracepoint fields in formatting and beauty
map
perf trace: Do not set unaugmented BPF program on sys_exit map
perf trace: Filter events in BPF and avoid tracepoint vetoes
perf trace: Handle fork and exit directly in BPF filter maps
perf test test_task_analyzer: Isolate in temporary directory and make
non-exclusive
perf test common: Do not globally disable tracing events in
clear_all_probes
perf test probe_vfs_getname: Scope probe name to PID and make
non-exclusive
perf test record+probe_libc_inet_pton: Scope event to PID, add
retries, and make non-exclusive
perf test trace_summary: Improve error diagnostics
perf test trace_btf_general: Drop --max-events=1 and make
non-exclusive
perf test trace_summary: Make non-exclusive
perf test uprobe_from_different_cu: Scope probe name to PID
tools/perf/Documentation/perf-trace.txt | 5 +
tools/perf/builtin-trace.c | 330 ++++++++++++++----
tools/perf/tests/shell/common/init.sh | 1 -
.../perf/tests/shell/lib/probe_vfs_getname.sh | 34 +-
tools/perf/tests/shell/probe_vfs_getname.sh | 3 +-
.../shell/record+probe_libc_inet_pton.sh | 83 +++--
.../shell/record+script_probe_vfs_getname.sh | 18 +-
tools/perf/tests/shell/test_task_analyzer.sh | 11 +-
.../shell/test_uprobe_from_different_cu.sh | 11 +-
.../tests/shell/trace+probe_vfs_getname.sh | 9 +
tools/perf/tests/shell/trace_btf_general.sh | 8 +-
tools/perf/tests/shell/trace_summary.sh | 14 +-
.../bpf_skel/augmented_raw_syscalls.bpf.c | 291 ++++++++++++++-
tools/perf/util/bpf_trace_augment.c | 173 ++++++++-
tools/perf/util/trace_augment.h | 25 +-
15 files changed, 875 insertions(+), 141 deletions(-)
base-commit: 91b0782fc9e9d2f0a40b5256146e014802fdbb36
--
2.55.0.1082.g2b9226bbc0-goog
^ permalink raw reply [flat|nested] 91+ messages in thread* [PATCH v1 01/13] perf trace: Start BPF summary before starting workload 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 02/13] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers ` (12 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers When using --bpf-summary, trace_start_bpf_summary() sets skel->bss->enabled = 1. In trace__run(), trace_start_bpf_summary() was previously invoked after evlist__start_workload(). Because evlist__start_workload() immediately unblocks the child process by writing to its go_pipe, short-lived workloads (such as `cat /dev/null`) can execute and complete their initial system calls before trace_start_bpf_summary() is reached by the parent process. Furthermore, under high system load, the child process may finish before the BPF summary tracking is enabled in the kernel at all, causing syscall summary tests to fail. Additionally, if initial_delay was configured, the workload was started before sleeping. Move trace_start_bpf_summary() to be invoked before evlist__start_workload(), matching evlist__enable(), and ensure it respects target.initial_delay. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 20fffc24507b..8da0c51ec380 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4838,17 +4838,19 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (!target__none(&trace->opts.target) && !trace->opts.target.initial_delay) evlist__enable(evlist); + if (trace->summary_bpf && !trace->opts.target.initial_delay) + trace_start_bpf_summary(); + if (forks) evlist__start_workload(evlist); if (trace->opts.target.initial_delay) { usleep(trace->opts.target.initial_delay * 1000); evlist__enable(evlist); + if (trace->summary_bpf) + trace_start_bpf_summary(); } - if (trace->summary_bpf) - trace_start_bpf_summary(); - trace->multiple_threads = perf_thread_map__pid(evlist__core(evlist)->threads, 0) == -1 || perf_thread_map__nr(evlist__core(evlist)->threads) > 1 || evlist__first(evlist)->core.attr.inherit; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 02/13] perf trace: Skip internal tracepoint fields in formatting and beauty map 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-17 6:42 ` [PATCH v1 01/13] perf trace: Start BPF summary before starting workload Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 03/13] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers ` (11 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers Linux 6.19+ added __data_loc char[] internal fields for string arguments in syscalls:sys_enter_<name> tracepoints (e.g., __data_loc_oldname in sys_enter_renameat2). While is_internal_field() was added to detect them, several places did not properly account for them: 1. In syscall_arg_fmt__init_array(), when an internal field was skipped, the arg pointer was still incremented, causing the subsequent argument formatters to be mismatched. 2. In syscall__scnprintf_args(), internal fields were not skipped, causing spurious trailing arguments like ", 0, 16" to be formatted and printed. 3. In trace__bpf_sys_enter_beauty_map(), internal fields were not skipped, offsetting beauty array argument indices and breaking string and buffer augmentation. 4. In trace__find_usable_bpf_prog_entry(), candidate pointer checks matched on internal pointer fields, breaking signature compatibility matching between syscalls for augmenter sharing. Introduce next_user_arg() and advance both cursors with it, so that the two argument lists are always compared at a real argument and the walk ends when one syscall runs out of arguments rather than when one happens to have trailing internal fields. 5. syscall__augmented_args() computed the augmented payload as sample->raw_size - sc->args_size for any sys_enter style sample. sc->args_size deliberately stops at the last non-internal field, so on 6.19+ a native syscalls:sys_enter_<name> record leaves the __data_loc words and their string payloads in the remainder. Those bytes are not a struct augmented_arg, so syscall_arg__scnprintf_augmented_string() read a bogus length and walked arg->augmented.args out of bounds. This is reachable from trace__event_handler(), which calls trace__fprintf_sys_enter() for any evsel whose tracepoint name starts with "sys_enter_". 6. In syscall__read_info(), syscall__alloc_arg_fmts() was called before checking and dropping the leading __syscall_nr (or nr) field, using nr_fields - 1 unconditionally. If a tracepoint format lacks that leading field, the allocated arg_fmt array is one entry too small and syscall_arg_fmt__init_array() writes one entry past the end of the heap buffer. Drop __syscall_nr/nr first and size the allocation from the remaining fields. Update these functions to check and skip is_internal_field() so that arguments are correctly formatted and beauty map entries match the expected syscall signatures, restrict syscall__augmented_args() to the __augmented_syscalls__ bpf-output evsel, and size arg_fmt after dropping the syscall number field. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 153 ++++++++++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 38 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 8da0c51ec380..af9696aadaec 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2277,15 +2277,20 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field struct tep_format_field *last_field = NULL; int len; - for (; field; field = field->next, ++arg) { - /* assume it's the last argument */ + for (; field; field = field->next) { + /* + * Skip internal tracepoint fields (e.g., __data_loc strings in + * Linux 6.19+) so they do not advance the syscall arg array index. + */ if (is_internal_field(field)) continue; last_field = field; - if (arg->scnprintf) + if (arg->scnprintf) { + ++arg; continue; + } len = strlen(field->name); @@ -2342,6 +2347,7 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field } } } + ++arg; } return last_field; @@ -2363,6 +2369,7 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) char tp_name[128]; const char *name; struct tep_format_field *field; + int nr_args; int err; if (sc->nonexistent) @@ -2401,24 +2408,25 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) return err; } - /* - * The tracepoint format contains __syscall_nr field, so it's one more - * than the actual number of syscall arguments. - */ - if (syscall__alloc_arg_fmts(sc, sc->tp_format->format.nr_fields - 1)) - return -ENOMEM; - sc->args = sc->tp_format->format.fields; + nr_args = sc->tp_format->format.nr_fields; /* * We need to check and discard the first variable '__syscall_nr' * or 'nr' that mean the syscall number. It is needless here. * So drop '__syscall_nr' or 'nr' field but does not exist on older kernels. + * + * Do this before allocating, and size the array from what is left, so + * that a format without the field does not leave + * syscall_arg_fmt__init_array() walking one entry past the end. */ if (sc->args && (!strcmp(sc->args->name, "__syscall_nr") || !strcmp(sc->args->name, "nr"))) { sc->args = sc->args->next; - --sc->nr_args; + --nr_args; } + if (syscall__alloc_arg_fmts(sc, nr_args)) + return -ENOMEM; + field = sc->args; while (field) { if (is_internal_field(field)) @@ -2636,11 +2644,17 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, if (sc->args != NULL) { struct tep_format_field *field; - for (field = sc->args; field; - field = field->next, ++arg.idx, bit <<= 1) { - if (arg.mask & bit) + for (field = sc->args; field; field = field->next) { + /* Skip internal fields so they are not printed as spurious arguments */ + if (is_internal_field(field)) continue; + if (arg.mask & bit) { + ++arg.idx; + bit <<= 1; + continue; + } + arg.fmt = &sc->arg_fmt[arg.idx]; val = syscall_arg__val(&arg, arg.idx); /* @@ -2658,8 +2672,11 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, */ if (val == 0 && !trace->show_zeros && !(sc->arg_fmt && sc->arg_fmt[arg.idx].show_zero) && - !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) + !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) { + ++arg.idx; + bit <<= 1; continue; + } printed += scnprintf(bf + printed, size - printed, "%s", printed ? ", " : ""); @@ -2674,12 +2691,16 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, size - printed, val, field->type); if (btf_printed) { printed += btf_printed; + ++arg.idx; + bit <<= 1; continue; } } printed += syscall_arg_fmt__scnprintf_val(&sc->arg_fmt[arg.idx], bf + printed, size - printed, &arg, val); + ++arg.idx; + bit <<= 1; } } else if (IS_ERR(sc->tp_format)) { /* @@ -2940,7 +2961,9 @@ static int trace__fprintf_sample(struct trace *trace, struct perf_sample *sample return printed; } -static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sample, int *augmented_args_size, int raw_augmented_args_size) +static void *syscall__augmented_args(struct trace *trace, struct syscall *sc, + struct perf_sample *sample, + int *augmented_args_size, int raw_augmented_args_size) { /* * For now with BPF raw_augmented we hook into raw_syscalls:sys_enter @@ -2958,6 +2981,24 @@ static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sam */ int args_size = raw_augmented_args_size ?: sc->args_size; + /* + * Augmented arguments are a perf trace specific payload, they are only + * ever appended to samples emitted by the BPF __augmented_syscalls__ + * bpf-output event. + * + * Native syscalls:sys_enter_NAME tracepoints may also carry trailing + * data of their own: since Linux 6.19 they append __data_loc char[] + * fields plus the string payloads they point at. Those bytes are not a + * struct augmented_arg, so treating them as one would make + * syscall_arg__scnprintf_augmented_string() read a bogus length and + * walk arg->augmented.args far out of bounds. + * + * So only look for augmented arguments on the event that can actually + * produce them. + */ + if (sample->evsel != trace->syscalls.events.bpf_output) + return NULL; + *augmented_args_size = sample->raw_size - args_size; if (*augmented_args_size > 0) { static uintptr_t argbuf[1024]; /* assuming single-threaded */ @@ -3016,17 +3057,13 @@ static int trace__sys_enter(struct trace *trace, if (!(trace->duration_filter || trace->summary_only || trace->min_stack)) trace__printf_interrupted_entry(trace); /* - * If this is raw_syscalls.sys_enter, then it always comes with the 6 possible - * arguments, even if the syscall being handled, say "openat", uses only 4 arguments - * this breaks syscall__augmented_args() check for augmented args, as we calculate - * syscall->args_size using each syscalls:sys_enter_NAME tracefs format file, - * so when handling, say the openat syscall, we end up getting 6 args for the - * raw_syscalls:sys_enter event, when we expected just 4, we end up mistakenly - * thinking that the extra 2 u64 args are the augmented filename, so just check - * here and avoid using augmented syscalls when the evsel is the raw_syscalls one. + * syscall__augmented_args() only returns a payload for the BPF + * __augmented_syscalls__ event, so raw_syscalls:sys_enter (which always + * carries all 6 possible arguments rather than sc->args_size worth) and + * the native syscalls:sys_enter_NAME tracepoints are both handled there. */ - if (evsel != trace->syscalls.events.sys_enter) - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); ttrace->entry_time = sample->time; ttrace->entry_cpu = sample->cpu; msg = ttrace->entry_str; @@ -3071,7 +3108,7 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam struct syscall *sc; char msg[1024]; void *args, *augmented_args = NULL; - int augmented_args_size, e_machine; + int augmented_args_size = 0, e_machine; size_t printed = 0; @@ -3089,7 +3126,8 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam goto out_put; args = perf_evsel__sc_tp_ptr(args, sample); - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); printed += syscall__scnprintf_args(sc, msg, sizeof(msg), args, augmented_args, augmented_args_size, trace, thread); fprintf(trace->output, "%.*s", (int)printed, msg); err = 0; @@ -4121,10 +4159,16 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i if (trace->btf == NULL) return -1; - for (i = 0, field = sc->args; field; ++i, field = field->next) { + for (i = 0, field = sc->args; field; field = field->next) { + /* Skip internal fields to keep beauty array index aligned with syscall arguments */ + if (is_internal_field(field)) + continue; + // XXX We're only collecting pointer payloads _from_ user space - if (!sc->arg_fmt[i].from_user) + if (!sc->arg_fmt[i].from_user) { + ++i; continue; + } struct_offset = strstr(field->type, "struct "); if (struct_offset == NULL) @@ -4143,8 +4187,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i name[cnt] = '\0'; /* cache struct's btf_type and type_id */ - if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) + if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) { + ++i; continue; + } bt = sc->arg_fmt[i].type; beauty_array[i] = bt->size; @@ -4170,7 +4216,9 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i struct tep_format_field *field_tmp; /* find the size of the buffer that appears in pairs with buf */ - for (j = 0, field_tmp = sc->args; field_tmp; ++j, field_tmp = field_tmp->next) { + for (j = 0, field_tmp = sc->args; field_tmp; field_tmp = field_tmp->next) { + if (is_internal_field(field_tmp)) + continue; if (!(field_tmp->flags & TEP_FIELD_IS_POINTER) && /* only integers */ (strstr(field_tmp->name, "count") || strstr(field_tmp->name, "siz") || /* size, bufsiz */ @@ -4180,8 +4228,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i can_augment = true; break; } + ++j; } } + ++i; } if (can_augment) @@ -4190,6 +4240,19 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i return -1; } +/* + * Advance to the first field that is a real syscall argument, so that callers + * walking two argument lists in step never have to reason about internal + * fields appearing in one list but not the other. + */ +static struct tep_format_field *next_user_arg(struct tep_format_field *field) +{ + while (field && is_internal_field(field)) + field = field->next; + + return field; +} + static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace, struct syscall *sc) { @@ -4197,7 +4260,7 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace /* * We're only interested in syscalls that have a pointer: */ - for (field = sc->args; field; field = field->next) { + for (field = next_user_arg(sc->args); field; field = next_user_arg(field->next)) { if (field->flags & TEP_FIELD_IS_POINTER) goto try_to_find_pair; } @@ -4215,21 +4278,31 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace pair->bpf_prog.sys_enter == unaugmented_prog) continue; - for (field = sc->args, candidate_field = pair->args; - field && candidate_field; field = field->next, candidate_field = candidate_field->next) { + /* + * Both cursors only ever point at real arguments, so the loop + * ends when one of the two syscalls runs out of them, rather + * than when one happens to have trailing internal fields. + */ + field = next_user_arg(sc->args); + candidate_field = next_user_arg(pair->args); + while (field && candidate_field) { bool is_pointer = field->flags & TEP_FIELD_IS_POINTER, candidate_is_pointer = candidate_field->flags & TEP_FIELD_IS_POINTER; if (is_pointer) { - if (!candidate_is_pointer) { + if (!candidate_is_pointer) { // The candidate just doesn't copies our pointer arg, might copy other pointers we want. + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; - } + } } else { if (candidate_is_pointer) { // The candidate might copy a pointer we don't have, skip it. goto next_candidate; } + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; } @@ -4250,6 +4323,8 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace goto next_candidate; is_candidate = true; + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); } if (!is_candidate) @@ -4261,7 +4336,9 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace * more than what is common to the two syscalls. */ if (candidate_field) { - for (candidate_field = candidate_field->next; candidate_field; candidate_field = candidate_field->next) + candidate_field = next_user_arg(candidate_field->next); + for (; candidate_field; + candidate_field = next_user_arg(candidate_field->next)) if (candidate_field->flags & TEP_FIELD_IS_POINTER) goto next_candidate; } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 03/13] perf trace: Do not set unaugmented BPF program on sys_exit map 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-17 6:42 ` [PATCH v1 01/13] perf trace: Start BPF summary before starting workload Ian Rogers 2026-09-17 6:42 ` [PATCH v1 02/13] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 04/13] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers ` (10 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers In trace__init_syscalls_bpf_prog_array_maps(), the BPF program array map for sys_exit (syscalls_sys_exit) was populated with the result of trace__bpf_prog_sys_exit_fd(). When a syscall had no specific exit augmenter, trace__find_syscall_bpf_prog() fell back to unaugmented_prog (syscall_unaugmented). However, syscall_unaugmented is a sys_enter program that outputs enter arguments to __augmented_syscalls__. As a consequence, when an unaugmented syscall exited, sys_exit tail-called syscall_unaugmented, which interpreted the exit arguments as enter arguments and emitted a duplicate, corrupt sys_enter event into __augmented_syscalls__ right as the syscall completed. Fix this by: 1. Returning NULL from trace__find_syscall_bpf_prog() when looking up exit augmenters and none is found. 2. Returning -1 from trace__bpf_prog_sys_exit_fd() when no exit program is present. 3. Only updating map_exit_fd when prog_fd >= 0. 4. Clearing err = 0 when trace__bpf_sys_enter_beauty_map() returns non-zero (indicating the syscall has no augmentable pointer arguments) before continuing the loop, so a trailing run of such syscalls (e.g. 'perf trace -e close') does not leave err non-zero on return and abort the session. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index af9696aadaec..a68d34256996 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4117,7 +4117,12 @@ static struct bpf_program *trace__find_syscall_bpf_prog(struct trace *trace __ma pr_debug("Couldn't find BPF prog \"%s\" to associate with syscalls:sys_%s_%s, not augmenting it\n", prog_name, type, sc->name); out_unaugmented: - return unaugmented_prog; + /* + * Do not set unaugmented_prog for exit: syscall_unaugmented is a + * sys_enter program that outputs enter arguments. Exit without a + * specialized return augmenter returns 1 directly from sys_exit. + */ + return !strcmp(type, "exit") ? NULL : unaugmented_prog; } static void trace__init_syscall_bpf_progs(struct trace *trace, int e_machine, int id) @@ -4140,7 +4145,7 @@ static int trace__bpf_prog_sys_enter_fd(struct trace *trace, int e_machine, int static int trace__bpf_prog_sys_exit_fd(struct trace *trace, int e_machine, int id) { struct syscall *sc = trace__syscall_info(trace, NULL, e_machine, id); - return sc ? bpf_program__fd(sc->bpf_prog.sys_exit) : bpf_program__fd(unaugmented_prog); + return sc && sc->bpf_prog.sys_exit ? bpf_program__fd(sc->bpf_prog.sys_exit) : -1; } static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, int key, unsigned int *beauty_array) @@ -4395,16 +4400,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace, int e_m err = bpf_map_update_elem(map_enter_fd, &key, &prog_fd, BPF_ANY); if (err) break; + /* Only update the exit prog array map if an exit augmenter exists */ prog_fd = trace__bpf_prog_sys_exit_fd(trace, e_machine, key); - err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); - if (err) - break; + if (prog_fd >= 0) { + err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); + if (err) + break; + } /* use beauty_map to tell BPF how many bytes to collect, set beauty_map's value here */ memset(beauty_array, 0, sizeof(beauty_array)); err = trace__bpf_sys_enter_beauty_map(trace, e_machine, key, (unsigned int *)beauty_array); - if (err) + if (err) { + /* + * Not a failure: the syscall just has no augmentable + * arguments. Clear err, or a trailing run of such + * syscalls, e.g. all of them for 'perf trace -e close', + * would leave it set on return and abort the session. + */ + err = 0; continue; + } err = bpf_map_update_elem(beauty_map_fd, &key, beauty_array, BPF_ANY); if (err) break; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 04/13] perf trace: Filter events in BPF and avoid tracepoint vetoes 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (2 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 03/13] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 05/13] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers ` (9 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers The BPF augmented_raw_syscalls sys_enter and sys_exit programs returned 0 for syscalls that were not of interest. Returning 0 from a tracepoint BPF program vetoes the event for the whole system, so an unrelated concurrent perf trace, perf record or ftrace session listening to raw_syscalls would silently lose events. This is a cross-session side effect and shows up as flaky failures when perf tests run in parallel. Furthermore, syscall_unaugmented previously returned 1 without writing anything to the __augmented_syscalls__ ring buffer. This forced userspace perf trace to listen to both raw_syscalls:sys_enter and __augmented_syscalls__ in its evlist, requiring userspace event deduplication. Address these issues: 1. In augmented_raw_syscalls.bpf.c, never return 0 from tracepoint handlers: return 1 so non-traced syscalls pass through without vetoing other concurrent listeners. 2. Introduce pids_to_trace and syscalls_to_trace BPF hash maps to perform targeted filtering directly in BPF. Unselected syscalls or PIDs return 1 immediately without writing to the buffer. 3. In syscall_unaugmented, output the unaugmented enter payload into __augmented_syscalls__ and return 1. Change its section from SEC("tp/raw_syscalls/sys_enter") to SEC("tp/syscalls/sys_enter_unaugmented") so libbpf does not attempt to auto-attach it to raw_syscalls:sys_enter. 4. In bpf_trace_augment.c, add helpers to configure target PIDs and syscalls in the BPF maps, setting the activation flags (has_pids_to_trace, has_syscalls_to_trace) only after the maps are fully populated so already-attached BPF programs do not filter against a half-filled map. Explicitly attach only sys_enter and sys_exit via an attach_prog() helper that saves -errno before calling pr_debug() or bpf_link__destroy(). Destroy the skeleton on every failure path. Leaving a loaded but unusable skeleton behind is not inert: the setters called later from trace__run() would program its maps, and a partial attach would leave a BPF program live on raw_syscalls for a session that never starts. Since augmented_syscalls__{prepare,create_bpf_output}() failures fall back to unaugmented tracing rather than aborting, those setters have to become no-ops, which they only do once skel is NULL again. errno is used directly here, so include <errno.h> rather than relying on it arriving via another header, which it does not under musl. 5. In builtin-trace.c, hook trace__set_ev_qualifier_filter() and PID filtering into the BPF maps. When __augmented_syscalls__ is active, remove raw_syscalls:sys_enter from trace.evlist since all traced enter events (both augmented and unaugmented) are now emitted by BPF into __augmented_syscalls__. Restore tracking on the remaining evsel via evlist__set_tracking_event() so PERF_RECORD_COMM and tracking events continue to be recorded. Errors from augmented_syscalls__set_target_syscalls() are reported and propagated, the tracepoint filter string is freed on every exit path, and an allocation failure in trace__set_filter_pids() now returns -ENOMEM instead of being silently ignored. Note that in trace__set_filter_pids() the target pids and the filtered pids are two independent axes and both have to be programmed. Naming pids to leave out with --filter-pids does not widen -p/-t or a workload to the whole system, and a BPF tracepoint program is attached system wide rather than to the target's file descriptors, so pids_to_trace is the only thing keeping other tasks out. 6. Add --syscall-augment option (defaulting to true) to allow users to explicitly use --no-syscall-augment to run perf trace in the classic unaugmented tracepoint mode without BPF. When BPF is unavailable or disabled, ensure the non-augmented tracepoint path cleanly configures sys_enter and sys_exit without duplicate entries. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 146 +++++++++++++++--- .../bpf_skel/augmented_raw_syscalls.bpf.c | 133 ++++++++++++++-- tools/perf/util/bpf_trace_augment.c | 135 +++++++++++++++- tools/perf/util/trace_augment.h | 33 ++++ 5 files changed, 414 insertions(+), 38 deletions(-) diff --git a/tools/perf/Documentation/perf-trace.txt b/tools/perf/Documentation/perf-trace.txt index d20b43ea3d37..4680c69160d7 100644 --- a/tools/perf/Documentation/perf-trace.txt +++ b/tools/perf/Documentation/perf-trace.txt @@ -260,6 +260,11 @@ the thread executes on the designated CPUs. Default is to monitor all CPUs. Maximum number of lines in the summary mode. Note that this applies to each entry (thread or cgroup). +--syscall-augment:: + Augment syscalls with BPF. Enabled by default when BPF support is available. + Use --no-syscall-augment to disable BPF augmentation and fall back to the + unaugmented tracepoint approach. + PAGEFAULTS ---------- diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index a68d34256996..e21b2b4a8794 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -200,6 +200,7 @@ struct trace { int max_summary; int raw_augmented_syscalls_args_size; bool raw_augmented_syscalls; + bool syscall_augment; bool fd_path_disabled; bool sort_events; bool not_ev_qualifier; @@ -2054,6 +2055,23 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; + case PERF_RECORD_FORK: + if (trace->raw_augmented_syscalls && + (augmented_syscalls__has_target_pid(event->fork.ppid) || + augmented_syscalls__has_target_pid(event->fork.ptid))) { + augmented_syscalls__add_target_pid(event->fork.pid); + } + ret = machine__process_fork_event(machine, event, sample); + break; + case PERF_RECORD_EXIT: + if (trace->raw_augmented_syscalls) { + if (event->fork.pid == event->fork.tid) + augmented_syscalls__del_target_pid(event->fork.pid); + else + augmented_syscalls__del_target_pid(event->fork.tid); + } + ret = machine__process_exit_event(machine, event, sample); + break; default: ret = machine__process_event(machine, event, sample); break; @@ -4043,7 +4061,7 @@ static int trace__add_syscall_newtp(struct trace *trace) static int trace__set_ev_qualifier_tp_filter(struct trace *trace) { - int err = -1; + int err = 0; struct evsel *sys_exit; char *filter = asprintf_expr_inout_ints("id", !trace->not_ev_qualifier, trace->ev_qualifier_ids.nr, @@ -4052,10 +4070,17 @@ static int trace__set_ev_qualifier_tp_filter(struct trace *trace) if (filter == NULL) goto out_enomem; - if (!evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter)) { - sys_exit = trace->syscalls.events.sys_exit; + /* + * With BPF augmentation sys_enter is filtered in BPF and removed from + * the evlist, so only apply the tracepoint filter to the events that + * are actually present. + */ + if (trace->syscalls.events.sys_enter) + err = evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter); + + sys_exit = trace->syscalls.events.sys_exit; + if (!err && sys_exit) err = evsel__append_tp_filter(sys_exit, filter); - } free(filter); out: @@ -4502,7 +4527,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace __maybe_ static int trace__set_ev_qualifier_filter(struct trace *trace) { - if (trace->syscalls.events.sys_enter) + /* + * Synchronize syscall filter with BPF augmenter map: + * Pass trace->not_ev_qualifier to indicate blacklist mode ('!' prefix, + * e.g., -e !open,close) vs whitelist mode (-e open,close). + * + * A failure here would leave the BPF program filtering on a partially + * populated map, silently dropping or emitting the wrong syscalls, so + * propagate the error rather than continuing. + */ + if (trace->ev_qualifier_ids.nr > 0) { + int err = augmented_syscalls__set_target_syscalls(trace->ev_qualifier_ids.nr, + trace->ev_qualifier_ids.entries, + trace->not_ev_qualifier); + + if (err) { + pr_err("Failed to set the syscalls to trace in the BPF map: %d\n", err); + return err; + } + } + + if (trace->syscalls.events.sys_enter || trace->syscalls.events.sys_exit) return trace__set_ev_qualifier_tp_filter(trace); return 0; } @@ -4543,13 +4588,21 @@ static int trace__set_filter_loop_pids(struct trace *trace) static int trace__set_filter_pids(struct trace *trace) { - int err = 0; + struct perf_thread_map *threads = evlist__core(trace->evlist)->threads; /* * Better not use !target__has_task() here because we need to cover the * case where no threads were specified in the command line, but a * workload was, and in that case we will fill in the thread_map when * we fork the workload in evlist__prepare_workload. */ + bool has_target = perf_thread_map__pid(threads, 0) != -1; + int err = 0; + + /* + * The exclusion list: --filter-pids names tasks to never report, and + * with no target at all we instead exclude perf itself so that tracing + * does not feed back into itself. + */ if (trace->filter_pids.nr > 0) { err = evlist__append_tp_filter_pids(trace->evlist, trace->filter_pids.nr, trace->filter_pids.entries); @@ -4557,10 +4610,37 @@ static int trace__set_filter_pids(struct trace *trace) err = augmented_syscalls__set_filter_pids(trace->filter_pids.nr, trace->filter_pids.entries); } - } else if (perf_thread_map__pid(evlist__core(trace->evlist)->threads, 0) == -1) { + } else if (!has_target) { err = trace__set_filter_loop_pids(trace); } + if (err) + return err; + + /* + * The inclusion list, which is a separate axis from the exclusion list + * above and so must be programmed even when --filter-pids was given: + * naming tasks to leave out does not widen -p/-t or a workload to the + * whole system. + * + * This matters more than it does on the tracepoint only path. A BPF + * tracepoint program is attached system wide rather than to the + * target's file descriptors, so pids_to_trace is the only thing + * keeping other tasks out. + */ + if (has_target) { + int nr = perf_thread_map__nr(threads); + pid_t *pids = malloc(nr * sizeof(pid_t)); + + if (pids == NULL) + return -ENOMEM; + + for (int i = 0; i < nr; i++) + pids[i] = perf_thread_map__pid(threads, i); + err = augmented_syscalls__set_target_pids(nr, pids); + free(pids); + } + return err; } @@ -4787,7 +4867,8 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } if (!trace->raw_augmented_syscalls) { - if (trace->trace_syscalls && trace__add_syscall_newtp(trace)) + if (trace->trace_syscalls && !trace->syscalls.events.sys_enter && + trace__add_syscall_newtp(trace)) goto out_error_raw_syscalls; if (trace->trace_syscalls) @@ -5795,6 +5876,7 @@ int cmd_trace(int argc, const char **argv) .show_arg_names = true, .args_alignment = 70, .trace_syscalls = false, + .syscall_augment = true, .kernel_syscallchains = false, .max_stack = UINT_MAX, .max_events = ULONG_MAX, @@ -5850,6 +5932,8 @@ int cmd_trace(int argc, const char **argv) OPT_CALLBACK_DEFAULT('F', "pf", &trace.trace_pgfaults, "all|maj|min", "Trace pagefaults", parse_pagefaults, "maj"), OPT_BOOLEAN(0, "syscalls", &trace.trace_syscalls, "Trace syscalls"), + OPT_BOOLEAN(0, "syscall-augment", &trace.syscall_augment, + "Augment syscalls with BPF"), OPT_BOOLEAN('f', "force", &trace.force, "don't complain, do it"), OPT_CALLBACK(0, "call-graph", &trace.opts, "record_mode[,record_size]", record_callchain_help, @@ -5972,7 +6056,7 @@ int cmd_trace(int argc, const char **argv) "cgroup monitoring only available in system-wide mode"); } - if (!trace.trace_syscalls) + if (!trace.trace_syscalls || !trace.syscall_augment) goto skip_augmentation; if ((argc >= 1) && (strcmp(argv[0], "record") == 0)) { @@ -5997,8 +6081,19 @@ int cmd_trace(int argc, const char **argv) trace__add_syscall_newtp(&trace); err = augmented_syscalls__create_bpf_output(trace.evlist); - if (err == 0) + if (err == 0) { trace.syscalls.events.bpf_output = evlist__last(trace.evlist); + } else { + /* + * augmented_syscalls__prepare() already attached sys_enter and + * sys_exit, which are system wide. Falling through to + * skip_augmentation without undoing that would run a BPF + * program for every syscall on the machine, for the whole + * session, with nothing consuming the output. + */ + pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); + augmented_syscalls__cleanup(); + } skip_augmentation: err = -1; @@ -6054,7 +6149,9 @@ int cmd_trace(int argc, const char **argv) * syscall. */ if (trace.syscalls.events.bpf_output) { - evlist__for_each_entry(trace.evlist, evsel) { + struct evsel *n; + + evlist__for_each_entry_safe(trace.evlist, n, evsel) { bool raw_syscalls_sys_exit = evsel__name_is(evsel, "raw_syscalls:sys_exit"); if (raw_syscalls_sys_exit) { @@ -6069,21 +6166,26 @@ int cmd_trace(int argc, const char **argv) evsel__init_augmented_syscall_tp_args(augmented)) goto out; /* - * Augmented is __augmented_syscalls__ BPF_OUTPUT event + * Augmented is __augmented_syscalls__ BPF_OUTPUT event. * Above we made sure we can get from the payload the tp fields * that we get from syscalls:sys_enter tracefs format file. + * Since BPF outputs all enter events (both augmented and + * unaugmented) into __augmented_syscalls__, we remove the raw + * sys_enter evsel from evlist so that perf trace only listens + * to __augmented_syscalls__, avoiding duplicate events and + * avoiding kernel tracepoint vetoes. + * + * Because evlist__remove() removes the first evsel (which had + * tracking=true by default), re-designate the tracking event + * so PERF_RECORD_COMM and fork tracking continue to be enabled. */ augmented->handler = trace__sys_enter; - /* - * Now we do the same for the *syscalls:sys_enter event so that - * if we handle it directly, i.e. if the BPF prog returns 0 so - * as not to filter it, then we'll handle it just like we would - * for the BPF_OUTPUT one: - */ - if (evsel__init_augmented_syscall_tp(evsel, evsel) || - evsel__init_augmented_syscall_tp_args(evsel)) - goto out; - evsel->handler = trace__sys_enter; + evlist__remove(trace.evlist, evsel); + evsel__put_and_free_priv(evsel); + trace.syscalls.events.sys_enter = NULL; + evlist__set_tracking_event(trace.evlist, + trace.syscalls.events.sys_exit ?: augmented); + continue; } if (strstarts(evsel__name(evsel), "syscalls:sys_exit_")) { diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 3bc9e28a9b8a..6ca9507ecc02 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -114,6 +114,41 @@ struct pids_filtered { __uint(max_entries, 64); } pids_filtered SEC(".maps"); +/* + * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when + * attached to a process with -p or tracing a specific command workload). + * + * has_pids_to_trace: Set to true if target PID filtering is active. + * When false, all processes are eligible for tracing. + */ +struct pids_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, pid_t); + __type(value, bool); + __uint(max_entries, 1024); +} pids_to_trace SEC(".maps"); + +bool has_pids_to_trace; + +/* + * Hash map storing syscall IDs for filtering (via 'perf trace -e ...'). + * + * has_syscalls_to_trace: Set to true if any syscall filter is active. + * not_syscalls_to_trace: Inverts matching when '!' prefix is used in -e + * (e.g., -e !open,close means trace everything EXCEPT + * open and close; an exclusion blacklist rather than + * an inclusion whitelist). + */ +struct syscalls_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, int); + __type(value, bool); + __uint(max_entries, 1024); +} syscalls_to_trace SEC(".maps"); + +bool has_syscalls_to_trace; +bool not_syscalls_to_trace; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -154,8 +189,8 @@ static inline struct augmented_args_payload *augmented_args_payload(void) static inline int augmented__output(void *ctx, struct augmented_args_payload *args, int len) { - /* If perf_event_output fails, return non-zero so that it gets recorded unaugmented */ - return bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + return 1; } static inline int augmented__beauty_output(void *ctx, void *data, int len) @@ -191,10 +226,21 @@ unsigned int augmented_arg__read_str(struct augmented_arg *augmented_arg, const return augmented_len; } -SEC("tp/raw_syscalls/sys_enter") +/* + * Default sys_enter program for syscalls without pointer argument augmentation. + * Writes the raw struct syscall_enter_args payload into __augmented_syscalls__ + * and returns 1 so the tracepoint is never vetoed in the kernel. + */ +SEC("tp/syscalls/sys_enter_unaugmented") int syscall_unaugmented(struct syscall_enter_args *args) { - return 1; + struct augmented_args_payload *augmented_args = augmented_args_payload(); + + if (augmented_args == NULL) + return 1; + + bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + return augmented__output(args, augmented_args, sizeof(augmented_args->args)); } /* @@ -424,11 +470,41 @@ static pid_t getpid(void) return bpf_get_current_pid_tgid(); } +/* + * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { return bpf_map_lookup_elem(pids, &pid) != NULL; } +/* + * Checks if the current task (thread PID or process TGID) is targeted for tracing. + * Checks both PID (thread ID) and TGID (process ID) so that all threads of a + * target process match. + */ +static inline bool pid_to_trace__has(pid_t pid) +{ + pid_t tgid = bpf_get_current_pid_tgid() >> 32; + + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || + bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; +} + +/* + * Determines if a syscall should be traced based on the filter map: + * - When not_syscalls_to_trace is true: blacklist mode (trace if NOT in map). + * - When not_syscalls_to_trace is false: whitelist mode (trace ONLY if IN map). + */ +static inline bool syscall_to_trace__enabled(int id) +{ + bool in_map = bpf_map_lookup_elem(&syscalls_to_trace, &id) != NULL; + + if (not_syscalls_to_trace) + return !in_map; + return in_map; +} + u64 ZERO = 0; /* @@ -562,6 +638,11 @@ static int augment_sys_enter(void *ctx, struct syscall_enter_args *args) return augmented__beauty_output(ctx, payload, sizeof(struct syscall_enter_args) + output); } +/* + * Main raw_syscalls:sys_enter tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel for + * other concurrent listeners. Filtered events simply do not output to the ring buffer. + */ SEC("tp/raw_syscalls/sys_enter") int sys_enter(struct syscall_enter_args *args) { @@ -576,8 +657,11 @@ int sys_enter(struct syscall_enter_args *args) * initial, non-augmented raw_syscalls:sys_enter payload. */ + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; augmented_args = augmented_args_payload(); if (augmented_args == NULL) @@ -585,25 +669,41 @@ int sys_enter(struct syscall_enter_args *args) bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + if (has_syscalls_to_trace && !syscall_to_trace__enabled(augmented_args->args.syscall_nr)) + return 1; + /* - * Jump to syscall specific augmenter, even if the default one, - * "!raw_syscalls:unaugmented" that will just return 1 to return the - * unaugmented tracepoint payload. + * Jump to syscall specific augmenter. If augmented, augment_sys_enter() + * outputs the payload to __augmented_syscalls__ and returns 0. + * Return 1 so we never veto the kernel tracepoint for other listeners. */ - if (augment_sys_enter(args, &augmented_args->args)) - bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + if (augment_sys_enter(args, &augmented_args->args) == 0) + return 1; - // If not found on the PROG_ARRAY syscalls map, then we're filtering it: - return 0; + bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + + /* + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. + */ + return 1; } +/* + * Main raw_syscalls:sys_exit tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel. + */ SEC("tp/raw_syscalls/sys_exit") int sys_exit(struct syscall_exit_args *args) { struct syscall_exit_args exit_args; + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); /* @@ -613,9 +713,12 @@ int sys_exit(struct syscall_exit_args *args) */ bpf_tail_call(args, &syscalls_sys_exit, exit_args.syscall_nr); /* - * If not found on the PROG_ARRAY syscalls map, then we're filtering it: + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. perf trace's own evsel filter will discard non-matching + * syscalls. */ - return 0; + return 1; } char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index a9cf2a77ded1..b9d1208e50f2 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,4 +1,5 @@ #include <bpf/libbpf.h> +#include <errno.h> #include <internal/xyarray.h> #include "bpf_skel/augmented_raw_syscalls.skel.h" @@ -10,6 +11,23 @@ static struct augmented_raw_syscalls_bpf *skel; static struct evsel *bpf_output; +/* Set by attach_prog() so the first failure is what gets reported. */ +static int attach_err; + +static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const char *name) +{ + *link = bpf_program__attach(prog); + if (*link) + return 0; + /* + * Save errno before pr_debug(), which formats and writes output and so + * can overwrite it. + */ + attach_err = -errno; + pr_debug("Failed to attach %s BPF program\n", name); + return attach_err; +} + int augmented_syscalls__prepare(void) { struct bpf_program *prog; @@ -35,11 +53,35 @@ int augmented_syscalls__prepare(void) if (err < 0) { libbpf_strerror(err, buf, sizeof(buf)); pr_debug("Failed to load augmented syscalls BPF skeleton: %s\n", buf); + /* + * Tear the skeleton down rather than leaving a half initialized + * one behind. The caller falls back to unaugmented tracing and + * still calls the setters below, which must then do nothing + * instead of failing against a skeleton with no maps. + */ + augmented_syscalls__cleanup(); return err; } - augmented_raw_syscalls_bpf__attach(skel); + /* + * Only sys_enter and sys_exit are attached, the remaining programs are + * reached by tail calls. Attach them explicitly and, on failure, undo + * any partial attachment: leaving sys_enter live on + * raw_syscalls:sys_enter would keep running a BPF program for every + * syscall on the system for a perf trace session that never starts. + */ + if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) + goto out_cleanup; + if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) + goto out_cleanup; + return 0; + +out_cleanup: + err = attach_err; + /* Destroys every link attached above along with the skeleton. */ + augmented_syscalls__cleanup(); + return err; } int augmented_syscalls__create_bpf_output(struct evlist *evlist) @@ -98,6 +140,96 @@ int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) return err; } +/* + * Populate target PIDs in the BPF pids_to_trace map (e.g., for -p <PID> or + * when tracing a specified command workload). + */ +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) +{ + bool value = true; + int err = 0; + + if (skel == NULL || nr == 0) + return 0; + + for (size_t i = 0; i < nr; ++i) { + err = bpf_map__update_elem(skel->maps.pids_to_trace, &pids[i], + sizeof(*pids), &value, sizeof(value), + BPF_ANY); + if (err) + return err; + } + /* + * Set the flag only once every target is in the map. The BPF programs + * are attached by this point, so flipping it first would have them + * filter against a partially populated map and drop syscalls made by + * the targets that had not been added yet. + */ + skel->bss->has_pids_to_trace = true; + return 0; +} + +int augmented_syscalls__add_target_pid(pid_t pid) +{ + bool value = true; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), BPF_ANY); +} + +int augmented_syscalls__del_target_pid(pid_t pid) +{ + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); +} + +bool augmented_syscalls__has_target_pid(pid_t pid) +{ + bool value; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return false; + + return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), 0) == 0; +} + +/* + * Populate syscalls in the BPF syscalls_to_trace map: + * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace + * all syscalls EXCEPT these). + * false if whitelist mode (trace ONLY these syscalls). + */ +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls) +{ + bool value = true; + int err = 0; + + if (skel == NULL || nr == 0) + return 0; + + skel->bss->not_syscalls_to_trace = not_syscalls; + for (size_t i = 0; i < nr; ++i) { + err = bpf_map__update_elem(skel->maps.syscalls_to_trace, &syscall_ids[i], + sizeof(int), &value, sizeof(value), + BPF_ANY); + if (err) + return err; + } + /* + * As for the pid maps, publish the filter only once it is complete: + * in whitelist mode a half filled map would drop syscalls that were + * asked for but not added yet. + */ + skel->bss->has_syscalls_to_trace = true; + return 0; +} + int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd) { if (skel == NULL) @@ -140,4 +272,5 @@ struct bpf_program *augmented_syscalls__find_by_title(const char *name) void augmented_syscalls__cleanup(void) { augmented_raw_syscalls_bpf__destroy(skel); + skel = NULL; } diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 4f729bc67753..56f4dbab7d4b 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -12,6 +12,11 @@ int augmented_syscalls__prepare(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__add_target_pid(pid_t pid); +int augmented_syscalls__del_target_pid(pid_t pid); +bool augmented_syscalls__has_target_pid(pid_t pid); +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); struct bpf_program *augmented_syscalls__unaugmented(void); @@ -39,6 +44,34 @@ static inline int augmented_syscalls__set_filter_pids(unsigned int nr __maybe_un return 0; } +static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_unused, + pid_t *pids __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) +{ + return false; +} + +static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, + int *syscall_ids __maybe_unused, + bool not_syscalls __maybe_unused) +{ + return 0; +} + static inline int augmented_syscalls__get_map_fds(int *enter_fd __maybe_unused, int *exit_fd __maybe_unused, int *beauty_fd __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 05/13] perf trace: Handle fork and exit directly in BPF filter maps 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (3 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 04/13] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 06/13] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers ` (8 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers Updating target or filtered PIDs in userspace upon processing PERF_RECORD_FORK and PERF_RECORD_EXIT events introduces latency between event occurrence and userspace BPF map updates. If a newly forked child executes system calls before userspace processes PERF_RECORD_FORK, those syscalls may be dropped by BPF PID filtering. Conversely, if userspace evicts PIDs asynchronously on PERF_RECORD_EXIT, the kernel may recycle a PID before userspace processes the exit event, causing the late eviction to silently drop a newly created task that received the recycled PID. Address this by attaching BTF-typed raw tracepoint BPF programs directly to the scheduler task lifetime tracepoints: 1. Attach SEC("tp_btf/sched_process_fork") (sched_process_fork), which runs in copy_process() in the parent's context before wake_up_new_task() wakes the child. Using tp_btf rather than SEC("tp/sched/sched_process_fork") receives the stable TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) rather than the tracepoint ring-buffer record (TP_STRUCT__entry), whose layout changed in Linux 6.16 when parent_comm and child_comm were converted from 16-byte arrays to 4-byte __data_loc strings. When inherit is enabled and the parent's PID or TGID is in pids_to_trace or pids_filtered, insert child->pid into the corresponding map immediately. Because child->pid is task_struct.pid (the global initial-namespace PID), this works accurately across PID namespaces without aliasing host PIDs, and covers both new processes and CLONE_THREAD threads without needing real_parent CO-RE walks or syscall-return heuristics. 2. Attach SEC("tp_btf/sched_process_exit") (sched_process_exit), which runs in do_exit() for every task in its own context, including tasks killed by signals (SIGKILL, SIGSEGV, etc.) and secondary threads torn down implicitly by exit_group. Delete the dying task's PID from pids_to_trace and pids_filtered immediately in kernel space, eliminating both map leaks and any asynchronous userspace eviction window where PID recycling could occur. 3. Attach SEC("tp_btf/sched_process_exec") (sched_process_exec) to follow the one case where a live task's pid changes underneath the maps. When a thread that is not the group leader execs, de_thread() kills the leader and hands the leader's pid, which is the tgid, to the exec'ing thread. The leader dies first, so sched_process_exit() has already dropped exactly the pid the survivor now holds, and the survivor's old entry would be stranded in the map for good. Move the entry from old_pid to p->pid. old_pid is sampled in bprm_execve() before de_thread() runs, so the ordinary group leader exec is a no-op here. 4. With every live task registered before its first syscall and evicted in do_exit(), simplify pid_to_trace__has() and pid_filter__has() to single BPF hash map lookups, and move bpf_probe_read_kernel() in sys_exit back after the PID filter checks. 5. Pass the inherit flag from userspace to BPF .rodata via augmented_syscalls__prepare(!trace.opts.no_inherit), and split attaching out of it into augmented_syscalls__attach(), called from trace__run() once the pid, syscall and program array maps have all been programmed. These are system wide programs, so from the instant they attach they alone decide what is traced: attaching at load time, as before, left a window in which a target could fork without sched_process_fork() knowing the parent was a target, and with the userspace fork handling gone there was nothing left to recover it. The scheduler programs are attached ahead of sys_enter and sys_exit for the same reason. Set has_pids_filtered only after populating pids_filtered. 6. Remove the userspace BPF map updates from PERF_RECORD_FORK and PERF_RECORD_EXIT in trace__process_event(), and delete the now-unused augmented_syscalls__{add,del,has}_target_pid() helpers. No coverage is lost with them: those records only come into being once the ring buffers are mapped by evlist__do_mmap() and the events are switched on by evlist__enable(), both of which run after augmented_syscalls__attach() in trace__run(), and they are then acted on later still, whenever the poll loop gets round to them. The scheduler programs therefore go live strictly earlier than the userspace path could ever have reacted. 7. Gate pid_filter__has() on a has_pids_filtered flag in .bss so the common case without --filter-pids performs no map lookups, and size pids_to_trace and pids_filtered at 16384 entries. pids_filtered is grown from 64 because it is no longer just the handful of pids userspace names: sched_process_fork() adds every descendant of those, so a --filter-pids target that forks or is heavily threaded needs the same headroom as a traced one. A fork or exit is still not seen if it happens before the programs are attached, that is between evlist__create_maps() scanning /proc for a -p target and augmented_syscalls__attach(). Such a window is inherent in programming a system wide filter before switching it on, and as above the userspace handling did not cover it either. What it costs is small: a child forked in the window is still traced through the tracepoints its parent's events were inherited by, only unaugmented, because sys_enter returns 1 for a pid that is not in the map rather than vetoing the tracepoint. A workload started by 'perf trace -- cmd' cannot hit it at all, as evlist__prepare_workload() leaves the child blocked on a pipe until evlist__start_workload(), well after the attach. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 39 ++-- .../bpf_skel/augmented_raw_syscalls.bpf.c | 178 +++++++++++++++++- tools/perf/util/bpf_trace_augment.c | 116 +++++++----- tools/perf/util/trace_augment.h | 28 +-- 4 files changed, 262 insertions(+), 99 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index e21b2b4a8794..a30fe273f452 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2055,23 +2055,6 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; - case PERF_RECORD_FORK: - if (trace->raw_augmented_syscalls && - (augmented_syscalls__has_target_pid(event->fork.ppid) || - augmented_syscalls__has_target_pid(event->fork.ptid))) { - augmented_syscalls__add_target_pid(event->fork.pid); - } - ret = machine__process_fork_event(machine, event, sample); - break; - case PERF_RECORD_EXIT: - if (trace->raw_augmented_syscalls) { - if (event->fork.pid == event->fork.tid) - augmented_syscalls__del_target_pid(event->fork.pid); - else - augmented_syscalls__del_target_pid(event->fork.tid); - } - ret = machine__process_exit_event(machine, event, sample); - break; default: ret = machine__process_event(machine, event, sample); break; @@ -4982,6 +4965,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } } + /* + * Everything the BPF programs filter on is now in their maps, so it is + * safe to let them run. They are attached system wide, so anything + * before this point would have been filtered against a map that was + * still being built up. + */ + err = augmented_syscalls__attach(); + if (err < 0) + goto out_errno; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the @@ -6074,7 +6067,7 @@ int cmd_trace(int argc, const char **argv) goto skip_augmentation; } - err = augmented_syscalls__prepare(); + err = augmented_syscalls__prepare(!trace.opts.no_inherit); if (err < 0) goto skip_augmentation; @@ -6085,11 +6078,11 @@ int cmd_trace(int argc, const char **argv) trace.syscalls.events.bpf_output = evlist__last(trace.evlist); } else { /* - * augmented_syscalls__prepare() already attached sys_enter and - * sys_exit, which are system wide. Falling through to - * skip_augmentation without undoing that would run a BPF - * program for every syscall on the machine, for the whole - * session, with nothing consuming the output. + * Drop the loaded skeleton before falling back to unaugmented + * tracing. Otherwise the setters called from trace__run() would + * still program its maps, and augmented_syscalls__attach() would + * then put system wide BPF programs on raw_syscalls for a + * session with nothing consuming their output. */ pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); augmented_syscalls__cleanup(); diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 6ca9507ecc02..7124ed3c39c8 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -9,6 +9,7 @@ #include "vmlinux.h" #include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> #include <linux/limits.h> #define PERF_ALIGN(x, a) __PERF_ALIGN_MASK(x, (typeof(x))(a)-1) @@ -107,25 +108,48 @@ struct augmented_arg { }; }; +/* + * Hash map of PIDs/TGIDs whose events must be discarded, e.g. perf trace's own + * pid, so that tracing doesn't feed back on itself. + * + * has_pids_filtered: set to true only when the map is populated. Checking a + * boolean is much cheaper than a map lookup, and sys_enter + * runs for every syscall on the system, so the common + * "no pids filtered" case must stay on a fast path. + * + * max_entries matches pids_to_trace: userspace only ever names a handful of + * pids here, but sched_process_fork() below adds every descendant of those, + * so a --filter-pids target that forks or is heavily threaded needs the same + * headroom as a traced one. + */ struct pids_filtered { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 64); + __uint(max_entries, 16384); } pids_filtered SEC(".maps"); +bool has_pids_filtered; + /* * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when * attached to a process with -p or tracing a specific command workload). * * has_pids_to_trace: Set to true if target PID filtering is active. * When false, all processes are eligible for tracing. + * + * max_entries bounds how many tasks can be tracked at once. sched_process_exit + * below evicts a task as it dies, whatever it died of, so the map holds live + * tasks rather than growing without bound. It is sized well + * above the thread count of realistic traced workloads; should a workload + * still exceed it, bpf_map_update_elem() fails with -E2BIG and the extra + * tasks are simply not traced rather than anything being corrupted. */ struct pids_to_trace { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 1024); + __uint(max_entries, 16384); } pids_to_trace SEC(".maps"); bool has_pids_to_trace; @@ -149,6 +173,9 @@ struct syscalls_to_trace { bool has_syscalls_to_trace; bool not_syscalls_to_trace; +/* Inherit tracing for child tasks (set to false if --no-inherit is specified) */ +const volatile bool inherit = true; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -471,24 +498,35 @@ static pid_t getpid(void) } /* - * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * Checks if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * + * Children of a filtered task are added to the map by sched_process_fork() + * below, so a plain lookup is all that is needed here. */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { + /* + * Fast path: this runs for every syscall on the system, so when no pid + * is filtered do no work at all rather than failing a lookup. + */ + if (!has_pids_filtered) + return false; + return bpf_map_lookup_elem(pids, &pid) != NULL; } /* - * Checks if the current task (thread PID or process TGID) is targeted for tracing. - * Checks both PID (thread ID) and TGID (process ID) so that all threads of a - * target process match. + * Checks if the current task is targeted for tracing. + * + * Every thread that existed when tracing started was named by the target and + * inserted from userspace, and every task created since was inserted by + * sched_process_fork() below, before it was able to run. So there is nothing + * to derive here, and in particular no need to consult the tgid or walk to the + * parent: a task is traced if and only if it is in the map. */ static inline bool pid_to_trace__has(pid_t pid) { - pid_t tgid = bpf_get_current_pid_tgid() >> 32; - - return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || - bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL; } /* @@ -706,6 +744,7 @@ int sys_exit(struct syscall_exit_args *args) return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); + /* * Jump to syscall specific return augmenter, even if the default one, * "!raw_syscalls:unaugmented" that will just return 1 to return the @@ -721,4 +760,123 @@ int sys_exit(struct syscall_exit_args *args) return 1; } +/* + * Propagate tracing to a newly created task. + * + * tp_btf/sched_process_fork is raised by copy_process(), in the parent's + * context and before the child is woken, so the child is in the maps before it + * can issue its first syscall. That removes the need to inspect real_parent + * when a syscall is seen from an unknown task, which could neither tell a + * genuine descendant from a task merely reparented to a traced init, nor keep + * following a descendant whose parent had already exited. + * + * Using tp_btf rather than tp/sched/sched_process_fork avoids depending on the + * tracepoint ring-buffer record layout (TP_STRUCT__entry), which changed in + * Linux 6.16 when parent_comm and child_comm were converted from fixed 16-byte + * arrays to 4-byte __data_loc strings (shrinking the tracepoint context from + * 48 to 24 bytes and causing BPF_PROG_TYPE_TRACEPOINT attachment to fail with + * -EACCES when accessing higher offsets). Instead, tp_btf receives the stable + * TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) + * directly. + * + * child->pid is task_struct.pid, i.e. the pid in the initial namespace, which + * is what the maps are keyed by. A clone() return value, in contrast, is the + * pid in the caller's namespace and would alias an unrelated host task when a + * containerised workload is traced. + * + * CLONE_THREAD needs no special handling: a new thread arrives here like any + * other task and is inserted under its own pid. + */ +SEC("tp_btf/sched_process_fork") +int BPF_PROG(sched_process_fork, struct task_struct *parent, struct task_struct *child) +{ + pid_t parent_tgid, parent_pid, child_pid; + bool val = true; + + if (!inherit) + return 0; + + /* + * The parent's own pid and tgid: the thread that called clone() may + * itself only be tracked by the pid of its thread group leader. + */ + parent_pid = parent->pid; + parent_tgid = parent->tgid; + child_pid = child->pid; + + if (has_pids_to_trace && + (bpf_map_lookup_elem(&pids_to_trace, &parent_pid) != NULL || + bpf_map_lookup_elem(&pids_to_trace, &parent_tgid) != NULL)) + bpf_map_update_elem(&pids_to_trace, &child_pid, &val, BPF_ANY); + + if (has_pids_filtered && + (bpf_map_lookup_elem(&pids_filtered, &parent_pid) != NULL || + bpf_map_lookup_elem(&pids_filtered, &parent_tgid) != NULL)) + bpf_map_update_elem(&pids_filtered, &child_pid, &val, BPF_ANY); + + return 0; +} + +/* + * Drop a dying task from the maps. + * + * tp_btf/sched_process_exit is raised by do_exit() for every task, in its own + * context, so unlike hooking the exit and exit_group syscalls this also covers + * tasks killed by a signal and threads torn down implicitly by exit_group. + * + * Doing it here rather than from the userspace PERF_RECORD_EXIT handler also + * means there is no window between the task dying and the map being updated, + * during which the kernel could recycle the pid and the late eviction silently + * stop tracing whichever new task received it. + * + * Each thread is reported separately, including the group leader, whose pid is + * the thread group's tgid, so one delete per map covers both uses of the key. + */ +SEC("tp_btf/sched_process_exit") +int BPF_PROG(sched_process_exit, struct task_struct *p) +{ + pid_t pid = p->pid; + + bpf_map_delete_elem(&pids_to_trace, &pid); + bpf_map_delete_elem(&pids_filtered, &pid); + + return 0; +} + +/* + * Follow a task whose pid changed under it. + * + * When a thread that is not the thread group leader execs, de_thread() kills + * the rest of the group and then hands the leader's pid, which is the tgid, to + * the exec'ing thread. The leader dies first, so sched_process_exit() above + * has already dropped that pid from the maps, and the survivor is now keyed by + * a pid nothing knows about while its original entry is left behind for good. + * + * Move the entry across so the task stays tracked and nothing is leaked. + * old_pid is sampled in bprm_execve() before de_thread() runs, so for the + * common case of the group leader exec'ing it simply equals p->pid and there + * is nothing to do. + */ +SEC("tp_btf/sched_process_exec") +int BPF_PROG(sched_process_exec, struct task_struct *p, pid_t old_pid) +{ + pid_t pid = p->pid; + bool val = true; + + if (pid == old_pid) + return 0; + + if (bpf_map_lookup_elem(&pids_to_trace, &old_pid) != NULL) { + bpf_map_update_elem(&pids_to_trace, &pid, &val, BPF_ANY); + bpf_map_delete_elem(&pids_to_trace, &old_pid); + } + + if (bpf_map_lookup_elem(&pids_filtered, &old_pid) != NULL) { + bpf_map_update_elem(&pids_filtered, &pid, &val, BPF_ANY); + bpf_map_delete_elem(&pids_filtered, &old_pid); + } + + return 0; +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index b9d1208e50f2..41a867d60b37 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -28,7 +28,7 @@ static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const c return attach_err; } -int augmented_syscalls__prepare(void) +int augmented_syscalls__prepare(bool inherit) { struct bpf_program *prog; char buf[128]; @@ -40,12 +40,18 @@ int augmented_syscalls__prepare(void) return -errno; } + skel->rodata->inherit = inherit; + /* - * Disable attaching the BPF programs except for sys_enter and - * sys_exit that tail call into this as necessary. + * Disable attaching the BPF programs other than those attached + * explicitly by augmented_syscalls__attach(), the rest are reached by + * tail calls. */ bpf_object__for_each_program(prog, skel->obj) { - if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit) + if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit && + prog != skel->progs.sched_process_fork && + prog != skel->progs.sched_process_exit && + prog != skel->progs.sched_process_exec) bpf_program__set_autoattach(prog, /*autoattach=*/false); } @@ -63,13 +69,42 @@ int augmented_syscalls__prepare(void) return err; } + return 0; +} + +int augmented_syscalls__attach(void) +{ + int err; + + if (skel == NULL) + return 0; + /* - * Only sys_enter and sys_exit are attached, the remaining programs are - * reached by tail calls. Attach them explicitly and, on failure, undo - * any partial attachment: leaving sys_enter live on - * raw_syscalls:sys_enter would keep running a BPF program for every - * syscall on the system for a perf trace session that never starts. + * Attaching is deliberately separate from, and a lot later than, + * loading: these are system wide tracepoint programs, so from the + * moment they are attached they are the only thing deciding which + * tasks and syscalls are traced. Going live before the pid and + * syscall maps are populated would mean a target that forked in the + * meantime was never picked up by sched_process_fork() below. + * + * Attach explicitly, so that a failure part way through can undo what + * came before it: leaving sys_enter live on raw_syscalls:sys_enter + * would keep running a BPF program for every syscall on the system for + * a perf trace session that never starts. + * + * The scheduler programs maintain the pid maps, and are attached first + * so that no fork, exit or exec can be missed between sys_enter going + * live and the maps being maintained. */ + if (attach_prog(&skel->links.sched_process_fork, skel->progs.sched_process_fork, + "sched_process_fork")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exit, skel->progs.sched_process_exit, + "sched_process_exit")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exec, skel->progs.sched_process_exec, + "sched_process_exec")) + goto out_cleanup; if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) goto out_cleanup; if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) @@ -81,6 +116,13 @@ int augmented_syscalls__prepare(void) err = attach_err; /* Destroys every link attached above along with the skeleton. */ augmented_syscalls__cleanup(); + /* + * Tearing the skeleton down closes file descriptors and frees memory, + * either of which may overwrite errno. Restore it so that a caller + * reporting this with "%m" describes the attach failure rather than + * whatever the teardown happened to do last. + */ + errno = -err; return err; } @@ -127,17 +169,29 @@ int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) bool value = true; int err = 0; - if (skel == NULL) + if (skel == NULL || nr == 0) return 0; + /* + * Tell the BPF program that the pids_filtered map is in use. Without + * this it would have to look up every task in an empty map, on every + * syscall on the system, to find out that nothing is filtered. + */ for (size_t i = 0; i < nr; ++i) { err = bpf_map__update_elem(skel->maps.pids_filtered, &pids[i], sizeof(*pids), &value, sizeof(value), BPF_ANY); if (err) - break; + return err; } - return err; + /* + * Publish the filter only now that the map is fully populated. + * augmented_syscalls__attach() has not run yet, so nothing is reading + * either of them, but keeping the flag and the map consistent means + * the ordering stays correct however the callers are rearranged. + */ + skel->bss->has_pids_filtered = true; + return 0; } /* @@ -160,45 +214,15 @@ int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) return err; } /* - * Set the flag only once every target is in the map. The BPF programs - * are attached by this point, so flipping it first would have them - * filter against a partially populated map and drop syscalls made by - * the targets that had not been added yet. + * Set the flag only once every target is in the map, so that the two + * are never inconsistent. Publishing it first would, once the + * programs are attached, have them filter against a partially + * populated map and drop syscalls made by targets not yet added. */ skel->bss->has_pids_to_trace = true; return 0; } -int augmented_syscalls__add_target_pid(pid_t pid) -{ - bool value = true; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), BPF_ANY); -} - -int augmented_syscalls__del_target_pid(pid_t pid) -{ - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); -} - -bool augmented_syscalls__has_target_pid(pid_t pid) -{ - bool value; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return false; - - return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), 0) == 0; -} - /* * Populate syscalls in the BPF syscalls_to_trace map: * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 56f4dbab7d4b..f9eecff7efad 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -8,14 +8,12 @@ struct evlist; #ifdef HAVE_BPF_SKEL -int augmented_syscalls__prepare(void); +int augmented_syscalls__prepare(bool inherit); +int augmented_syscalls__attach(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); -int augmented_syscalls__add_target_pid(pid_t pid); -int augmented_syscalls__del_target_pid(pid_t pid); -bool augmented_syscalls__has_target_pid(pid_t pid); int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); @@ -24,11 +22,16 @@ void augmented_syscalls__cleanup(void); #else /* !HAVE_BPF_SKEL */ -static inline int augmented_syscalls__prepare(void) +static inline int augmented_syscalls__prepare(bool inherit __maybe_unused) { return -1; } +static inline int augmented_syscalls__attach(void) +{ + return 0; +} + static inline int augmented_syscalls__create_bpf_output(struct evlist *evlist __maybe_unused) { return -1; @@ -50,21 +53,6 @@ static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_un return 0; } -static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) -{ - return false; -} - static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, int *syscall_ids __maybe_unused, bool not_syscalls __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 06/13] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (4 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 05/13] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 07/13] perf test common: Do not globally disable tracing events in clear_all_probes Ian Rogers ` (7 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers test_task_analyzer.sh writes perf.data and temporary files directly into the current working directory, causing collisions when running tests in parallel. As a temporary measure until `perf script report` supports an input file option, resolve perfdir to an absolute path, change directory into $tmpdir for the test duration, and clean up in the exit trap. Remove the (exclusive) tag so the test runs in parallel. perfdir is derived from $0, which may be relative, so it has to be resolved before the cd into $tmpdir, otherwise both PERF_EXEC_PATH and the cleanup trap point at paths that no longer exist. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/test_task_analyzer.sh | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/test_task_analyzer.sh b/tools/perf/tests/shell/test_task_analyzer.sh index 0314412e63b4..6f729d9508f8 100755 --- a/tools/perf/tests/shell/test_task_analyzer.sh +++ b/tools/perf/tests/shell/test_task_analyzer.sh @@ -1,8 +1,13 @@ #!/bin/bash -# perf script task-analyzer tests (exclusive) +# perf script task-analyzer tests # SPDX-License-Identifier: GPL-2.0 +# Resolve the source directory before changing the working directory below, +# $0 may be a relative path and would no longer resolve from $tmpdir. +perfdir=$(cd "$(dirname "$0")/../.." && pwd) + tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) +cd "$tmpdir" || exit 1 # TODO: perf script report only supports input from the CWD perf.data file, make # it support input from any file. perfdata="perf.data" @@ -11,7 +16,6 @@ csvsummary="$tmpdir/csvsummary" err=0 # set PERF_EXEC_PATH to find scripts in the source directory -perfdir=$(dirname "$0")/../.. if [ -e "$perfdir/scripts/python/Perf-Trace-Util" ]; then export PERF_EXEC_PATH=$perfdir fi @@ -20,8 +24,7 @@ fi export ASAN_OPTIONS=detect_leaks=0 cleanup() { - rm -f "${perfdata}" - rm -f "${perfdata}".old + cd "$perfdir" || cd /tmp || exit rm -rf "$tmpdir" trap - exit term int -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 07/13] perf test common: Do not globally disable tracing events in clear_all_probes 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (5 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 06/13] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 08/13] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers ` (6 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers `echo 0 > /sys/kernel/debug/tracing/events/enable` disables tracepoint events system-wide. When running tests in parallel, this kills active tracing and recording sessions in concurrent tests (such as perf trace and perf record). Remove the global event disable from clear_all_probes(). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/common/init.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/perf/tests/shell/common/init.sh b/tools/perf/tests/shell/common/init.sh index cbfc78bec974..d2c7a31e2c6f 100644 --- a/tools/perf/tests/shell/common/init.sh +++ b/tools/perf/tests/shell/common/init.sh @@ -132,7 +132,6 @@ check_uprobes_available() clear_all_probes() { - echo 0 > /sys/kernel/debug/tracing/events/enable check_kprobes_available && echo > /sys/kernel/debug/tracing/kprobe_events check_uprobes_available && echo > /sys/kernel/debug/tracing/uprobe_events } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 08/13] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (6 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 07/13] perf test common: Do not globally disable tracing events in clear_all_probes Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 09/13] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers ` (5 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers The probe name `vfs_getname` was hardcoded, causing collisions when tests ran concurrently. Furthermore, `cleanup_probe_vfs_getname()` used `perf probe -d probe:vfs_getname*`, deleting probes registered by other parallel tests. Scope the probe name to the pid, and rename it to `getname_flags_$$` so that it no longer begins with "vfs_getname". perf trace calls evlist__add_vfs_getname(), which opens every event matching a hardcoded "probe:vfs_getname*" wildcard, so a perf trace run by any other test would otherwise pin this probe and make `perf probe -d` fail with -EBUSY. That also unblocks making the perf trace tests non-exclusive later in this series. Enumerate the probes to record and to delete from `perf probe -l`, matching `^probe:${vfs_getname}(_[[:digit:]]+)?$` exactly, rather than globbing on `${vfs_getname}*`. perf probe appends _1, _2, ... when getname_flags is inlined at more than one call site, so the variants do have to be matched, but since the name now ends in a pid a trailing wildcard would also match the probes of a test whose pid merely starts with this one's, e.g. 123 and 1234. Remove the `(exclusive)` tag from probe_vfs_getname.sh and record+script_probe_vfs_getname.sh so they run concurrently in pass 1. trace+probe_vfs_getname.sh has to stay exclusive: it is the one test that wants to be discovered by that wildcard, so it sets vfs_getname to a "vfs_getname_$$" name before sourcing the library, and would then pin its siblings' probes if it ran alongside them. A comment in the test records this. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/lib/probe_vfs_getname.sh | 34 ++++++++++++++++--- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+script_probe_vfs_getname.sh | 18 +++++++--- .../tests/shell/trace+probe_vfs_getname.sh | 9 +++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/tools/perf/tests/shell/lib/probe_vfs_getname.sh b/tools/perf/tests/shell/lib/probe_vfs_getname.sh index 88cd0e26d5f6..89a4b6fa5ea1 100644 --- a/tools/perf/tests/shell/lib/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/lib/probe_vfs_getname.sh @@ -1,12 +1,38 @@ #!/bin/bash # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 -perf probe -l 2>&1 | grep -q probe:vfs_getname +# The name of the getname_flags probe added and removed below. +# +# It is scoped to the pid so that tests running in parallel do not collide, +# and it deliberately does not start with "vfs_getname": perf trace calls +# evlist__add_vfs_getname(), which opens everything matching the hardcoded +# "probe:vfs_getname*" wildcard, so a perf trace running in another test would +# otherwise pin this probe and make the 'perf probe -d' below fail with -EBUSY. +# +# trace+probe_vfs_getname.sh is the one test that does want to be found that +# way, so it sets vfs_getname itself before sourcing this file, and is +# (exclusive) as a result. +: "${vfs_getname:=getname_flags_$$}" + +# Print the probes add_probe_vfs_getname() created. perf probe appends _1, _2, +# ... when getname_flags is inlined at more than one call site, so there can be +# several. Match them exactly rather than with a "${vfs_getname}*" glob: the +# name ends in a pid, so such a glob would also match the probes of a test +# whose pid merely starts with this one's, e.g. 123 and 1234. +probes_vfs_getname() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe:${vfs_getname}(_[[:digit:]]+)?$" +} + +[ -n "$(probes_vfs_getname)" ] had_vfs_getname=$? cleanup_probe_vfs_getname() { if [ $had_vfs_getname -eq 1 ] ; then - perf probe -q -d probe:vfs_getname* + local probe + for probe in $(probes_vfs_getname); do + perf probe -q -d "$probe" + done fi } @@ -33,8 +59,8 @@ add_probe_vfs_getname() { return 2 fi - perf probe -q "vfs_getname=getname_flags:${line} pathname=result->name:string" || \ - perf probe $add_probe_verbose "vfs_getname=getname_flags:${line} pathname=filename:ustring" || return 1 + perf probe -q "${vfs_getname}=getname_flags:${line} pathname=result->name:string" || \ + perf probe $add_probe_verbose "${vfs_getname}=getname_flags:${line} pathname=filename:ustring" || return 1 fi } diff --git a/tools/perf/tests/shell/probe_vfs_getname.sh b/tools/perf/tests/shell/probe_vfs_getname.sh index 5fe5682c28ce..05f1d50732b6 100755 --- a/tools/perf/tests/shell/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/probe_vfs_getname.sh @@ -1,6 +1,5 @@ #!/bin/bash -# Add vfs_getname probe to get syscall args filenames (exclusive) - +# Add vfs_getname probe to get syscall args filenames # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 diff --git a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh index 002f7037f182..1d4fb4a4fbfe 100755 --- a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh +++ b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Use vfs_getname probe to get syscall args filenames (exclusive) +# Use vfs_getname probe to get syscall args filenames # Uses the 'perf test shell' library to add probe:vfs_getname to the system # then use it with 'perf record' using 'touch' to write to a temp file, then @@ -17,22 +17,32 @@ skip_if_no_perf_probe || exit 2 # shellcheck source=lib/probe_vfs_getname.sh . "$(dirname "$0")/lib/probe_vfs_getname.sh" +# shellcheck disable=SC2154 # vfs_getname is assigned in lib/probe_vfs_getname.sh record_open_file() { echo "Recording open file:" # Check presence of libtraceevent support to run perf record - skip_no_probe_record_support "probe:vfs_getname*" + skip_no_probe_record_support if [ $? -eq 2 ]; then echo "WARN: Skipping test record_open_file. No libtraceevent support" return 2 fi - perf record -o ${perfdata} -e probe:vfs_getname\* touch $file + # Record every probe the inlining of getname_flags produced, naming + # them exactly rather than with a "${vfs_getname}*" glob, which would + # also match the probes of a test whose pid starts with this one's. + local events + events=$(probes_vfs_getname | paste -sd, -) + if [ -z "${events}" ] ; then + echo "FAIL: no ${vfs_getname} probe to record" + return 1 + fi + perf record -o ${perfdata} -e "${events}" touch $file } perf_script_filenames() { echo "Looking at perf.data file for vfs_getname records for the file we touched:" perf script -i ${perfdata} | \ - grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:vfs_getname[_0-9]*: +\([[:xdigit:]]+\) +pathname=\"${file}\"" + grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:${vfs_getname}(_[0-9]+)?: +\([[:xdigit:]]+\) +pathname=\"${file}\"" } add_probe_vfs_getname diff --git a/tools/perf/tests/shell/trace+probe_vfs_getname.sh b/tools/perf/tests/shell/trace+probe_vfs_getname.sh index 7a0b1145d0cd..146305f4d549 100755 --- a/tools/perf/tests/shell/trace+probe_vfs_getname.sh +++ b/tools/perf/tests/shell/trace+probe_vfs_getname.sh @@ -10,6 +10,13 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 +# This test must stay exclusive, and is the only one of the probe tests that +# does: it does not name the event it uses. perf trace discovers it with the +# hardcoded "probe:vfs_getname*" wildcard in evlist__add_vfs_getname(), so the +# probe has to carry that prefix, and a parallel run of this test would then +# also match, and pin, the probes of the other tests. The sibling tests avoid +# all of this by using a name that the wildcard cannot reach. + # shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh @@ -17,6 +24,8 @@ skip_if_no_perf_probe || exit 2 skip_if_no_perf_trace || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# shellcheck disable=SC2034 # consumed by lib/probe_vfs_getname.sh +vfs_getname="vfs_getname_$$" . "$(dirname $0)"/lib/probe_vfs_getname.sh trace_open_vfs_getname() { -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 09/13] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (7 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 08/13] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 10/13] perf test trace_summary: Improve error diagnostics Ian Rogers ` (4 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers The uprobe name was not scoped to PID, and concurrent writes to `/sys/kernel/debug/tracing/uprobe_events` can occasionally return `-EBUSY` when another process holds the tracefs inode lock. Scope the probe event name with `$$` (`inet_pton_$$=inet_pton`) and add a retry loop with backoff for uprobe addition. Drop the `(exclusive)` tag so the test can run in parallel during pass 1. A PID scoped probe is no longer cleaned up by any other test, so add an EXIT/TERM/INT trap to delete it, otherwise an interrupted run leaks the uprobe into the system. The trap is installed only after the root and IPv6 checks that `exit 2` to skip the test, as trap_cleanup() exits 1 and would otherwise turn those skips into failures. Deletion enumerates the probes from `perf probe -l`, matching `^probe_libc:inet_pton_$$(_[[:digit:]]+)?$` exactly, rather than reading $event_name: a signal arriving after perf probe injected the uprobe but before the assignment completed would leave that variable empty and leak the probe, and an `inet_pton_$$*` glob would reach the probe of a test whose pid merely starts with this one's. While here use mktemp rather than mktemp -u for the temporary files: this test runs as root in a world writable /tmp, and predicting a name without creating it allows another user to win the race and plant a symlink. The perf.data check becomes -s rather than -e as mktemp now pre-creates an empty file. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../shell/record+probe_libc_inet_pton.sh | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh index eca629ee83f0..3eb51426373b 100755 --- a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh +++ b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh @@ -1,5 +1,5 @@ #!/bin/bash -# probe libc's inet_pton & backtrace it with ping (exclusive) +# probe libc's inet_pton & backtrace it with ping # Installs a probe on libc's inet_pton function, that will use uprobes, # then use 'perf trace' on a ping to localhost asking for just one packet @@ -21,20 +21,30 @@ nm -Dg $libc 2>/dev/null | grep -F -q inet_pton || exit 254 event_pattern='probe_libc:inet_pton(_[[:digit:]]+)?' add_libc_inet_pton_event() { + local attempts=0 + while [ $attempts -lt 3 ]; do + event_name=$(perf probe -f -x $libc -a "inet_pton_$$=inet_pton" 2>&1 | \ + awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ + ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) + + if [ -n "$event_name" ]; then + return 0 + fi + attempts=$((attempts + 1)) + sleep 0.1 + done - event_name=$(perf probe -f -x $libc -a inet_pton 2>&1 | \ - awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ - ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) - - if [ $? -ne 0 ] || [ -z "$event_name" ] ; then - printf "FAIL: could not add event\n" - return 1 - fi + printf "FAIL: could not add event\n" + return 1 } trace_libc_inet_pton_backtrace() { - expected=`mktemp -u /tmp/expected.XXX` + # Create the files rather than just reserving names with mktemp -u: + # this runs as root and /tmp is world writable, so a predictable name + # that is written to later can be pre-created as a symlink by an + # unprivileged user and used to clobber an arbitrary file. + expected=$(mktemp /tmp/expected.XXX) echo "ping[][0-9 \.:]+$event_name: \([[:xdigit:]]+\)" > $expected echo ".*inet_pton\+0x[[:xdigit:]]+[[:space:]]\($libc|inlined\)$" >> $expected @@ -50,8 +60,8 @@ trace_libc_inet_pton_backtrace() { ;; esac - perf_data=`mktemp -u /tmp/perf.data.XXX` - perf_script=`mktemp -u /tmp/perf.script.XXX` + perf_data=$(mktemp /tmp/perf.data.XXX) + perf_script=$(mktemp /tmp/perf.script.XXX) # Check presence of libtraceevent support to run perf record skip_no_probe_record_support "$event_name/$eventattr/" @@ -61,9 +71,10 @@ trace_libc_inet_pton_backtrace() { fi perf record -e $event_name/$eventattr/ -o $perf_data ping -6 -c 1 ::1 > /dev/null 2>&1 - # check if perf data file got created in above step. - if [ ! -e $perf_data ]; then - printf "FAIL: perf record failed to create \"%s\" \n" "$perf_data" + # Check perf record actually wrote data. mktemp already created the + # file, so test that it is non-empty rather than that it exists. + if [ ! -s $perf_data ]; then + printf "FAIL: perf record failed to write \"%s\" \n" "$perf_data" return 1 fi perf script -i $perf_data | tac | grep -m1 ^ping -B9 | tac > $perf_script @@ -97,21 +108,53 @@ trace_libc_inet_pton_backtrace() { # even if the perf script output does not match. } +# Print the pid scoped uprobes this test may have created. perf probe appends +# _1, _2, ... when the name is already taken, so match those too, but anchor +# the match: an "inet_pton_$$*" glob would also match the probe of a test whose +# pid merely starts with this one's, e.g. 123 and 1234. +libc_inet_pton_events() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe_libc:inet_pton_$$(_[[:digit:]]+)?$" +} + delete_libc_inet_pton_event() { + # Ask the kernel what is actually there rather than trusting + # $event_name: a signal arriving after perf probe injected the uprobe + # but before the assignment to event_name completed would otherwise + # leave the variable empty and leak the probe. + local probe + for probe in $(libc_inet_pton_events); do + perf probe -q -d "$probe" + done +} - if [ -n "$event_name" ] ; then - perf probe -q -d $event_name - fi +cleanup() { + rm -f ${perf_data} ${perf_script} ${expected} + delete_libc_inet_pton_event + + trap - EXIT TERM INT +} + +trap_cleanup() { + cleanup + exit 1 } # Check for IPv6 interface existence ip a sh lo | grep -F -q inet6 || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# Install the trap only now that the skips above are out of the way: it exits +# 1, so arming it any earlier would turn an 'exit 2' skip into a failure. +# +# The event name is pid scoped, so unlike the old fixed name an orphan left +# behind by an interrupted run is never overwritten by a later run: it would +# stay in the kernel forever. Always clean up, including on a signal. +trap trap_cleanup EXIT TERM INT + skip_if_no_perf_probe && \ add_libc_inet_pton_event && \ trace_libc_inet_pton_backtrace err=$? -rm -f ${perf_data} ${perf_script} ${expected} -delete_libc_inet_pton_event +cleanup exit $err -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 10/13] perf test trace_summary: Improve error diagnostics 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (8 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 09/13] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 11/13] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers ` (3 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers When pattern matching fails in test_perf_trace(), print the command that failed along with the actual match count, the matching lines found, and the first 15 lines of output to aid debugging. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index b80dea77cec6..f975176247b5 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -28,10 +28,14 @@ test_perf_trace() { count=$(grep -E -c -m 3 "${search}" ${OUTPUT}) if [ "${count}" != "3" ]; then - echo "Error: cannot find enough pattern ${search} in the output" - cat ${OUTPUT} - rm -f ${OUTPUT} - exit 1 + echo "Error: cannot find enough pattern ${search} (count=${count}) in output of:" + echo "Error: perf trace ${args} -- ${workload}" + echo "Error: matched lines:" + grep -E "${search}" ${OUTPUT} || echo "none" + echo "Error: first 15 lines of output:" + head -n 15 ${OUTPUT} + rm -f ${OUTPUT} + exit 1 fi } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 11/13] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (9 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 10/13] perf test trace_summary: Improve error diagnostics Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 12/13] perf test trace_summary: Make non-exclusive Ian Rogers ` (2 subsequent siblings) 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers trace_btf_general.sh used `--max-events=1` with `perf trace` on commands such as `mv`, `echo`, and `sleep`. When background activity occurs or tests run in parallel, `perf trace` can capture an event from an unrelated process and exit prematurely before recording the target command's syscalls. Drop `--max-events=1` and let tracing run until the command completes, checking for the expected output with grep (matching trace_btf_enum.sh). Remove the (exclusive) tag so the test runs in parallel. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_btf_general.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_btf_general.sh b/tools/perf/tests/shell/trace_btf_general.sh index 7a94a5743924..4d654b687a4e 100755 --- a/tools/perf/tests/shell/trace_btf_general.sh +++ b/tools/perf/tests/shell/trace_btf_general.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace BTF general tests (exclusive) +# perf trace BTF general tests # SPDX-License-Identifier: GPL-2.0 err=0 @@ -27,7 +27,7 @@ check_vmlinux() { trace_test_string() { echo "Testing perf trace's string augmentation" - output="$(perf trace --sort-events -e renameat* --max-events=1 -- mv ${file1} ${file2} 2>&1)" + output="$(perf trace --sort-events -e renameat* -- mv ${file1} ${file2} 2>&1)" if ! echo "$output" | grep -q -E "^mv/[0-9]+ renameat(2)?\(.*, \"${file1}\", .*, \"${file2}\", .*\) += +[0-9]+$" then printf "String augmentation test failed, output:\n$output\n" @@ -38,7 +38,7 @@ trace_test_string() { trace_test_buffer() { echo "Testing perf trace's buffer augmentation" # echo will insert a newline (\10) at the end of the buffer - output="$(perf trace --sort-events -e write --max-events=1 -- echo "${buffer}" 2>&1)" + output="$(perf trace --sort-events -e write -- echo "${buffer}" 2>&1)" if ! echo "$output" | grep -q -E "^echo/[0-9]+ write\([0-9]+, ${buffer}.*, [0-9]+\) += +[0-9]+$" then printf "Buffer augmentation test failed, output:\n$output\n" @@ -48,7 +48,7 @@ trace_test_buffer() { trace_test_struct_btf() { echo "Testing perf trace's struct augmentation" - output="$(perf trace --sort-events -e clock_nanosleep --force-btf --max-events=1 -- sleep 1 2>&1)" + output="$(perf trace --sort-events -e clock_nanosleep --force-btf -- sleep 1 2>&1)" if ! echo "$output" | grep -q -E "^sleep/[0-9]+ clock_nanosleep\(0, 0, \{1,.*\}, 0x[0-9a-f]+\) += +[0-9]+$" then printf "BTF struct augmentation test failed, output:\n$output\n" -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 12/13] perf test trace_summary: Make non-exclusive 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (10 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 11/13] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 6:42 ` [PATCH v1 13/13] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers trace_summary.sh tests various summary modes of `perf trace`. It already directs output to a unique temporary file without polluting the current working directory. Remove the (exclusive) tag so it can run concurrently in parallel test runs. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index f975176247b5..e2834bc4eee6 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace summary (exclusive) +# perf trace summary # SPDX-License-Identifier: GPL-2.0 # Check that perf trace works with various summary mode -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v1 13/13] perf test uprobe_from_different_cu: Scope probe name to PID 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (11 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 12/13] perf test trace_summary: Make non-exclusive Ian Rogers @ 2026-09-17 6:42 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 13 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 6:42 UTC (permalink / raw) To: Arnaldo Carvalho de Melo, Namhyung Kim Cc: Peter Zijlstra, Ingo Molnar, Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users, linux-kernel, Ian Rogers The test builds a binary in a per-run temporary directory and probes its foo function. The directory name is unique, but perf probe derives the event name from the probed function and the group name from the binary's basename, so every run registers the same probe_testfile:foo event. Running the test concurrently with itself, as 'perf test -r3' does, therefore fails in all but one of the runs with: Error: event "foo" already exists. Hint: Remove existing event by 'perf probe -d' and a losing run's cleanup goes on to delete the winning run's probe out from under it. Name the event after the pid, foo_$$, so that parallel runs no longer collide. This lets the test stay in the parallel pass rather than having to be marked (exclusive). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/test_uprobe_from_different_cu.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh index 7adf9755d6de..47c99d93436b 100755 --- a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh +++ b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh @@ -18,12 +18,19 @@ fi temp_dir=$(mktemp -d /tmp/perf-uprobe-different-cu-sh.XXXXXXXXXX) +# The name of the uprobe added and removed below. The probe is placed on +# ${temp_dir}/testfile, but perf probe derives the event name from the probed +# function and the group name from the binary's basename, so every run would +# otherwise share one probe_testfile:foo event, and a concurrent run would +# fail with 'event "foo" already exists'. Scope the event name to the pid. +probe_name="foo_$$" + cleanup() { trap - EXIT TERM INT if [[ "${temp_dir}" =~ ^/tmp/perf-uprobe-different-cu-sh.*$ ]]; then echo "--- Cleaning up ---" - perf probe -x ${temp_dir}/testfile -d foo || true + perf probe -x ${temp_dir}/testfile -d ${probe_name} || true rm -f "${temp_dir}/"* rmdir "${temp_dir}" fi @@ -84,6 +91,6 @@ gcc -g -Og -c ${temp_dir}/testfile-main.c -o ${temp_dir}/testfile-main.o gcc -g -Og -o ${temp_dir}/testfile ${temp_dir}/testfile-foo.o ${temp_dir}/testfile-main.o perf probe -x ${temp_dir}/testfile --funcs foo | grep "foo" -perf probe -x ${temp_dir}/testfile foo +perf probe -x ${temp_dir}/testfile ${probe_name}=foo cleanup -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (12 preceding siblings ...) 2026-09-17 6:42 ` [PATCH v1 13/13] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 01/14] perf trace: Include the headers declaring pid_t and strcmp Ian Rogers ` (14 more replies) 13 siblings, 15 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz perf trace's BPF augmentation attaches to raw_syscalls:sys_enter and raw_syscalls:sys_exit system wide, and used the program return value to decide whether a syscall was interesting. Returning 0 from a BPF_PROG_TYPE_TRACEPOINT program makes perf_trace_run_bpf_submit() drop the event for every listener on that tracepoint, not just for the perf trace that installed the program. Any concurrent perf trace, perf record or ftrace session watching raw_syscalls therefore lost events, which is one of the reasons so many of the perf trace and perf probe shell tests had to be marked (exclusive) and run on their own. Patch 1 is an independent build fix for the two files the rest of the series goes on to rework. Patches 2 to 6 fix perf trace. They stop the return value being used as a filter and do the filtering in BPF maps instead, fix argument handling for the __data_loc internal tracepoint fields that syscalls:sys_enter_* gained in 6.19, stop the sys_exit program array tail calling a sys_enter augmenter, and replace the userspace PERF_RECORD_FORK/PERF_RECORD_EXIT bookkeeping with BTF-typed raw tracepoint programs on sched_process_{fork,exit,exec}. A task is then registered before its first syscall and evicted in do_exit(), rather than whenever userspace next drains the ring buffer. Patches 7 to 14 deal with the tests. Several collided with each other through global state rather than through perf trace: fixed probe names, clear_all_probes() disabling tracing events globally, and perf trace's hardcoded "probe:vfs_getname*" wildcard pinning probes belonging to other tests. With those scoped to a pid they can drop (exclusive) and run in parallel again. Tested on x86_64. The trace and probe tests pass under 'perf test -r3', which runs the repeats concurrently. Every patch builds individually, and the series also builds with BUILD_BPF_SKEL=0. Changes since v1: - New patch 1 includes <sys/types.h> and <string.h> for the pid_t and strcmp() uses that were relying on the include chain happening to drag them in, which does not hold on libcs such as musl. - Patch 5 no longer returns success when the event qualifier filter string fails to allocate. err now defaults to 0 because either tracepoint may legitimately be absent, so the ENOMEM path has to set the error itself rather than rely on that default. It also includes <stdbool.h> for the bool parameters it adds to trace_augment.h. - Patch 7 removes the temporary directory if the cd into it fails. That happens before the cleanup trap is installed, so the directory would otherwise be left behind in /tmp. Ian Rogers (14): perf trace: Include the headers declaring pid_t and strcmp perf trace: Start BPF summary before starting workload perf trace: Skip internal tracepoint fields in formatting and beauty map perf trace: Do not set unaugmented BPF program on sys_exit map perf trace: Filter events in BPF and avoid tracepoint vetoes perf trace: Handle fork and exit directly in BPF filter maps perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive perf test common: Do not globally disable tracing events in clear_all_probes perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive perf test trace_summary: Improve error diagnostics perf test trace_btf_general: Drop --max-events=1 and make non-exclusive perf test trace_summary: Make non-exclusive perf test uprobe_from_different_cu: Scope probe name to PID tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 335 ++++++++++++++---- tools/perf/tests/shell/common/init.sh | 1 - .../perf/tests/shell/lib/probe_vfs_getname.sh | 34 +- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+probe_libc_inet_pton.sh | 83 +++-- .../shell/record+script_probe_vfs_getname.sh | 18 +- tools/perf/tests/shell/test_task_analyzer.sh | 16 +- .../shell/test_uprobe_from_different_cu.sh | 11 +- .../tests/shell/trace+probe_vfs_getname.sh | 9 + tools/perf/tests/shell/trace_btf_general.sh | 8 +- tools/perf/tests/shell/trace_summary.sh | 14 +- .../bpf_skel/augmented_raw_syscalls.bpf.c | 291 ++++++++++++++- tools/perf/util/bpf_trace_augment.c | 174 ++++++++- tools/perf/util/trace_augment.h | 27 +- 15 files changed, 888 insertions(+), 141 deletions(-) base-commit: 91b0782fc9e9d2f0a40b5256146e014802fdbb36 -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 01/14] perf trace: Include the headers declaring pid_t and strcmp 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 02/14] perf trace: Start BPF summary before starting workload Ian Rogers ` (13 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_augment.h uses pid_t in the augmented_syscalls__set_filter_pids() prototype and in the !HAVE_BPF_SKEL stub, and bpf_trace_augment.c calls strcmp() in augmented_syscalls__find_by_title(), but neither pulls in the header that declares what it uses. Both happen to build today only because something else in the include chain drags <sys/types.h> and <string.h> in first, which is not guaranteed and does not hold on libcs such as musl that keep the POSIX namespaces strictly separated. Include <sys/types.h> and <string.h> explicitly. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/util/bpf_trace_augment.c | 1 + tools/perf/util/trace_augment.h | 1 + 2 files changed, 2 insertions(+) diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index a9cf2a77ded1..645819d79b6d 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,6 @@ #include <bpf/libbpf.h> #include <internal/xyarray.h> +#include <string.h> #include "bpf_skel/augmented_raw_syscalls.skel.h" #include "debug.h" diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 4f729bc67753..a1cd9a5e0213 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <sys/types.h> struct bpf_program; struct evlist; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 02/14] perf trace: Start BPF summary before starting workload 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-17 16:38 ` [PATCH v2 01/14] perf trace: Include the headers declaring pid_t and strcmp Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 03/14] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers ` (12 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When using --bpf-summary, trace_start_bpf_summary() sets skel->bss->enabled = 1. In trace__run(), trace_start_bpf_summary() was previously invoked after evlist__start_workload(). Because evlist__start_workload() immediately unblocks the child process by writing to its go_pipe, short-lived workloads (such as `cat /dev/null`) can execute and complete their initial system calls before trace_start_bpf_summary() is reached by the parent process. Furthermore, under high system load, the child process may finish before the BPF summary tracking is enabled in the kernel at all, causing syscall summary tests to fail. Additionally, if initial_delay was configured, the workload was started before sleeping. Move trace_start_bpf_summary() to be invoked before evlist__start_workload(), matching evlist__enable(), and ensure it respects target.initial_delay. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 20fffc24507b..8da0c51ec380 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4838,17 +4838,19 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (!target__none(&trace->opts.target) && !trace->opts.target.initial_delay) evlist__enable(evlist); + if (trace->summary_bpf && !trace->opts.target.initial_delay) + trace_start_bpf_summary(); + if (forks) evlist__start_workload(evlist); if (trace->opts.target.initial_delay) { usleep(trace->opts.target.initial_delay * 1000); evlist__enable(evlist); + if (trace->summary_bpf) + trace_start_bpf_summary(); } - if (trace->summary_bpf) - trace_start_bpf_summary(); - trace->multiple_threads = perf_thread_map__pid(evlist__core(evlist)->threads, 0) == -1 || perf_thread_map__nr(evlist__core(evlist)->threads) > 1 || evlist__first(evlist)->core.attr.inherit; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 03/14] perf trace: Skip internal tracepoint fields in formatting and beauty map 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-17 16:38 ` [PATCH v2 01/14] perf trace: Include the headers declaring pid_t and strcmp Ian Rogers 2026-09-17 16:38 ` [PATCH v2 02/14] perf trace: Start BPF summary before starting workload Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 04/14] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers ` (11 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Linux 6.19+ added __data_loc char[] internal fields for string arguments in syscalls:sys_enter_<name> tracepoints (e.g., __data_loc_oldname in sys_enter_renameat2). While is_internal_field() was added to detect them, several places did not properly account for them: 1. In syscall_arg_fmt__init_array(), when an internal field was skipped, the arg pointer was still incremented, causing the subsequent argument formatters to be mismatched. 2. In syscall__scnprintf_args(), internal fields were not skipped, causing spurious trailing arguments like ", 0, 16" to be formatted and printed. 3. In trace__bpf_sys_enter_beauty_map(), internal fields were not skipped, offsetting beauty array argument indices and breaking string and buffer augmentation. 4. In trace__find_usable_bpf_prog_entry(), candidate pointer checks matched on internal pointer fields, breaking signature compatibility matching between syscalls for augmenter sharing. Introduce next_user_arg() and advance both cursors with it, so that the two argument lists are always compared at a real argument and the walk ends when one syscall runs out of arguments rather than when one happens to have trailing internal fields. 5. syscall__augmented_args() computed the augmented payload as sample->raw_size - sc->args_size for any sys_enter style sample. sc->args_size deliberately stops at the last non-internal field, so on 6.19+ a native syscalls:sys_enter_<name> record leaves the __data_loc words and their string payloads in the remainder. Those bytes are not a struct augmented_arg, so syscall_arg__scnprintf_augmented_string() read a bogus length and walked arg->augmented.args out of bounds. This is reachable from trace__event_handler(), which calls trace__fprintf_sys_enter() for any evsel whose tracepoint name starts with "sys_enter_". 6. In syscall__read_info(), syscall__alloc_arg_fmts() was called before checking and dropping the leading __syscall_nr (or nr) field, using nr_fields - 1 unconditionally. If a tracepoint format lacks that leading field, the allocated arg_fmt array is one entry too small and syscall_arg_fmt__init_array() writes one entry past the end of the heap buffer. Drop __syscall_nr/nr first and size the allocation from the remaining fields. Update these functions to check and skip is_internal_field() so that arguments are correctly formatted and beauty map entries match the expected syscall signatures, restrict syscall__augmented_args() to the __augmented_syscalls__ bpf-output evsel, and size arg_fmt after dropping the syscall number field. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 153 ++++++++++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 38 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 8da0c51ec380..af9696aadaec 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2277,15 +2277,20 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field struct tep_format_field *last_field = NULL; int len; - for (; field; field = field->next, ++arg) { - /* assume it's the last argument */ + for (; field; field = field->next) { + /* + * Skip internal tracepoint fields (e.g., __data_loc strings in + * Linux 6.19+) so they do not advance the syscall arg array index. + */ if (is_internal_field(field)) continue; last_field = field; - if (arg->scnprintf) + if (arg->scnprintf) { + ++arg; continue; + } len = strlen(field->name); @@ -2342,6 +2347,7 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field } } } + ++arg; } return last_field; @@ -2363,6 +2369,7 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) char tp_name[128]; const char *name; struct tep_format_field *field; + int nr_args; int err; if (sc->nonexistent) @@ -2401,24 +2408,25 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) return err; } - /* - * The tracepoint format contains __syscall_nr field, so it's one more - * than the actual number of syscall arguments. - */ - if (syscall__alloc_arg_fmts(sc, sc->tp_format->format.nr_fields - 1)) - return -ENOMEM; - sc->args = sc->tp_format->format.fields; + nr_args = sc->tp_format->format.nr_fields; /* * We need to check and discard the first variable '__syscall_nr' * or 'nr' that mean the syscall number. It is needless here. * So drop '__syscall_nr' or 'nr' field but does not exist on older kernels. + * + * Do this before allocating, and size the array from what is left, so + * that a format without the field does not leave + * syscall_arg_fmt__init_array() walking one entry past the end. */ if (sc->args && (!strcmp(sc->args->name, "__syscall_nr") || !strcmp(sc->args->name, "nr"))) { sc->args = sc->args->next; - --sc->nr_args; + --nr_args; } + if (syscall__alloc_arg_fmts(sc, nr_args)) + return -ENOMEM; + field = sc->args; while (field) { if (is_internal_field(field)) @@ -2636,11 +2644,17 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, if (sc->args != NULL) { struct tep_format_field *field; - for (field = sc->args; field; - field = field->next, ++arg.idx, bit <<= 1) { - if (arg.mask & bit) + for (field = sc->args; field; field = field->next) { + /* Skip internal fields so they are not printed as spurious arguments */ + if (is_internal_field(field)) continue; + if (arg.mask & bit) { + ++arg.idx; + bit <<= 1; + continue; + } + arg.fmt = &sc->arg_fmt[arg.idx]; val = syscall_arg__val(&arg, arg.idx); /* @@ -2658,8 +2672,11 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, */ if (val == 0 && !trace->show_zeros && !(sc->arg_fmt && sc->arg_fmt[arg.idx].show_zero) && - !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) + !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) { + ++arg.idx; + bit <<= 1; continue; + } printed += scnprintf(bf + printed, size - printed, "%s", printed ? ", " : ""); @@ -2674,12 +2691,16 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, size - printed, val, field->type); if (btf_printed) { printed += btf_printed; + ++arg.idx; + bit <<= 1; continue; } } printed += syscall_arg_fmt__scnprintf_val(&sc->arg_fmt[arg.idx], bf + printed, size - printed, &arg, val); + ++arg.idx; + bit <<= 1; } } else if (IS_ERR(sc->tp_format)) { /* @@ -2940,7 +2961,9 @@ static int trace__fprintf_sample(struct trace *trace, struct perf_sample *sample return printed; } -static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sample, int *augmented_args_size, int raw_augmented_args_size) +static void *syscall__augmented_args(struct trace *trace, struct syscall *sc, + struct perf_sample *sample, + int *augmented_args_size, int raw_augmented_args_size) { /* * For now with BPF raw_augmented we hook into raw_syscalls:sys_enter @@ -2958,6 +2981,24 @@ static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sam */ int args_size = raw_augmented_args_size ?: sc->args_size; + /* + * Augmented arguments are a perf trace specific payload, they are only + * ever appended to samples emitted by the BPF __augmented_syscalls__ + * bpf-output event. + * + * Native syscalls:sys_enter_NAME tracepoints may also carry trailing + * data of their own: since Linux 6.19 they append __data_loc char[] + * fields plus the string payloads they point at. Those bytes are not a + * struct augmented_arg, so treating them as one would make + * syscall_arg__scnprintf_augmented_string() read a bogus length and + * walk arg->augmented.args far out of bounds. + * + * So only look for augmented arguments on the event that can actually + * produce them. + */ + if (sample->evsel != trace->syscalls.events.bpf_output) + return NULL; + *augmented_args_size = sample->raw_size - args_size; if (*augmented_args_size > 0) { static uintptr_t argbuf[1024]; /* assuming single-threaded */ @@ -3016,17 +3057,13 @@ static int trace__sys_enter(struct trace *trace, if (!(trace->duration_filter || trace->summary_only || trace->min_stack)) trace__printf_interrupted_entry(trace); /* - * If this is raw_syscalls.sys_enter, then it always comes with the 6 possible - * arguments, even if the syscall being handled, say "openat", uses only 4 arguments - * this breaks syscall__augmented_args() check for augmented args, as we calculate - * syscall->args_size using each syscalls:sys_enter_NAME tracefs format file, - * so when handling, say the openat syscall, we end up getting 6 args for the - * raw_syscalls:sys_enter event, when we expected just 4, we end up mistakenly - * thinking that the extra 2 u64 args are the augmented filename, so just check - * here and avoid using augmented syscalls when the evsel is the raw_syscalls one. + * syscall__augmented_args() only returns a payload for the BPF + * __augmented_syscalls__ event, so raw_syscalls:sys_enter (which always + * carries all 6 possible arguments rather than sc->args_size worth) and + * the native syscalls:sys_enter_NAME tracepoints are both handled there. */ - if (evsel != trace->syscalls.events.sys_enter) - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); ttrace->entry_time = sample->time; ttrace->entry_cpu = sample->cpu; msg = ttrace->entry_str; @@ -3071,7 +3108,7 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam struct syscall *sc; char msg[1024]; void *args, *augmented_args = NULL; - int augmented_args_size, e_machine; + int augmented_args_size = 0, e_machine; size_t printed = 0; @@ -3089,7 +3126,8 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam goto out_put; args = perf_evsel__sc_tp_ptr(args, sample); - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); printed += syscall__scnprintf_args(sc, msg, sizeof(msg), args, augmented_args, augmented_args_size, trace, thread); fprintf(trace->output, "%.*s", (int)printed, msg); err = 0; @@ -4121,10 +4159,16 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i if (trace->btf == NULL) return -1; - for (i = 0, field = sc->args; field; ++i, field = field->next) { + for (i = 0, field = sc->args; field; field = field->next) { + /* Skip internal fields to keep beauty array index aligned with syscall arguments */ + if (is_internal_field(field)) + continue; + // XXX We're only collecting pointer payloads _from_ user space - if (!sc->arg_fmt[i].from_user) + if (!sc->arg_fmt[i].from_user) { + ++i; continue; + } struct_offset = strstr(field->type, "struct "); if (struct_offset == NULL) @@ -4143,8 +4187,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i name[cnt] = '\0'; /* cache struct's btf_type and type_id */ - if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) + if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) { + ++i; continue; + } bt = sc->arg_fmt[i].type; beauty_array[i] = bt->size; @@ -4170,7 +4216,9 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i struct tep_format_field *field_tmp; /* find the size of the buffer that appears in pairs with buf */ - for (j = 0, field_tmp = sc->args; field_tmp; ++j, field_tmp = field_tmp->next) { + for (j = 0, field_tmp = sc->args; field_tmp; field_tmp = field_tmp->next) { + if (is_internal_field(field_tmp)) + continue; if (!(field_tmp->flags & TEP_FIELD_IS_POINTER) && /* only integers */ (strstr(field_tmp->name, "count") || strstr(field_tmp->name, "siz") || /* size, bufsiz */ @@ -4180,8 +4228,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i can_augment = true; break; } + ++j; } } + ++i; } if (can_augment) @@ -4190,6 +4240,19 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i return -1; } +/* + * Advance to the first field that is a real syscall argument, so that callers + * walking two argument lists in step never have to reason about internal + * fields appearing in one list but not the other. + */ +static struct tep_format_field *next_user_arg(struct tep_format_field *field) +{ + while (field && is_internal_field(field)) + field = field->next; + + return field; +} + static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace, struct syscall *sc) { @@ -4197,7 +4260,7 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace /* * We're only interested in syscalls that have a pointer: */ - for (field = sc->args; field; field = field->next) { + for (field = next_user_arg(sc->args); field; field = next_user_arg(field->next)) { if (field->flags & TEP_FIELD_IS_POINTER) goto try_to_find_pair; } @@ -4215,21 +4278,31 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace pair->bpf_prog.sys_enter == unaugmented_prog) continue; - for (field = sc->args, candidate_field = pair->args; - field && candidate_field; field = field->next, candidate_field = candidate_field->next) { + /* + * Both cursors only ever point at real arguments, so the loop + * ends when one of the two syscalls runs out of them, rather + * than when one happens to have trailing internal fields. + */ + field = next_user_arg(sc->args); + candidate_field = next_user_arg(pair->args); + while (field && candidate_field) { bool is_pointer = field->flags & TEP_FIELD_IS_POINTER, candidate_is_pointer = candidate_field->flags & TEP_FIELD_IS_POINTER; if (is_pointer) { - if (!candidate_is_pointer) { + if (!candidate_is_pointer) { // The candidate just doesn't copies our pointer arg, might copy other pointers we want. + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; - } + } } else { if (candidate_is_pointer) { // The candidate might copy a pointer we don't have, skip it. goto next_candidate; } + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; } @@ -4250,6 +4323,8 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace goto next_candidate; is_candidate = true; + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); } if (!is_candidate) @@ -4261,7 +4336,9 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace * more than what is common to the two syscalls. */ if (candidate_field) { - for (candidate_field = candidate_field->next; candidate_field; candidate_field = candidate_field->next) + candidate_field = next_user_arg(candidate_field->next); + for (; candidate_field; + candidate_field = next_user_arg(candidate_field->next)) if (candidate_field->flags & TEP_FIELD_IS_POINTER) goto next_candidate; } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 04/14] perf trace: Do not set unaugmented BPF program on sys_exit map 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (2 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 03/14] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 05/14] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers ` (10 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz In trace__init_syscalls_bpf_prog_array_maps(), the BPF program array map for sys_exit (syscalls_sys_exit) was populated with the result of trace__bpf_prog_sys_exit_fd(). When a syscall had no specific exit augmenter, trace__find_syscall_bpf_prog() fell back to unaugmented_prog (syscall_unaugmented). However, syscall_unaugmented is a sys_enter program that outputs enter arguments to __augmented_syscalls__. As a consequence, when an unaugmented syscall exited, sys_exit tail-called syscall_unaugmented, which interpreted the exit arguments as enter arguments and emitted a duplicate, corrupt sys_enter event into __augmented_syscalls__ right as the syscall completed. Fix this by: 1. Returning NULL from trace__find_syscall_bpf_prog() when looking up exit augmenters and none is found. 2. Returning -1 from trace__bpf_prog_sys_exit_fd() when no exit program is present. 3. Only updating map_exit_fd when prog_fd >= 0. 4. Clearing err = 0 when trace__bpf_sys_enter_beauty_map() returns non-zero (indicating the syscall has no augmentable pointer arguments) before continuing the loop, so a trailing run of such syscalls (e.g. 'perf trace -e close') does not leave err non-zero on return and abort the session. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index af9696aadaec..a68d34256996 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4117,7 +4117,12 @@ static struct bpf_program *trace__find_syscall_bpf_prog(struct trace *trace __ma pr_debug("Couldn't find BPF prog \"%s\" to associate with syscalls:sys_%s_%s, not augmenting it\n", prog_name, type, sc->name); out_unaugmented: - return unaugmented_prog; + /* + * Do not set unaugmented_prog for exit: syscall_unaugmented is a + * sys_enter program that outputs enter arguments. Exit without a + * specialized return augmenter returns 1 directly from sys_exit. + */ + return !strcmp(type, "exit") ? NULL : unaugmented_prog; } static void trace__init_syscall_bpf_progs(struct trace *trace, int e_machine, int id) @@ -4140,7 +4145,7 @@ static int trace__bpf_prog_sys_enter_fd(struct trace *trace, int e_machine, int static int trace__bpf_prog_sys_exit_fd(struct trace *trace, int e_machine, int id) { struct syscall *sc = trace__syscall_info(trace, NULL, e_machine, id); - return sc ? bpf_program__fd(sc->bpf_prog.sys_exit) : bpf_program__fd(unaugmented_prog); + return sc && sc->bpf_prog.sys_exit ? bpf_program__fd(sc->bpf_prog.sys_exit) : -1; } static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, int key, unsigned int *beauty_array) @@ -4395,16 +4400,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace, int e_m err = bpf_map_update_elem(map_enter_fd, &key, &prog_fd, BPF_ANY); if (err) break; + /* Only update the exit prog array map if an exit augmenter exists */ prog_fd = trace__bpf_prog_sys_exit_fd(trace, e_machine, key); - err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); - if (err) - break; + if (prog_fd >= 0) { + err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); + if (err) + break; + } /* use beauty_map to tell BPF how many bytes to collect, set beauty_map's value here */ memset(beauty_array, 0, sizeof(beauty_array)); err = trace__bpf_sys_enter_beauty_map(trace, e_machine, key, (unsigned int *)beauty_array); - if (err) + if (err) { + /* + * Not a failure: the syscall just has no augmentable + * arguments. Clear err, or a trailing run of such + * syscalls, e.g. all of them for 'perf trace -e close', + * would leave it set on return and abort the session. + */ + err = 0; continue; + } err = bpf_map_update_elem(beauty_map_fd, &key, beauty_array, BPF_ANY); if (err) break; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 05/14] perf trace: Filter events in BPF and avoid tracepoint vetoes 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (3 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 04/14] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 06/14] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers ` (9 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The BPF augmented_raw_syscalls sys_enter and sys_exit programs returned 0 for syscalls that were not of interest. Returning 0 from a tracepoint BPF program vetoes the event for the whole system, so an unrelated concurrent perf trace, perf record or ftrace session listening to raw_syscalls would silently lose events. This is a cross-session side effect and shows up as flaky failures when perf tests run in parallel. Furthermore, syscall_unaugmented previously returned 1 without writing anything to the __augmented_syscalls__ ring buffer. This forced userspace perf trace to listen to both raw_syscalls:sys_enter and __augmented_syscalls__ in its evlist, requiring userspace event deduplication. Address these issues: 1. In augmented_raw_syscalls.bpf.c, never return 0 from tracepoint handlers: return 1 so non-traced syscalls pass through without vetoing other concurrent listeners. 2. Introduce pids_to_trace and syscalls_to_trace BPF hash maps to perform targeted filtering directly in BPF. Unselected syscalls or PIDs return 1 immediately without writing to the buffer. 3. In syscall_unaugmented, output the unaugmented enter payload into __augmented_syscalls__ and return 1. Change its section from SEC("tp/raw_syscalls/sys_enter") to SEC("tp/syscalls/sys_enter_unaugmented") so libbpf does not attempt to auto-attach it to raw_syscalls:sys_enter. 4. In bpf_trace_augment.c, add helpers to configure target PIDs and syscalls in the BPF maps, setting the activation flags (has_pids_to_trace, has_syscalls_to_trace) only after the maps are fully populated so already-attached BPF programs do not filter against a half-filled map. Explicitly attach only sys_enter and sys_exit via an attach_prog() helper that saves -errno before calling pr_debug() or bpf_link__destroy(). Destroy the skeleton on every failure path. Leaving a loaded but unusable skeleton behind is not inert: the setters called later from trace__run() would program its maps, and a partial attach would leave a BPF program live on raw_syscalls for a session that never starts. Since augmented_syscalls__{prepare,create_bpf_output}() failures fall back to unaugmented tracing rather than aborting, those setters have to become no-ops, which they only do once skel is NULL again. errno is used directly here, so include <errno.h> rather than relying on it arriving via another header, which it does not under musl. 5. In builtin-trace.c, hook trace__set_ev_qualifier_filter() and PID filtering into the BPF maps. When __augmented_syscalls__ is active, remove raw_syscalls:sys_enter from trace.evlist since all traced enter events (both augmented and unaugmented) are now emitted by BPF into __augmented_syscalls__. Restore tracking on the remaining evsel via evlist__set_tracking_event() so PERF_RECORD_COMM and tracking events continue to be recorded. Errors from augmented_syscalls__set_target_syscalls() are reported and propagated, the tracepoint filter string is freed on every exit path, and an allocation failure in trace__set_filter_pids() now returns -ENOMEM instead of being silently ignored. Note that in trace__set_filter_pids() the target pids and the filtered pids are two independent axes and both have to be programmed. Naming pids to leave out with --filter-pids does not widen -p/-t or a workload to the whole system, and a BPF tracepoint program is attached system wide rather than to the target's file descriptors, so pids_to_trace is the only thing keeping other tasks out. 6. Add --syscall-augment option (defaulting to true) to allow users to explicitly use --no-syscall-augment to run perf trace in the classic unaugmented tracepoint mode without BPF. When BPF is unavailable or disabled, ensure the non-augmented tracepoint path cleanly configures sys_enter and sys_exit without duplicate entries. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 151 +++++++++++++++--- .../bpf_skel/augmented_raw_syscalls.bpf.c | 133 +++++++++++++-- tools/perf/util/bpf_trace_augment.c | 135 +++++++++++++++- tools/perf/util/trace_augment.h | 34 ++++ 5 files changed, 420 insertions(+), 38 deletions(-) diff --git a/tools/perf/Documentation/perf-trace.txt b/tools/perf/Documentation/perf-trace.txt index d20b43ea3d37..4680c69160d7 100644 --- a/tools/perf/Documentation/perf-trace.txt +++ b/tools/perf/Documentation/perf-trace.txt @@ -260,6 +260,11 @@ the thread executes on the designated CPUs. Default is to monitor all CPUs. Maximum number of lines in the summary mode. Note that this applies to each entry (thread or cgroup). +--syscall-augment:: + Augment syscalls with BPF. Enabled by default when BPF support is available. + Use --no-syscall-augment to disable BPF augmentation and fall back to the + unaugmented tracepoint approach. + PAGEFAULTS ---------- diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index a68d34256996..9bb8316e334c 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -200,6 +200,7 @@ struct trace { int max_summary; int raw_augmented_syscalls_args_size; bool raw_augmented_syscalls; + bool syscall_augment; bool fd_path_disabled; bool sort_events; bool not_ev_qualifier; @@ -2054,6 +2055,23 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; + case PERF_RECORD_FORK: + if (trace->raw_augmented_syscalls && + (augmented_syscalls__has_target_pid(event->fork.ppid) || + augmented_syscalls__has_target_pid(event->fork.ptid))) { + augmented_syscalls__add_target_pid(event->fork.pid); + } + ret = machine__process_fork_event(machine, event, sample); + break; + case PERF_RECORD_EXIT: + if (trace->raw_augmented_syscalls) { + if (event->fork.pid == event->fork.tid) + augmented_syscalls__del_target_pid(event->fork.pid); + else + augmented_syscalls__del_target_pid(event->fork.tid); + } + ret = machine__process_exit_event(machine, event, sample); + break; default: ret = machine__process_event(machine, event, sample); break; @@ -4043,7 +4061,7 @@ static int trace__add_syscall_newtp(struct trace *trace) static int trace__set_ev_qualifier_tp_filter(struct trace *trace) { - int err = -1; + int err = 0; struct evsel *sys_exit; char *filter = asprintf_expr_inout_ints("id", !trace->not_ev_qualifier, trace->ev_qualifier_ids.nr, @@ -4052,15 +4070,27 @@ static int trace__set_ev_qualifier_tp_filter(struct trace *trace) if (filter == NULL) goto out_enomem; - if (!evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter)) { - sys_exit = trace->syscalls.events.sys_exit; + /* + * With BPF augmentation sys_enter is filtered in BPF and removed from + * the evlist, so only apply the tracepoint filter to the events that + * are actually present. + */ + if (trace->syscalls.events.sys_enter) + err = evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter); + + sys_exit = trace->syscalls.events.sys_exit; + if (!err && sys_exit) err = evsel__append_tp_filter(sys_exit, filter); - } free(filter); out: return err; out_enomem: + /* + * err defaults to 0 because either tracepoint may legitimately be + * absent, so the error has to be set explicitly here. + */ + err = -ENOMEM; errno = ENOMEM; goto out; } @@ -4502,7 +4532,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace __maybe_ static int trace__set_ev_qualifier_filter(struct trace *trace) { - if (trace->syscalls.events.sys_enter) + /* + * Synchronize syscall filter with BPF augmenter map: + * Pass trace->not_ev_qualifier to indicate blacklist mode ('!' prefix, + * e.g., -e !open,close) vs whitelist mode (-e open,close). + * + * A failure here would leave the BPF program filtering on a partially + * populated map, silently dropping or emitting the wrong syscalls, so + * propagate the error rather than continuing. + */ + if (trace->ev_qualifier_ids.nr > 0) { + int err = augmented_syscalls__set_target_syscalls(trace->ev_qualifier_ids.nr, + trace->ev_qualifier_ids.entries, + trace->not_ev_qualifier); + + if (err) { + pr_err("Failed to set the syscalls to trace in the BPF map: %d\n", err); + return err; + } + } + + if (trace->syscalls.events.sys_enter || trace->syscalls.events.sys_exit) return trace__set_ev_qualifier_tp_filter(trace); return 0; } @@ -4543,13 +4593,21 @@ static int trace__set_filter_loop_pids(struct trace *trace) static int trace__set_filter_pids(struct trace *trace) { - int err = 0; + struct perf_thread_map *threads = evlist__core(trace->evlist)->threads; /* * Better not use !target__has_task() here because we need to cover the * case where no threads were specified in the command line, but a * workload was, and in that case we will fill in the thread_map when * we fork the workload in evlist__prepare_workload. */ + bool has_target = perf_thread_map__pid(threads, 0) != -1; + int err = 0; + + /* + * The exclusion list: --filter-pids names tasks to never report, and + * with no target at all we instead exclude perf itself so that tracing + * does not feed back into itself. + */ if (trace->filter_pids.nr > 0) { err = evlist__append_tp_filter_pids(trace->evlist, trace->filter_pids.nr, trace->filter_pids.entries); @@ -4557,10 +4615,37 @@ static int trace__set_filter_pids(struct trace *trace) err = augmented_syscalls__set_filter_pids(trace->filter_pids.nr, trace->filter_pids.entries); } - } else if (perf_thread_map__pid(evlist__core(trace->evlist)->threads, 0) == -1) { + } else if (!has_target) { err = trace__set_filter_loop_pids(trace); } + if (err) + return err; + + /* + * The inclusion list, which is a separate axis from the exclusion list + * above and so must be programmed even when --filter-pids was given: + * naming tasks to leave out does not widen -p/-t or a workload to the + * whole system. + * + * This matters more than it does on the tracepoint only path. A BPF + * tracepoint program is attached system wide rather than to the + * target's file descriptors, so pids_to_trace is the only thing + * keeping other tasks out. + */ + if (has_target) { + int nr = perf_thread_map__nr(threads); + pid_t *pids = malloc(nr * sizeof(pid_t)); + + if (pids == NULL) + return -ENOMEM; + + for (int i = 0; i < nr; i++) + pids[i] = perf_thread_map__pid(threads, i); + err = augmented_syscalls__set_target_pids(nr, pids); + free(pids); + } + return err; } @@ -4787,7 +4872,8 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } if (!trace->raw_augmented_syscalls) { - if (trace->trace_syscalls && trace__add_syscall_newtp(trace)) + if (trace->trace_syscalls && !trace->syscalls.events.sys_enter && + trace__add_syscall_newtp(trace)) goto out_error_raw_syscalls; if (trace->trace_syscalls) @@ -5795,6 +5881,7 @@ int cmd_trace(int argc, const char **argv) .show_arg_names = true, .args_alignment = 70, .trace_syscalls = false, + .syscall_augment = true, .kernel_syscallchains = false, .max_stack = UINT_MAX, .max_events = ULONG_MAX, @@ -5850,6 +5937,8 @@ int cmd_trace(int argc, const char **argv) OPT_CALLBACK_DEFAULT('F', "pf", &trace.trace_pgfaults, "all|maj|min", "Trace pagefaults", parse_pagefaults, "maj"), OPT_BOOLEAN(0, "syscalls", &trace.trace_syscalls, "Trace syscalls"), + OPT_BOOLEAN(0, "syscall-augment", &trace.syscall_augment, + "Augment syscalls with BPF"), OPT_BOOLEAN('f', "force", &trace.force, "don't complain, do it"), OPT_CALLBACK(0, "call-graph", &trace.opts, "record_mode[,record_size]", record_callchain_help, @@ -5972,7 +6061,7 @@ int cmd_trace(int argc, const char **argv) "cgroup monitoring only available in system-wide mode"); } - if (!trace.trace_syscalls) + if (!trace.trace_syscalls || !trace.syscall_augment) goto skip_augmentation; if ((argc >= 1) && (strcmp(argv[0], "record") == 0)) { @@ -5997,8 +6086,19 @@ int cmd_trace(int argc, const char **argv) trace__add_syscall_newtp(&trace); err = augmented_syscalls__create_bpf_output(trace.evlist); - if (err == 0) + if (err == 0) { trace.syscalls.events.bpf_output = evlist__last(trace.evlist); + } else { + /* + * augmented_syscalls__prepare() already attached sys_enter and + * sys_exit, which are system wide. Falling through to + * skip_augmentation without undoing that would run a BPF + * program for every syscall on the machine, for the whole + * session, with nothing consuming the output. + */ + pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); + augmented_syscalls__cleanup(); + } skip_augmentation: err = -1; @@ -6054,7 +6154,9 @@ int cmd_trace(int argc, const char **argv) * syscall. */ if (trace.syscalls.events.bpf_output) { - evlist__for_each_entry(trace.evlist, evsel) { + struct evsel *n; + + evlist__for_each_entry_safe(trace.evlist, n, evsel) { bool raw_syscalls_sys_exit = evsel__name_is(evsel, "raw_syscalls:sys_exit"); if (raw_syscalls_sys_exit) { @@ -6069,21 +6171,26 @@ int cmd_trace(int argc, const char **argv) evsel__init_augmented_syscall_tp_args(augmented)) goto out; /* - * Augmented is __augmented_syscalls__ BPF_OUTPUT event + * Augmented is __augmented_syscalls__ BPF_OUTPUT event. * Above we made sure we can get from the payload the tp fields * that we get from syscalls:sys_enter tracefs format file. + * Since BPF outputs all enter events (both augmented and + * unaugmented) into __augmented_syscalls__, we remove the raw + * sys_enter evsel from evlist so that perf trace only listens + * to __augmented_syscalls__, avoiding duplicate events and + * avoiding kernel tracepoint vetoes. + * + * Because evlist__remove() removes the first evsel (which had + * tracking=true by default), re-designate the tracking event + * so PERF_RECORD_COMM and fork tracking continue to be enabled. */ augmented->handler = trace__sys_enter; - /* - * Now we do the same for the *syscalls:sys_enter event so that - * if we handle it directly, i.e. if the BPF prog returns 0 so - * as not to filter it, then we'll handle it just like we would - * for the BPF_OUTPUT one: - */ - if (evsel__init_augmented_syscall_tp(evsel, evsel) || - evsel__init_augmented_syscall_tp_args(evsel)) - goto out; - evsel->handler = trace__sys_enter; + evlist__remove(trace.evlist, evsel); + evsel__put_and_free_priv(evsel); + trace.syscalls.events.sys_enter = NULL; + evlist__set_tracking_event(trace.evlist, + trace.syscalls.events.sys_exit ?: augmented); + continue; } if (strstarts(evsel__name(evsel), "syscalls:sys_exit_")) { diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 3bc9e28a9b8a..6ca9507ecc02 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -114,6 +114,41 @@ struct pids_filtered { __uint(max_entries, 64); } pids_filtered SEC(".maps"); +/* + * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when + * attached to a process with -p or tracing a specific command workload). + * + * has_pids_to_trace: Set to true if target PID filtering is active. + * When false, all processes are eligible for tracing. + */ +struct pids_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, pid_t); + __type(value, bool); + __uint(max_entries, 1024); +} pids_to_trace SEC(".maps"); + +bool has_pids_to_trace; + +/* + * Hash map storing syscall IDs for filtering (via 'perf trace -e ...'). + * + * has_syscalls_to_trace: Set to true if any syscall filter is active. + * not_syscalls_to_trace: Inverts matching when '!' prefix is used in -e + * (e.g., -e !open,close means trace everything EXCEPT + * open and close; an exclusion blacklist rather than + * an inclusion whitelist). + */ +struct syscalls_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, int); + __type(value, bool); + __uint(max_entries, 1024); +} syscalls_to_trace SEC(".maps"); + +bool has_syscalls_to_trace; +bool not_syscalls_to_trace; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -154,8 +189,8 @@ static inline struct augmented_args_payload *augmented_args_payload(void) static inline int augmented__output(void *ctx, struct augmented_args_payload *args, int len) { - /* If perf_event_output fails, return non-zero so that it gets recorded unaugmented */ - return bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + return 1; } static inline int augmented__beauty_output(void *ctx, void *data, int len) @@ -191,10 +226,21 @@ unsigned int augmented_arg__read_str(struct augmented_arg *augmented_arg, const return augmented_len; } -SEC("tp/raw_syscalls/sys_enter") +/* + * Default sys_enter program for syscalls without pointer argument augmentation. + * Writes the raw struct syscall_enter_args payload into __augmented_syscalls__ + * and returns 1 so the tracepoint is never vetoed in the kernel. + */ +SEC("tp/syscalls/sys_enter_unaugmented") int syscall_unaugmented(struct syscall_enter_args *args) { - return 1; + struct augmented_args_payload *augmented_args = augmented_args_payload(); + + if (augmented_args == NULL) + return 1; + + bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + return augmented__output(args, augmented_args, sizeof(augmented_args->args)); } /* @@ -424,11 +470,41 @@ static pid_t getpid(void) return bpf_get_current_pid_tgid(); } +/* + * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { return bpf_map_lookup_elem(pids, &pid) != NULL; } +/* + * Checks if the current task (thread PID or process TGID) is targeted for tracing. + * Checks both PID (thread ID) and TGID (process ID) so that all threads of a + * target process match. + */ +static inline bool pid_to_trace__has(pid_t pid) +{ + pid_t tgid = bpf_get_current_pid_tgid() >> 32; + + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || + bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; +} + +/* + * Determines if a syscall should be traced based on the filter map: + * - When not_syscalls_to_trace is true: blacklist mode (trace if NOT in map). + * - When not_syscalls_to_trace is false: whitelist mode (trace ONLY if IN map). + */ +static inline bool syscall_to_trace__enabled(int id) +{ + bool in_map = bpf_map_lookup_elem(&syscalls_to_trace, &id) != NULL; + + if (not_syscalls_to_trace) + return !in_map; + return in_map; +} + u64 ZERO = 0; /* @@ -562,6 +638,11 @@ static int augment_sys_enter(void *ctx, struct syscall_enter_args *args) return augmented__beauty_output(ctx, payload, sizeof(struct syscall_enter_args) + output); } +/* + * Main raw_syscalls:sys_enter tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel for + * other concurrent listeners. Filtered events simply do not output to the ring buffer. + */ SEC("tp/raw_syscalls/sys_enter") int sys_enter(struct syscall_enter_args *args) { @@ -576,8 +657,11 @@ int sys_enter(struct syscall_enter_args *args) * initial, non-augmented raw_syscalls:sys_enter payload. */ + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; augmented_args = augmented_args_payload(); if (augmented_args == NULL) @@ -585,25 +669,41 @@ int sys_enter(struct syscall_enter_args *args) bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + if (has_syscalls_to_trace && !syscall_to_trace__enabled(augmented_args->args.syscall_nr)) + return 1; + /* - * Jump to syscall specific augmenter, even if the default one, - * "!raw_syscalls:unaugmented" that will just return 1 to return the - * unaugmented tracepoint payload. + * Jump to syscall specific augmenter. If augmented, augment_sys_enter() + * outputs the payload to __augmented_syscalls__ and returns 0. + * Return 1 so we never veto the kernel tracepoint for other listeners. */ - if (augment_sys_enter(args, &augmented_args->args)) - bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + if (augment_sys_enter(args, &augmented_args->args) == 0) + return 1; - // If not found on the PROG_ARRAY syscalls map, then we're filtering it: - return 0; + bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + + /* + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. + */ + return 1; } +/* + * Main raw_syscalls:sys_exit tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel. + */ SEC("tp/raw_syscalls/sys_exit") int sys_exit(struct syscall_exit_args *args) { struct syscall_exit_args exit_args; + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); /* @@ -613,9 +713,12 @@ int sys_exit(struct syscall_exit_args *args) */ bpf_tail_call(args, &syscalls_sys_exit, exit_args.syscall_nr); /* - * If not found on the PROG_ARRAY syscalls map, then we're filtering it: + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. perf trace's own evsel filter will discard non-matching + * syscalls. */ - return 0; + return 1; } char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index 645819d79b6d..b47f11d40dd8 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,4 +1,5 @@ #include <bpf/libbpf.h> +#include <errno.h> #include <internal/xyarray.h> #include <string.h> @@ -11,6 +12,23 @@ static struct augmented_raw_syscalls_bpf *skel; static struct evsel *bpf_output; +/* Set by attach_prog() so the first failure is what gets reported. */ +static int attach_err; + +static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const char *name) +{ + *link = bpf_program__attach(prog); + if (*link) + return 0; + /* + * Save errno before pr_debug(), which formats and writes output and so + * can overwrite it. + */ + attach_err = -errno; + pr_debug("Failed to attach %s BPF program\n", name); + return attach_err; +} + int augmented_syscalls__prepare(void) { struct bpf_program *prog; @@ -36,11 +54,35 @@ int augmented_syscalls__prepare(void) if (err < 0) { libbpf_strerror(err, buf, sizeof(buf)); pr_debug("Failed to load augmented syscalls BPF skeleton: %s\n", buf); + /* + * Tear the skeleton down rather than leaving a half initialized + * one behind. The caller falls back to unaugmented tracing and + * still calls the setters below, which must then do nothing + * instead of failing against a skeleton with no maps. + */ + augmented_syscalls__cleanup(); return err; } - augmented_raw_syscalls_bpf__attach(skel); + /* + * Only sys_enter and sys_exit are attached, the remaining programs are + * reached by tail calls. Attach them explicitly and, on failure, undo + * any partial attachment: leaving sys_enter live on + * raw_syscalls:sys_enter would keep running a BPF program for every + * syscall on the system for a perf trace session that never starts. + */ + if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) + goto out_cleanup; + if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) + goto out_cleanup; + return 0; + +out_cleanup: + err = attach_err; + /* Destroys every link attached above along with the skeleton. */ + augmented_syscalls__cleanup(); + return err; } int augmented_syscalls__create_bpf_output(struct evlist *evlist) @@ -99,6 +141,96 @@ int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) return err; } +/* + * Populate target PIDs in the BPF pids_to_trace map (e.g., for -p <PID> or + * when tracing a specified command workload). + */ +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) +{ + bool value = true; + int err = 0; + + if (skel == NULL || nr == 0) + return 0; + + for (size_t i = 0; i < nr; ++i) { + err = bpf_map__update_elem(skel->maps.pids_to_trace, &pids[i], + sizeof(*pids), &value, sizeof(value), + BPF_ANY); + if (err) + return err; + } + /* + * Set the flag only once every target is in the map. The BPF programs + * are attached by this point, so flipping it first would have them + * filter against a partially populated map and drop syscalls made by + * the targets that had not been added yet. + */ + skel->bss->has_pids_to_trace = true; + return 0; +} + +int augmented_syscalls__add_target_pid(pid_t pid) +{ + bool value = true; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), BPF_ANY); +} + +int augmented_syscalls__del_target_pid(pid_t pid) +{ + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); +} + +bool augmented_syscalls__has_target_pid(pid_t pid) +{ + bool value; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return false; + + return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), 0) == 0; +} + +/* + * Populate syscalls in the BPF syscalls_to_trace map: + * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace + * all syscalls EXCEPT these). + * false if whitelist mode (trace ONLY these syscalls). + */ +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls) +{ + bool value = true; + int err = 0; + + if (skel == NULL || nr == 0) + return 0; + + skel->bss->not_syscalls_to_trace = not_syscalls; + for (size_t i = 0; i < nr; ++i) { + err = bpf_map__update_elem(skel->maps.syscalls_to_trace, &syscall_ids[i], + sizeof(int), &value, sizeof(value), + BPF_ANY); + if (err) + return err; + } + /* + * As for the pid maps, publish the filter only once it is complete: + * in whitelist mode a half filled map would drop syscalls that were + * asked for but not added yet. + */ + skel->bss->has_syscalls_to_trace = true; + return 0; +} + int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd) { if (skel == NULL) @@ -141,4 +273,5 @@ struct bpf_program *augmented_syscalls__find_by_title(const char *name) void augmented_syscalls__cleanup(void) { augmented_raw_syscalls_bpf__destroy(skel); + skel = NULL; } diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index a1cd9a5e0213..5702eda3b469 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <stdbool.h> #include <sys/types.h> struct bpf_program; @@ -13,6 +14,11 @@ int augmented_syscalls__prepare(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__add_target_pid(pid_t pid); +int augmented_syscalls__del_target_pid(pid_t pid); +bool augmented_syscalls__has_target_pid(pid_t pid); +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); struct bpf_program *augmented_syscalls__unaugmented(void); @@ -40,6 +46,34 @@ static inline int augmented_syscalls__set_filter_pids(unsigned int nr __maybe_un return 0; } +static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_unused, + pid_t *pids __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) +{ + return false; +} + +static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, + int *syscall_ids __maybe_unused, + bool not_syscalls __maybe_unused) +{ + return 0; +} + static inline int augmented_syscalls__get_map_fds(int *enter_fd __maybe_unused, int *exit_fd __maybe_unused, int *beauty_fd __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 06/14] perf trace: Handle fork and exit directly in BPF filter maps 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (4 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 05/14] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 07/14] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers ` (8 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Updating target or filtered PIDs in userspace upon processing PERF_RECORD_FORK and PERF_RECORD_EXIT events introduces latency between event occurrence and userspace BPF map updates. If a newly forked child executes system calls before userspace processes PERF_RECORD_FORK, those syscalls may be dropped by BPF PID filtering. Conversely, if userspace evicts PIDs asynchronously on PERF_RECORD_EXIT, the kernel may recycle a PID before userspace processes the exit event, causing the late eviction to silently drop a newly created task that received the recycled PID. Address this by attaching BTF-typed raw tracepoint BPF programs directly to the scheduler task lifetime tracepoints: 1. Attach SEC("tp_btf/sched_process_fork") (sched_process_fork), which runs in copy_process() in the parent's context before wake_up_new_task() wakes the child. Using tp_btf rather than SEC("tp/sched/sched_process_fork") receives the stable TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) rather than the tracepoint ring-buffer record (TP_STRUCT__entry), whose layout changed in Linux 6.16 when parent_comm and child_comm were converted from 16-byte arrays to 4-byte __data_loc strings. When inherit is enabled and the parent's PID or TGID is in pids_to_trace or pids_filtered, insert child->pid into the corresponding map immediately. Because child->pid is task_struct.pid (the global initial-namespace PID), this works accurately across PID namespaces without aliasing host PIDs, and covers both new processes and CLONE_THREAD threads without needing real_parent CO-RE walks or syscall-return heuristics. 2. Attach SEC("tp_btf/sched_process_exit") (sched_process_exit), which runs in do_exit() for every task in its own context, including tasks killed by signals (SIGKILL, SIGSEGV, etc.) and secondary threads torn down implicitly by exit_group. Delete the dying task's PID from pids_to_trace and pids_filtered immediately in kernel space, eliminating both map leaks and any asynchronous userspace eviction window where PID recycling could occur. 3. Attach SEC("tp_btf/sched_process_exec") (sched_process_exec) to follow the one case where a live task's pid changes underneath the maps. When a thread that is not the group leader execs, de_thread() kills the leader and hands the leader's pid, which is the tgid, to the exec'ing thread. The leader dies first, so sched_process_exit() has already dropped exactly the pid the survivor now holds, and the survivor's old entry would be stranded in the map for good. Move the entry from old_pid to p->pid. old_pid is sampled in bprm_execve() before de_thread() runs, so the ordinary group leader exec is a no-op here. 4. With every live task registered before its first syscall and evicted in do_exit(), simplify pid_to_trace__has() and pid_filter__has() to single BPF hash map lookups, and move bpf_probe_read_kernel() in sys_exit back after the PID filter checks. 5. Pass the inherit flag from userspace to BPF .rodata via augmented_syscalls__prepare(!trace.opts.no_inherit), and split attaching out of it into augmented_syscalls__attach(), called from trace__run() once the pid, syscall and program array maps have all been programmed. These are system wide programs, so from the instant they attach they alone decide what is traced: attaching at load time, as before, left a window in which a target could fork without sched_process_fork() knowing the parent was a target, and with the userspace fork handling gone there was nothing left to recover it. The scheduler programs are attached ahead of sys_enter and sys_exit for the same reason. Set has_pids_filtered only after populating pids_filtered. 6. Remove the userspace BPF map updates from PERF_RECORD_FORK and PERF_RECORD_EXIT in trace__process_event(), and delete the now-unused augmented_syscalls__{add,del,has}_target_pid() helpers. No coverage is lost with them: those records only come into being once the ring buffers are mapped by evlist__do_mmap() and the events are switched on by evlist__enable(), both of which run after augmented_syscalls__attach() in trace__run(), and they are then acted on later still, whenever the poll loop gets round to them. The scheduler programs therefore go live strictly earlier than the userspace path could ever have reacted. 7. Gate pid_filter__has() on a has_pids_filtered flag in .bss so the common case without --filter-pids performs no map lookups, and size pids_to_trace and pids_filtered at 16384 entries. pids_filtered is grown from 64 because it is no longer just the handful of pids userspace names: sched_process_fork() adds every descendant of those, so a --filter-pids target that forks or is heavily threaded needs the same headroom as a traced one. A fork or exit is still not seen if it happens before the programs are attached, that is between evlist__create_maps() scanning /proc for a -p target and augmented_syscalls__attach(). Such a window is inherent in programming a system wide filter before switching it on, and as above the userspace handling did not cover it either. What it costs is small: a child forked in the window is still traced through the tracepoints its parent's events were inherited by, only unaugmented, because sys_enter returns 1 for a pid that is not in the map rather than vetoing the tracepoint. A workload started by 'perf trace -- cmd' cannot hit it at all, as evlist__prepare_workload() leaves the child blocked on a pipe until evlist__start_workload(), well after the attach. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 39 ++-- .../bpf_skel/augmented_raw_syscalls.bpf.c | 178 +++++++++++++++++- tools/perf/util/bpf_trace_augment.c | 116 +++++++----- tools/perf/util/trace_augment.h | 28 +-- 4 files changed, 262 insertions(+), 99 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 9bb8316e334c..f876f0df06a5 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2055,23 +2055,6 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; - case PERF_RECORD_FORK: - if (trace->raw_augmented_syscalls && - (augmented_syscalls__has_target_pid(event->fork.ppid) || - augmented_syscalls__has_target_pid(event->fork.ptid))) { - augmented_syscalls__add_target_pid(event->fork.pid); - } - ret = machine__process_fork_event(machine, event, sample); - break; - case PERF_RECORD_EXIT: - if (trace->raw_augmented_syscalls) { - if (event->fork.pid == event->fork.tid) - augmented_syscalls__del_target_pid(event->fork.pid); - else - augmented_syscalls__del_target_pid(event->fork.tid); - } - ret = machine__process_exit_event(machine, event, sample); - break; default: ret = machine__process_event(machine, event, sample); break; @@ -4987,6 +4970,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } } + /* + * Everything the BPF programs filter on is now in their maps, so it is + * safe to let them run. They are attached system wide, so anything + * before this point would have been filtered against a map that was + * still being built up. + */ + err = augmented_syscalls__attach(); + if (err < 0) + goto out_errno; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the @@ -6079,7 +6072,7 @@ int cmd_trace(int argc, const char **argv) goto skip_augmentation; } - err = augmented_syscalls__prepare(); + err = augmented_syscalls__prepare(!trace.opts.no_inherit); if (err < 0) goto skip_augmentation; @@ -6090,11 +6083,11 @@ int cmd_trace(int argc, const char **argv) trace.syscalls.events.bpf_output = evlist__last(trace.evlist); } else { /* - * augmented_syscalls__prepare() already attached sys_enter and - * sys_exit, which are system wide. Falling through to - * skip_augmentation without undoing that would run a BPF - * program for every syscall on the machine, for the whole - * session, with nothing consuming the output. + * Drop the loaded skeleton before falling back to unaugmented + * tracing. Otherwise the setters called from trace__run() would + * still program its maps, and augmented_syscalls__attach() would + * then put system wide BPF programs on raw_syscalls for a + * session with nothing consuming their output. */ pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); augmented_syscalls__cleanup(); diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 6ca9507ecc02..7124ed3c39c8 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -9,6 +9,7 @@ #include "vmlinux.h" #include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> #include <linux/limits.h> #define PERF_ALIGN(x, a) __PERF_ALIGN_MASK(x, (typeof(x))(a)-1) @@ -107,25 +108,48 @@ struct augmented_arg { }; }; +/* + * Hash map of PIDs/TGIDs whose events must be discarded, e.g. perf trace's own + * pid, so that tracing doesn't feed back on itself. + * + * has_pids_filtered: set to true only when the map is populated. Checking a + * boolean is much cheaper than a map lookup, and sys_enter + * runs for every syscall on the system, so the common + * "no pids filtered" case must stay on a fast path. + * + * max_entries matches pids_to_trace: userspace only ever names a handful of + * pids here, but sched_process_fork() below adds every descendant of those, + * so a --filter-pids target that forks or is heavily threaded needs the same + * headroom as a traced one. + */ struct pids_filtered { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 64); + __uint(max_entries, 16384); } pids_filtered SEC(".maps"); +bool has_pids_filtered; + /* * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when * attached to a process with -p or tracing a specific command workload). * * has_pids_to_trace: Set to true if target PID filtering is active. * When false, all processes are eligible for tracing. + * + * max_entries bounds how many tasks can be tracked at once. sched_process_exit + * below evicts a task as it dies, whatever it died of, so the map holds live + * tasks rather than growing without bound. It is sized well + * above the thread count of realistic traced workloads; should a workload + * still exceed it, bpf_map_update_elem() fails with -E2BIG and the extra + * tasks are simply not traced rather than anything being corrupted. */ struct pids_to_trace { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 1024); + __uint(max_entries, 16384); } pids_to_trace SEC(".maps"); bool has_pids_to_trace; @@ -149,6 +173,9 @@ struct syscalls_to_trace { bool has_syscalls_to_trace; bool not_syscalls_to_trace; +/* Inherit tracing for child tasks (set to false if --no-inherit is specified) */ +const volatile bool inherit = true; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -471,24 +498,35 @@ static pid_t getpid(void) } /* - * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * Checks if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * + * Children of a filtered task are added to the map by sched_process_fork() + * below, so a plain lookup is all that is needed here. */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { + /* + * Fast path: this runs for every syscall on the system, so when no pid + * is filtered do no work at all rather than failing a lookup. + */ + if (!has_pids_filtered) + return false; + return bpf_map_lookup_elem(pids, &pid) != NULL; } /* - * Checks if the current task (thread PID or process TGID) is targeted for tracing. - * Checks both PID (thread ID) and TGID (process ID) so that all threads of a - * target process match. + * Checks if the current task is targeted for tracing. + * + * Every thread that existed when tracing started was named by the target and + * inserted from userspace, and every task created since was inserted by + * sched_process_fork() below, before it was able to run. So there is nothing + * to derive here, and in particular no need to consult the tgid or walk to the + * parent: a task is traced if and only if it is in the map. */ static inline bool pid_to_trace__has(pid_t pid) { - pid_t tgid = bpf_get_current_pid_tgid() >> 32; - - return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || - bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL; } /* @@ -706,6 +744,7 @@ int sys_exit(struct syscall_exit_args *args) return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); + /* * Jump to syscall specific return augmenter, even if the default one, * "!raw_syscalls:unaugmented" that will just return 1 to return the @@ -721,4 +760,123 @@ int sys_exit(struct syscall_exit_args *args) return 1; } +/* + * Propagate tracing to a newly created task. + * + * tp_btf/sched_process_fork is raised by copy_process(), in the parent's + * context and before the child is woken, so the child is in the maps before it + * can issue its first syscall. That removes the need to inspect real_parent + * when a syscall is seen from an unknown task, which could neither tell a + * genuine descendant from a task merely reparented to a traced init, nor keep + * following a descendant whose parent had already exited. + * + * Using tp_btf rather than tp/sched/sched_process_fork avoids depending on the + * tracepoint ring-buffer record layout (TP_STRUCT__entry), which changed in + * Linux 6.16 when parent_comm and child_comm were converted from fixed 16-byte + * arrays to 4-byte __data_loc strings (shrinking the tracepoint context from + * 48 to 24 bytes and causing BPF_PROG_TYPE_TRACEPOINT attachment to fail with + * -EACCES when accessing higher offsets). Instead, tp_btf receives the stable + * TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) + * directly. + * + * child->pid is task_struct.pid, i.e. the pid in the initial namespace, which + * is what the maps are keyed by. A clone() return value, in contrast, is the + * pid in the caller's namespace and would alias an unrelated host task when a + * containerised workload is traced. + * + * CLONE_THREAD needs no special handling: a new thread arrives here like any + * other task and is inserted under its own pid. + */ +SEC("tp_btf/sched_process_fork") +int BPF_PROG(sched_process_fork, struct task_struct *parent, struct task_struct *child) +{ + pid_t parent_tgid, parent_pid, child_pid; + bool val = true; + + if (!inherit) + return 0; + + /* + * The parent's own pid and tgid: the thread that called clone() may + * itself only be tracked by the pid of its thread group leader. + */ + parent_pid = parent->pid; + parent_tgid = parent->tgid; + child_pid = child->pid; + + if (has_pids_to_trace && + (bpf_map_lookup_elem(&pids_to_trace, &parent_pid) != NULL || + bpf_map_lookup_elem(&pids_to_trace, &parent_tgid) != NULL)) + bpf_map_update_elem(&pids_to_trace, &child_pid, &val, BPF_ANY); + + if (has_pids_filtered && + (bpf_map_lookup_elem(&pids_filtered, &parent_pid) != NULL || + bpf_map_lookup_elem(&pids_filtered, &parent_tgid) != NULL)) + bpf_map_update_elem(&pids_filtered, &child_pid, &val, BPF_ANY); + + return 0; +} + +/* + * Drop a dying task from the maps. + * + * tp_btf/sched_process_exit is raised by do_exit() for every task, in its own + * context, so unlike hooking the exit and exit_group syscalls this also covers + * tasks killed by a signal and threads torn down implicitly by exit_group. + * + * Doing it here rather than from the userspace PERF_RECORD_EXIT handler also + * means there is no window between the task dying and the map being updated, + * during which the kernel could recycle the pid and the late eviction silently + * stop tracing whichever new task received it. + * + * Each thread is reported separately, including the group leader, whose pid is + * the thread group's tgid, so one delete per map covers both uses of the key. + */ +SEC("tp_btf/sched_process_exit") +int BPF_PROG(sched_process_exit, struct task_struct *p) +{ + pid_t pid = p->pid; + + bpf_map_delete_elem(&pids_to_trace, &pid); + bpf_map_delete_elem(&pids_filtered, &pid); + + return 0; +} + +/* + * Follow a task whose pid changed under it. + * + * When a thread that is not the thread group leader execs, de_thread() kills + * the rest of the group and then hands the leader's pid, which is the tgid, to + * the exec'ing thread. The leader dies first, so sched_process_exit() above + * has already dropped that pid from the maps, and the survivor is now keyed by + * a pid nothing knows about while its original entry is left behind for good. + * + * Move the entry across so the task stays tracked and nothing is leaked. + * old_pid is sampled in bprm_execve() before de_thread() runs, so for the + * common case of the group leader exec'ing it simply equals p->pid and there + * is nothing to do. + */ +SEC("tp_btf/sched_process_exec") +int BPF_PROG(sched_process_exec, struct task_struct *p, pid_t old_pid) +{ + pid_t pid = p->pid; + bool val = true; + + if (pid == old_pid) + return 0; + + if (bpf_map_lookup_elem(&pids_to_trace, &old_pid) != NULL) { + bpf_map_update_elem(&pids_to_trace, &pid, &val, BPF_ANY); + bpf_map_delete_elem(&pids_to_trace, &old_pid); + } + + if (bpf_map_lookup_elem(&pids_filtered, &old_pid) != NULL) { + bpf_map_update_elem(&pids_filtered, &pid, &val, BPF_ANY); + bpf_map_delete_elem(&pids_filtered, &old_pid); + } + + return 0; +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index b47f11d40dd8..c1b1baf90eb2 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -29,7 +29,7 @@ static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const c return attach_err; } -int augmented_syscalls__prepare(void) +int augmented_syscalls__prepare(bool inherit) { struct bpf_program *prog; char buf[128]; @@ -41,12 +41,18 @@ int augmented_syscalls__prepare(void) return -errno; } + skel->rodata->inherit = inherit; + /* - * Disable attaching the BPF programs except for sys_enter and - * sys_exit that tail call into this as necessary. + * Disable attaching the BPF programs other than those attached + * explicitly by augmented_syscalls__attach(), the rest are reached by + * tail calls. */ bpf_object__for_each_program(prog, skel->obj) { - if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit) + if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit && + prog != skel->progs.sched_process_fork && + prog != skel->progs.sched_process_exit && + prog != skel->progs.sched_process_exec) bpf_program__set_autoattach(prog, /*autoattach=*/false); } @@ -64,13 +70,42 @@ int augmented_syscalls__prepare(void) return err; } + return 0; +} + +int augmented_syscalls__attach(void) +{ + int err; + + if (skel == NULL) + return 0; + /* - * Only sys_enter and sys_exit are attached, the remaining programs are - * reached by tail calls. Attach them explicitly and, on failure, undo - * any partial attachment: leaving sys_enter live on - * raw_syscalls:sys_enter would keep running a BPF program for every - * syscall on the system for a perf trace session that never starts. + * Attaching is deliberately separate from, and a lot later than, + * loading: these are system wide tracepoint programs, so from the + * moment they are attached they are the only thing deciding which + * tasks and syscalls are traced. Going live before the pid and + * syscall maps are populated would mean a target that forked in the + * meantime was never picked up by sched_process_fork() below. + * + * Attach explicitly, so that a failure part way through can undo what + * came before it: leaving sys_enter live on raw_syscalls:sys_enter + * would keep running a BPF program for every syscall on the system for + * a perf trace session that never starts. + * + * The scheduler programs maintain the pid maps, and are attached first + * so that no fork, exit or exec can be missed between sys_enter going + * live and the maps being maintained. */ + if (attach_prog(&skel->links.sched_process_fork, skel->progs.sched_process_fork, + "sched_process_fork")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exit, skel->progs.sched_process_exit, + "sched_process_exit")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exec, skel->progs.sched_process_exec, + "sched_process_exec")) + goto out_cleanup; if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) goto out_cleanup; if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) @@ -82,6 +117,13 @@ int augmented_syscalls__prepare(void) err = attach_err; /* Destroys every link attached above along with the skeleton. */ augmented_syscalls__cleanup(); + /* + * Tearing the skeleton down closes file descriptors and frees memory, + * either of which may overwrite errno. Restore it so that a caller + * reporting this with "%m" describes the attach failure rather than + * whatever the teardown happened to do last. + */ + errno = -err; return err; } @@ -128,17 +170,29 @@ int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) bool value = true; int err = 0; - if (skel == NULL) + if (skel == NULL || nr == 0) return 0; + /* + * Tell the BPF program that the pids_filtered map is in use. Without + * this it would have to look up every task in an empty map, on every + * syscall on the system, to find out that nothing is filtered. + */ for (size_t i = 0; i < nr; ++i) { err = bpf_map__update_elem(skel->maps.pids_filtered, &pids[i], sizeof(*pids), &value, sizeof(value), BPF_ANY); if (err) - break; + return err; } - return err; + /* + * Publish the filter only now that the map is fully populated. + * augmented_syscalls__attach() has not run yet, so nothing is reading + * either of them, but keeping the flag and the map consistent means + * the ordering stays correct however the callers are rearranged. + */ + skel->bss->has_pids_filtered = true; + return 0; } /* @@ -161,45 +215,15 @@ int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) return err; } /* - * Set the flag only once every target is in the map. The BPF programs - * are attached by this point, so flipping it first would have them - * filter against a partially populated map and drop syscalls made by - * the targets that had not been added yet. + * Set the flag only once every target is in the map, so that the two + * are never inconsistent. Publishing it first would, once the + * programs are attached, have them filter against a partially + * populated map and drop syscalls made by targets not yet added. */ skel->bss->has_pids_to_trace = true; return 0; } -int augmented_syscalls__add_target_pid(pid_t pid) -{ - bool value = true; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), BPF_ANY); -} - -int augmented_syscalls__del_target_pid(pid_t pid) -{ - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); -} - -bool augmented_syscalls__has_target_pid(pid_t pid) -{ - bool value; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return false; - - return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), 0) == 0; -} - /* * Populate syscalls in the BPF syscalls_to_trace map: * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 5702eda3b469..ad992f5fa726 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -10,14 +10,12 @@ struct evlist; #ifdef HAVE_BPF_SKEL -int augmented_syscalls__prepare(void); +int augmented_syscalls__prepare(bool inherit); +int augmented_syscalls__attach(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); -int augmented_syscalls__add_target_pid(pid_t pid); -int augmented_syscalls__del_target_pid(pid_t pid); -bool augmented_syscalls__has_target_pid(pid_t pid); int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); @@ -26,11 +24,16 @@ void augmented_syscalls__cleanup(void); #else /* !HAVE_BPF_SKEL */ -static inline int augmented_syscalls__prepare(void) +static inline int augmented_syscalls__prepare(bool inherit __maybe_unused) { return -1; } +static inline int augmented_syscalls__attach(void) +{ + return 0; +} + static inline int augmented_syscalls__create_bpf_output(struct evlist *evlist __maybe_unused) { return -1; @@ -52,21 +55,6 @@ static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_un return 0; } -static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) -{ - return false; -} - static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, int *syscall_ids __maybe_unused, bool not_syscalls __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 07/14] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (5 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 06/14] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 08/14] perf test common: Do not globally disable tracing events in clear_all_probes Ian Rogers ` (7 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz test_task_analyzer.sh writes perf.data and temporary files directly into the current working directory, causing collisions when running tests in parallel. As a temporary measure until `perf script report` supports an input file option, resolve perfdir to an absolute path, change directory into $tmpdir for the test duration, and clean up in the exit trap. Remove the (exclusive) tag so the test runs in parallel. perfdir is derived from $0, which may be relative, so it has to be resolved before the cd into $tmpdir, otherwise both PERF_EXEC_PATH and the cleanup trap point at paths that no longer exist. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/test_task_analyzer.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/test_task_analyzer.sh b/tools/perf/tests/shell/test_task_analyzer.sh index 0314412e63b4..76df8af3312d 100755 --- a/tools/perf/tests/shell/test_task_analyzer.sh +++ b/tools/perf/tests/shell/test_task_analyzer.sh @@ -1,8 +1,18 @@ #!/bin/bash -# perf script task-analyzer tests (exclusive) +# perf script task-analyzer tests # SPDX-License-Identifier: GPL-2.0 +# Resolve the source directory before changing the working directory below, +# $0 may be a relative path and would no longer resolve from $tmpdir. +perfdir=$(cd "$(dirname "$0")/../.." && pwd) + tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) +# The cleanup trap is only installed further down, once the functions it +# calls have been defined, so tidy up by hand if this cd fails. +cd "$tmpdir" || { + rmdir "$tmpdir" + exit 1 +} # TODO: perf script report only supports input from the CWD perf.data file, make # it support input from any file. perfdata="perf.data" @@ -11,7 +21,6 @@ csvsummary="$tmpdir/csvsummary" err=0 # set PERF_EXEC_PATH to find scripts in the source directory -perfdir=$(dirname "$0")/../.. if [ -e "$perfdir/scripts/python/Perf-Trace-Util" ]; then export PERF_EXEC_PATH=$perfdir fi @@ -20,8 +29,7 @@ fi export ASAN_OPTIONS=detect_leaks=0 cleanup() { - rm -f "${perfdata}" - rm -f "${perfdata}".old + cd "$perfdir" || cd /tmp || exit rm -rf "$tmpdir" trap - exit term int -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 08/14] perf test common: Do not globally disable tracing events in clear_all_probes 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (6 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 07/14] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 09/14] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers ` (6 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz `echo 0 > /sys/kernel/debug/tracing/events/enable` disables tracepoint events system-wide. When running tests in parallel, this kills active tracing and recording sessions in concurrent tests (such as perf trace and perf record). Remove the global event disable from clear_all_probes(). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/common/init.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/perf/tests/shell/common/init.sh b/tools/perf/tests/shell/common/init.sh index cbfc78bec974..d2c7a31e2c6f 100644 --- a/tools/perf/tests/shell/common/init.sh +++ b/tools/perf/tests/shell/common/init.sh @@ -132,7 +132,6 @@ check_uprobes_available() clear_all_probes() { - echo 0 > /sys/kernel/debug/tracing/events/enable check_kprobes_available && echo > /sys/kernel/debug/tracing/kprobe_events check_uprobes_available && echo > /sys/kernel/debug/tracing/uprobe_events } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 09/14] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (7 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 08/14] perf test common: Do not globally disable tracing events in clear_all_probes Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 10/14] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers ` (5 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The probe name `vfs_getname` was hardcoded, causing collisions when tests ran concurrently. Furthermore, `cleanup_probe_vfs_getname()` used `perf probe -d probe:vfs_getname*`, deleting probes registered by other parallel tests. Scope the probe name to the pid, and rename it to `getname_flags_$$` so that it no longer begins with "vfs_getname". perf trace calls evlist__add_vfs_getname(), which opens every event matching a hardcoded "probe:vfs_getname*" wildcard, so a perf trace run by any other test would otherwise pin this probe and make `perf probe -d` fail with -EBUSY. That also unblocks making the perf trace tests non-exclusive later in this series. Enumerate the probes to record and to delete from `perf probe -l`, matching `^probe:${vfs_getname}(_[[:digit:]]+)?$` exactly, rather than globbing on `${vfs_getname}*`. perf probe appends _1, _2, ... when getname_flags is inlined at more than one call site, so the variants do have to be matched, but since the name now ends in a pid a trailing wildcard would also match the probes of a test whose pid merely starts with this one's, e.g. 123 and 1234. Remove the `(exclusive)` tag from probe_vfs_getname.sh and record+script_probe_vfs_getname.sh so they run concurrently in pass 1. trace+probe_vfs_getname.sh has to stay exclusive: it is the one test that wants to be discovered by that wildcard, so it sets vfs_getname to a "vfs_getname_$$" name before sourcing the library, and would then pin its siblings' probes if it ran alongside them. A comment in the test records this. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/lib/probe_vfs_getname.sh | 34 ++++++++++++++++--- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+script_probe_vfs_getname.sh | 18 +++++++--- .../tests/shell/trace+probe_vfs_getname.sh | 9 +++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/tools/perf/tests/shell/lib/probe_vfs_getname.sh b/tools/perf/tests/shell/lib/probe_vfs_getname.sh index 88cd0e26d5f6..89a4b6fa5ea1 100644 --- a/tools/perf/tests/shell/lib/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/lib/probe_vfs_getname.sh @@ -1,12 +1,38 @@ #!/bin/bash # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 -perf probe -l 2>&1 | grep -q probe:vfs_getname +# The name of the getname_flags probe added and removed below. +# +# It is scoped to the pid so that tests running in parallel do not collide, +# and it deliberately does not start with "vfs_getname": perf trace calls +# evlist__add_vfs_getname(), which opens everything matching the hardcoded +# "probe:vfs_getname*" wildcard, so a perf trace running in another test would +# otherwise pin this probe and make the 'perf probe -d' below fail with -EBUSY. +# +# trace+probe_vfs_getname.sh is the one test that does want to be found that +# way, so it sets vfs_getname itself before sourcing this file, and is +# (exclusive) as a result. +: "${vfs_getname:=getname_flags_$$}" + +# Print the probes add_probe_vfs_getname() created. perf probe appends _1, _2, +# ... when getname_flags is inlined at more than one call site, so there can be +# several. Match them exactly rather than with a "${vfs_getname}*" glob: the +# name ends in a pid, so such a glob would also match the probes of a test +# whose pid merely starts with this one's, e.g. 123 and 1234. +probes_vfs_getname() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe:${vfs_getname}(_[[:digit:]]+)?$" +} + +[ -n "$(probes_vfs_getname)" ] had_vfs_getname=$? cleanup_probe_vfs_getname() { if [ $had_vfs_getname -eq 1 ] ; then - perf probe -q -d probe:vfs_getname* + local probe + for probe in $(probes_vfs_getname); do + perf probe -q -d "$probe" + done fi } @@ -33,8 +59,8 @@ add_probe_vfs_getname() { return 2 fi - perf probe -q "vfs_getname=getname_flags:${line} pathname=result->name:string" || \ - perf probe $add_probe_verbose "vfs_getname=getname_flags:${line} pathname=filename:ustring" || return 1 + perf probe -q "${vfs_getname}=getname_flags:${line} pathname=result->name:string" || \ + perf probe $add_probe_verbose "${vfs_getname}=getname_flags:${line} pathname=filename:ustring" || return 1 fi } diff --git a/tools/perf/tests/shell/probe_vfs_getname.sh b/tools/perf/tests/shell/probe_vfs_getname.sh index 5fe5682c28ce..05f1d50732b6 100755 --- a/tools/perf/tests/shell/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/probe_vfs_getname.sh @@ -1,6 +1,5 @@ #!/bin/bash -# Add vfs_getname probe to get syscall args filenames (exclusive) - +# Add vfs_getname probe to get syscall args filenames # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 diff --git a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh index 002f7037f182..1d4fb4a4fbfe 100755 --- a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh +++ b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Use vfs_getname probe to get syscall args filenames (exclusive) +# Use vfs_getname probe to get syscall args filenames # Uses the 'perf test shell' library to add probe:vfs_getname to the system # then use it with 'perf record' using 'touch' to write to a temp file, then @@ -17,22 +17,32 @@ skip_if_no_perf_probe || exit 2 # shellcheck source=lib/probe_vfs_getname.sh . "$(dirname "$0")/lib/probe_vfs_getname.sh" +# shellcheck disable=SC2154 # vfs_getname is assigned in lib/probe_vfs_getname.sh record_open_file() { echo "Recording open file:" # Check presence of libtraceevent support to run perf record - skip_no_probe_record_support "probe:vfs_getname*" + skip_no_probe_record_support if [ $? -eq 2 ]; then echo "WARN: Skipping test record_open_file. No libtraceevent support" return 2 fi - perf record -o ${perfdata} -e probe:vfs_getname\* touch $file + # Record every probe the inlining of getname_flags produced, naming + # them exactly rather than with a "${vfs_getname}*" glob, which would + # also match the probes of a test whose pid starts with this one's. + local events + events=$(probes_vfs_getname | paste -sd, -) + if [ -z "${events}" ] ; then + echo "FAIL: no ${vfs_getname} probe to record" + return 1 + fi + perf record -o ${perfdata} -e "${events}" touch $file } perf_script_filenames() { echo "Looking at perf.data file for vfs_getname records for the file we touched:" perf script -i ${perfdata} | \ - grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:vfs_getname[_0-9]*: +\([[:xdigit:]]+\) +pathname=\"${file}\"" + grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:${vfs_getname}(_[0-9]+)?: +\([[:xdigit:]]+\) +pathname=\"${file}\"" } add_probe_vfs_getname diff --git a/tools/perf/tests/shell/trace+probe_vfs_getname.sh b/tools/perf/tests/shell/trace+probe_vfs_getname.sh index 7a0b1145d0cd..146305f4d549 100755 --- a/tools/perf/tests/shell/trace+probe_vfs_getname.sh +++ b/tools/perf/tests/shell/trace+probe_vfs_getname.sh @@ -10,6 +10,13 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 +# This test must stay exclusive, and is the only one of the probe tests that +# does: it does not name the event it uses. perf trace discovers it with the +# hardcoded "probe:vfs_getname*" wildcard in evlist__add_vfs_getname(), so the +# probe has to carry that prefix, and a parallel run of this test would then +# also match, and pin, the probes of the other tests. The sibling tests avoid +# all of this by using a name that the wildcard cannot reach. + # shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh @@ -17,6 +24,8 @@ skip_if_no_perf_probe || exit 2 skip_if_no_perf_trace || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# shellcheck disable=SC2034 # consumed by lib/probe_vfs_getname.sh +vfs_getname="vfs_getname_$$" . "$(dirname $0)"/lib/probe_vfs_getname.sh trace_open_vfs_getname() { -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 10/14] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (8 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 09/14] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:38 ` [PATCH v2 11/14] perf test trace_summary: Improve error diagnostics Ian Rogers ` (4 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The uprobe name was not scoped to PID, and concurrent writes to `/sys/kernel/debug/tracing/uprobe_events` can occasionally return `-EBUSY` when another process holds the tracefs inode lock. Scope the probe event name with `$$` (`inet_pton_$$=inet_pton`) and add a retry loop with backoff for uprobe addition. Drop the `(exclusive)` tag so the test can run in parallel during pass 1. A PID scoped probe is no longer cleaned up by any other test, so add an EXIT/TERM/INT trap to delete it, otherwise an interrupted run leaks the uprobe into the system. The trap is installed only after the root and IPv6 checks that `exit 2` to skip the test, as trap_cleanup() exits 1 and would otherwise turn those skips into failures. Deletion enumerates the probes from `perf probe -l`, matching `^probe_libc:inet_pton_$$(_[[:digit:]]+)?$` exactly, rather than reading $event_name: a signal arriving after perf probe injected the uprobe but before the assignment completed would leave that variable empty and leak the probe, and an `inet_pton_$$*` glob would reach the probe of a test whose pid merely starts with this one's. While here use mktemp rather than mktemp -u for the temporary files: this test runs as root in a world writable /tmp, and predicting a name without creating it allows another user to win the race and plant a symlink. The perf.data check becomes -s rather than -e as mktemp now pre-creates an empty file. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../shell/record+probe_libc_inet_pton.sh | 83 ++++++++++++++----- 1 file changed, 63 insertions(+), 20 deletions(-) diff --git a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh index eca629ee83f0..3eb51426373b 100755 --- a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh +++ b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh @@ -1,5 +1,5 @@ #!/bin/bash -# probe libc's inet_pton & backtrace it with ping (exclusive) +# probe libc's inet_pton & backtrace it with ping # Installs a probe on libc's inet_pton function, that will use uprobes, # then use 'perf trace' on a ping to localhost asking for just one packet @@ -21,20 +21,30 @@ nm -Dg $libc 2>/dev/null | grep -F -q inet_pton || exit 254 event_pattern='probe_libc:inet_pton(_[[:digit:]]+)?' add_libc_inet_pton_event() { + local attempts=0 + while [ $attempts -lt 3 ]; do + event_name=$(perf probe -f -x $libc -a "inet_pton_$$=inet_pton" 2>&1 | \ + awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ + ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) + + if [ -n "$event_name" ]; then + return 0 + fi + attempts=$((attempts + 1)) + sleep 0.1 + done - event_name=$(perf probe -f -x $libc -a inet_pton 2>&1 | \ - awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ - ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) - - if [ $? -ne 0 ] || [ -z "$event_name" ] ; then - printf "FAIL: could not add event\n" - return 1 - fi + printf "FAIL: could not add event\n" + return 1 } trace_libc_inet_pton_backtrace() { - expected=`mktemp -u /tmp/expected.XXX` + # Create the files rather than just reserving names with mktemp -u: + # this runs as root and /tmp is world writable, so a predictable name + # that is written to later can be pre-created as a symlink by an + # unprivileged user and used to clobber an arbitrary file. + expected=$(mktemp /tmp/expected.XXX) echo "ping[][0-9 \.:]+$event_name: \([[:xdigit:]]+\)" > $expected echo ".*inet_pton\+0x[[:xdigit:]]+[[:space:]]\($libc|inlined\)$" >> $expected @@ -50,8 +60,8 @@ trace_libc_inet_pton_backtrace() { ;; esac - perf_data=`mktemp -u /tmp/perf.data.XXX` - perf_script=`mktemp -u /tmp/perf.script.XXX` + perf_data=$(mktemp /tmp/perf.data.XXX) + perf_script=$(mktemp /tmp/perf.script.XXX) # Check presence of libtraceevent support to run perf record skip_no_probe_record_support "$event_name/$eventattr/" @@ -61,9 +71,10 @@ trace_libc_inet_pton_backtrace() { fi perf record -e $event_name/$eventattr/ -o $perf_data ping -6 -c 1 ::1 > /dev/null 2>&1 - # check if perf data file got created in above step. - if [ ! -e $perf_data ]; then - printf "FAIL: perf record failed to create \"%s\" \n" "$perf_data" + # Check perf record actually wrote data. mktemp already created the + # file, so test that it is non-empty rather than that it exists. + if [ ! -s $perf_data ]; then + printf "FAIL: perf record failed to write \"%s\" \n" "$perf_data" return 1 fi perf script -i $perf_data | tac | grep -m1 ^ping -B9 | tac > $perf_script @@ -97,21 +108,53 @@ trace_libc_inet_pton_backtrace() { # even if the perf script output does not match. } +# Print the pid scoped uprobes this test may have created. perf probe appends +# _1, _2, ... when the name is already taken, so match those too, but anchor +# the match: an "inet_pton_$$*" glob would also match the probe of a test whose +# pid merely starts with this one's, e.g. 123 and 1234. +libc_inet_pton_events() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe_libc:inet_pton_$$(_[[:digit:]]+)?$" +} + delete_libc_inet_pton_event() { + # Ask the kernel what is actually there rather than trusting + # $event_name: a signal arriving after perf probe injected the uprobe + # but before the assignment to event_name completed would otherwise + # leave the variable empty and leak the probe. + local probe + for probe in $(libc_inet_pton_events); do + perf probe -q -d "$probe" + done +} - if [ -n "$event_name" ] ; then - perf probe -q -d $event_name - fi +cleanup() { + rm -f ${perf_data} ${perf_script} ${expected} + delete_libc_inet_pton_event + + trap - EXIT TERM INT +} + +trap_cleanup() { + cleanup + exit 1 } # Check for IPv6 interface existence ip a sh lo | grep -F -q inet6 || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# Install the trap only now that the skips above are out of the way: it exits +# 1, so arming it any earlier would turn an 'exit 2' skip into a failure. +# +# The event name is pid scoped, so unlike the old fixed name an orphan left +# behind by an interrupted run is never overwritten by a later run: it would +# stay in the kernel forever. Always clean up, including on a signal. +trap trap_cleanup EXIT TERM INT + skip_if_no_perf_probe && \ add_libc_inet_pton_event && \ trace_libc_inet_pton_backtrace err=$? -rm -f ${perf_data} ${perf_script} ${expected} -delete_libc_inet_pton_event +cleanup exit $err -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 11/14] perf test trace_summary: Improve error diagnostics 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (9 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 10/14] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers @ 2026-09-17 16:38 ` Ian Rogers 2026-09-17 16:39 ` [PATCH v2 12/14] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers ` (3 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:38 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When pattern matching fails in test_perf_trace(), print the command that failed along with the actual match count, the matching lines found, and the first 15 lines of output to aid debugging. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index b80dea77cec6..f975176247b5 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -28,10 +28,14 @@ test_perf_trace() { count=$(grep -E -c -m 3 "${search}" ${OUTPUT}) if [ "${count}" != "3" ]; then - echo "Error: cannot find enough pattern ${search} in the output" - cat ${OUTPUT} - rm -f ${OUTPUT} - exit 1 + echo "Error: cannot find enough pattern ${search} (count=${count}) in output of:" + echo "Error: perf trace ${args} -- ${workload}" + echo "Error: matched lines:" + grep -E "${search}" ${OUTPUT} || echo "none" + echo "Error: first 15 lines of output:" + head -n 15 ${OUTPUT} + rm -f ${OUTPUT} + exit 1 fi } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 12/14] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (10 preceding siblings ...) 2026-09-17 16:38 ` [PATCH v2 11/14] perf test trace_summary: Improve error diagnostics Ian Rogers @ 2026-09-17 16:39 ` Ian Rogers 2026-09-17 16:39 ` [PATCH v2 13/14] perf test trace_summary: Make non-exclusive Ian Rogers ` (2 subsequent siblings) 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:39 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_btf_general.sh used `--max-events=1` with `perf trace` on commands such as `mv`, `echo`, and `sleep`. When background activity occurs or tests run in parallel, `perf trace` can capture an event from an unrelated process and exit prematurely before recording the target command's syscalls. Drop `--max-events=1` and let tracing run until the command completes, checking for the expected output with grep (matching trace_btf_enum.sh). Remove the (exclusive) tag so the test runs in parallel. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_btf_general.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_btf_general.sh b/tools/perf/tests/shell/trace_btf_general.sh index 7a94a5743924..4d654b687a4e 100755 --- a/tools/perf/tests/shell/trace_btf_general.sh +++ b/tools/perf/tests/shell/trace_btf_general.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace BTF general tests (exclusive) +# perf trace BTF general tests # SPDX-License-Identifier: GPL-2.0 err=0 @@ -27,7 +27,7 @@ check_vmlinux() { trace_test_string() { echo "Testing perf trace's string augmentation" - output="$(perf trace --sort-events -e renameat* --max-events=1 -- mv ${file1} ${file2} 2>&1)" + output="$(perf trace --sort-events -e renameat* -- mv ${file1} ${file2} 2>&1)" if ! echo "$output" | grep -q -E "^mv/[0-9]+ renameat(2)?\(.*, \"${file1}\", .*, \"${file2}\", .*\) += +[0-9]+$" then printf "String augmentation test failed, output:\n$output\n" @@ -38,7 +38,7 @@ trace_test_string() { trace_test_buffer() { echo "Testing perf trace's buffer augmentation" # echo will insert a newline (\10) at the end of the buffer - output="$(perf trace --sort-events -e write --max-events=1 -- echo "${buffer}" 2>&1)" + output="$(perf trace --sort-events -e write -- echo "${buffer}" 2>&1)" if ! echo "$output" | grep -q -E "^echo/[0-9]+ write\([0-9]+, ${buffer}.*, [0-9]+\) += +[0-9]+$" then printf "Buffer augmentation test failed, output:\n$output\n" @@ -48,7 +48,7 @@ trace_test_buffer() { trace_test_struct_btf() { echo "Testing perf trace's struct augmentation" - output="$(perf trace --sort-events -e clock_nanosleep --force-btf --max-events=1 -- sleep 1 2>&1)" + output="$(perf trace --sort-events -e clock_nanosleep --force-btf -- sleep 1 2>&1)" if ! echo "$output" | grep -q -E "^sleep/[0-9]+ clock_nanosleep\(0, 0, \{1,.*\}, 0x[0-9a-f]+\) += +[0-9]+$" then printf "BTF struct augmentation test failed, output:\n$output\n" -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 13/14] perf test trace_summary: Make non-exclusive 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (11 preceding siblings ...) 2026-09-17 16:39 ` [PATCH v2 12/14] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers @ 2026-09-17 16:39 ` Ian Rogers 2026-09-17 16:39 ` [PATCH v2 14/14] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:39 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_summary.sh tests various summary modes of `perf trace`. It already directs output to a unique temporary file without polluting the current working directory. Remove the (exclusive) tag so it can run concurrently in parallel test runs. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index f975176247b5..e2834bc4eee6 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace summary (exclusive) +# perf trace summary # SPDX-License-Identifier: GPL-2.0 # Check that perf trace works with various summary mode -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v2 14/14] perf test uprobe_from_different_cu: Scope probe name to PID 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (12 preceding siblings ...) 2026-09-17 16:39 ` [PATCH v2 13/14] perf test trace_summary: Make non-exclusive Ian Rogers @ 2026-09-17 16:39 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 14 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-17 16:39 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The test builds a binary in a per-run temporary directory and probes its foo function. The directory name is unique, but perf probe derives the event name from the probed function and the group name from the binary's basename, so every run registers the same probe_testfile:foo event. Running the test concurrently with itself, as 'perf test -r3' does, therefore fails in all but one of the runs with: Error: event "foo" already exists. Hint: Remove existing event by 'perf probe -d' and a losing run's cleanup goes on to delete the winning run's probe out from under it. Name the event after the pid, foo_$$, so that parallel runs no longer collide. This lets the test stay in the parallel pass rather than having to be marked (exclusive). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/test_uprobe_from_different_cu.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh index 7adf9755d6de..47c99d93436b 100755 --- a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh +++ b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh @@ -18,12 +18,19 @@ fi temp_dir=$(mktemp -d /tmp/perf-uprobe-different-cu-sh.XXXXXXXXXX) +# The name of the uprobe added and removed below. The probe is placed on +# ${temp_dir}/testfile, but perf probe derives the event name from the probed +# function and the group name from the binary's basename, so every run would +# otherwise share one probe_testfile:foo event, and a concurrent run would +# fail with 'event "foo" already exists'. Scope the event name to the pid. +probe_name="foo_$$" + cleanup() { trap - EXIT TERM INT if [[ "${temp_dir}" =~ ^/tmp/perf-uprobe-different-cu-sh.*$ ]]; then echo "--- Cleaning up ---" - perf probe -x ${temp_dir}/testfile -d foo || true + perf probe -x ${temp_dir}/testfile -d ${probe_name} || true rm -f "${temp_dir}/"* rmdir "${temp_dir}" fi @@ -84,6 +91,6 @@ gcc -g -Og -c ${temp_dir}/testfile-main.c -o ${temp_dir}/testfile-main.o gcc -g -Og -o ${temp_dir}/testfile ${temp_dir}/testfile-foo.o ${temp_dir}/testfile-main.o perf probe -x ${temp_dir}/testfile --funcs foo | grep "foo" -perf probe -x ${temp_dir}/testfile foo +perf probe -x ${temp_dir}/testfile ${probe_name}=foo cleanup -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (13 preceding siblings ...) 2026-09-17 16:39 ` [PATCH v2 14/14] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 01/16] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers ` (16 more replies) 14 siblings, 17 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz perf trace's BPF augmentation attaches to raw_syscalls:sys_enter and raw_syscalls:sys_exit system wide, and used the program return value to decide whether a syscall was interesting. Returning 0 from a BPF_PROG_TYPE_TRACEPOINT program makes perf_trace_run_bpf_submit() drop the event for every listener on that tracepoint, not just for the perf trace that installed the program. Any concurrent perf trace, perf record or ftrace session watching raw_syscalls therefore lost events, which is one of the reasons so many of the perf trace and perf probe shell tests had to be marked (exclusive) and run on their own. Patches 1 and 2 are independent fixes to the files the rest of the series goes on to rework. Patches 3 to 8 fix perf trace. They stop the return value being used as a filter and do the filtering in BPF maps instead, fix argument handling for the __data_loc internal tracepoint fields that syscalls:sys_enter_* gained in 6.19, stop the sys_exit program array tail calling a sys_enter augmenter, and replace the userspace PERF_RECORD_FORK/PERF_RECORD_EXIT bookkeeping with BTF-typed raw tracepoint programs on sched_process_{fork,exit,exec}. A task is then registered before its first syscall and evicted in do_exit(), rather than whenever userspace next drains the ring buffer. Patches 9 to 16 deal with the tests. Several collided with each other through global state rather than through perf trace: fixed probe names, clear_all_probes() disabling every tracepoint on the system, and perf trace's hardcoded "probe:vfs_getname*" wildcard pinning probes belonging to other tests. With those scoped to a pid they can drop (exclusive) and run in parallel again. Tested on x86_64. The trace and probe tests pass under 'perf test -r3', which runs the repeats concurrently. Every patch builds individually, and the series also builds with BUILD_BPF_SKEL=0. Changes since v2: - Rebased onto the current perf-tools-next. - Patch 1 also includes <assert.h>, for the assert() in augmented_syscalls__create_bpf_output(). - New patch 2 makes evsel__put_and_free_priv() free the whole evsel_trace. It only zfree()d the struct, leaking the syscall_arg_fmt array hanging off it. No caller can reach that today, but patch 6 adds one that discards a fully set up evsel. - Patch 6 identifies the evsel to drop from the evlist by comparing against trace.syscalls.events.sys_enter instead of a strstr() for "syscalls:sys_enter". That substring also matches the per syscall syscalls:sys_enter_SYSCALL tracepoints, so a user asking for one of those by name would have had it removed from the evlist, and __augmented_syscalls__ described with its tracefs format rather than the raw tracepoint's. - Patch 6 reports a failure to program the pid filters with the error that caused it. trace__run() sent everything trace__set_filter_pids() returned to out_error_mem, which prints "Not enough memory to run!". That was already a guess, and a wrong one now the function writes BPF maps too: the update fails with -E2BIG when the target has more threads than pids_to_trace has room for. - Patch 6 reports a failure to program the syscall filters the same way. The pr_err() in trace__set_ev_qualifier_filter() ran before trace__run() printed "%m" at out_errno, and anything called in between could have changed errno by then, so the two did not have to agree. The inner report is now a pr_debug() and the caller prints the error it was given, which leaves out_errno without a user. - Patch 7 moves the pid across in sched_process_exec() by deleting the old key before inserting the new one. The move is only a rename, but holding both keys at once needs a spare slot, and on a full map the insert failed with -E2BIG while the delete still succeeded, losing the task instead of moving it. - Patch 7 no longer claims that a task forked during the attach window is still traced unaugmented. That was wrong: cmd_trace() removes the sys_enter evsel once the bpf-output event exists, so __augmented_syscalls__ is the only source of enter events and such a task is not reported at all. Describe what is really lost, why the tgid fallback is not kept as a safety net for it, note that only 'perf trace -p' is exposed, and that closing it needs the target's descendants re-enumerated from /proc after the attach. - Patch 7 drops the parent tgid test from sched_process_fork(), so a child inherits from the pid of the thread that called clone() and from nothing else. The lookups only ever match a task's own pid, and every thread of a -p target is enumerated from /proc/<pid>/task and inserted in its own right, so the tgid added no reach. What it did add was a child inheriting from a thread that is not traced itself, such as a sibling of the thread 'perf trace -t' selected. - Patch 7 ends the session with the error that caused it when the BPF programs cannot be attached, rather than printing a bare errno left over from unwinding the attach. There is nothing to fall back to at that point, cmd_trace() has already built the evlist around __augmented_syscalls__ and dropped the sys_enter evsel, and the code being replaced did not fall back either: it ignored the result of augmented_raw_syscalls_bpf__attach() altogether. - New patch 8 reads the target out of /proc again once the BPF programs are attached, so a task the target created while perf trace was starting up is traced rather than missed for the whole session. This is the gap patch 7 describes and left for later. It covers the target's new threads and, through task->children, anything it or they forked, to any depth. What is left is a task that made syscalls between sys_enter going live and being added to the map, which is momentary rather than lasting for the session. - Patch 9 bails out if mktemp fails rather than carrying on with an empty $tmpdir. cd rejects the null directory, and the rmdir that was meant to undo the mktemp then failed to remove '' instead. Its cleanup() also no longer exits when it cannot cd out of the temporary directory, which would have skipped removing it. The removal takes an absolute path and does not need the cd to have succeeded. - Patch 10 now narrows the disable in clear_all_probes() to the probes themselves rather than dropping it. Clearing kprobe_events or uprobe_events is all or nothing: dyn_events_release_all() returns -EBUSY without removing anything if it finds a probe that is still enabled, so simply removing the write could leave stale probes behind to collide with the next run. The set to disable is read from the kprobe_events and uprobe_events listings rather than assumed to be the groups perf uses, since a probe left enabled in another group, such as the default kprobes group used when kprobe_events is written directly, would abort the clear just the same. - Patch 11 deletes the probes from an exit trap as well as on the way out. A pid scoped name is never seen again, so a run interrupted before cleanup_probe_vfs_getname() left its probes behind for good, a set per run, where the fixed name was at least found and reused by the next run. - Patch 12 retries deleting the uprobe. Deletion writes uprobe_events just as addition does and can lose the same race with a concurrent test, and because the event name is now pid scoped a probe left behind is never overwritten by a later run. It also bails out if mktemp fails and quotes the path in the emptiness check, which unquoted would have tested the string "-s" and reported success. - Patch 13 prints the tail of the output rather than the head. The pattern it matches only appears in the summary, which is printed after any trace output, so the head of the file is not the part that failed to match. Changes since v1: - New patch 1 includes <sys/types.h> and <string.h> for the pid_t and strcmp() uses that were relying on the include chain happening to drag them in, which does not hold on libcs such as musl. - Patch 6 no longer returns success when the event qualifier filter string fails to allocate. err now defaults to 0 because either tracepoint may legitimately be absent, so the ENOMEM path has to set the error itself rather than rely on that default. It also includes <stdbool.h> for the bool parameters it adds to trace_augment.h. - Patch 9 removes the temporary directory if the cd into it fails. That happens before the cleanup trap is installed, so the directory would otherwise be left behind in /tmp. Ian Rogers (16): perf trace: Include the headers declaring pid_t, strcmp and assert perf trace: Free the whole evsel_trace in evsel__put_and_free_priv perf trace: Start BPF summary before starting workload perf trace: Skip internal tracepoint fields in formatting and beauty map perf trace: Do not set unaugmented BPF program on sys_exit map perf trace: Filter events in BPF and avoid tracepoint vetoes perf trace: Handle fork and exit directly in BPF filter maps perf trace: Enumerate the target again once BPF is attached perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive perf test common: Only disable probes in clear_all_probes perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive perf test trace_summary: Improve error diagnostics perf test trace_btf_general: Drop --max-events=1 and make non-exclusive perf test trace_summary: Make non-exclusive perf test uprobe_from_different_cu: Scope probe name to PID tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 637 +++++++++++++++--- tools/perf/tests/shell/common/init.sh | 33 +- .../perf/tests/shell/lib/probe_vfs_getname.sh | 51 +- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+probe_libc_inet_pton.sh | 107 ++- .../shell/record+script_probe_vfs_getname.sh | 18 +- tools/perf/tests/shell/test_task_analyzer.sh | 20 +- .../shell/test_uprobe_from_different_cu.sh | 11 +- .../tests/shell/trace+probe_vfs_getname.sh | 9 + tools/perf/tests/shell/trace_btf_general.sh | 8 +- tools/perf/tests/shell/trace_summary.sh | 16 +- .../bpf_skel/augmented_raw_syscalls.bpf.c | 312 ++++++++- tools/perf/util/bpf_trace_augment.c | 175 ++++- tools/perf/util/trace_augment.h | 27 +- 15 files changed, 1281 insertions(+), 151 deletions(-) base-commit: 86a27811675a415bd351efca1a194a1a94c082dd -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 01/16] perf trace: Include the headers declaring pid_t, strcmp and assert 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 02/16] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers ` (15 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_augment.h uses pid_t in the augmented_syscalls__set_filter_pids() prototype and in the !HAVE_BPF_SKEL stub. bpf_trace_augment.c calls strcmp() in augmented_syscalls__find_by_title() and assert() in augmented_syscalls__create_bpf_output(), but neither pulls in the header that declares what it uses. Both happen to build today only because something else in the include chain drags <sys/types.h>, <string.h> and <assert.h> in first, which is not guaranteed and does not hold on libcs such as musl that keep the POSIX namespaces strictly separated. Include <sys/types.h>, <string.h> and <assert.h> explicitly. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/util/bpf_trace_augment.c | 2 ++ tools/perf/util/trace_augment.h | 1 + 2 files changed, 3 insertions(+) diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index a9cf2a77ded1..ebb26225fb04 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,7 @@ +#include <assert.h> #include <bpf/libbpf.h> #include <internal/xyarray.h> +#include <string.h> #include "bpf_skel/augmented_raw_syscalls.skel.h" #include "debug.h" diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 4f729bc67753..a1cd9a5e0213 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <sys/types.h> struct bpf_program; struct evlist; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 02/16] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-18 14:06 ` [PATCH v3 01/16] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 03/16] perf trace: Start BPF summary before starting workload Ian Rogers ` (14 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Every evsel->priv in builtin-trace.c is a struct evsel_trace, allocated by evsel_trace__new(). It holds a syscall_arg_fmt array in its fmt member, which evsel__syscall_arg_fmt() allocates on demand for the syscalls:sys_{enter,exit}_SYSCALL tracepoints and for every other tracepoint that gets its arguments pretty printed. evsel__put_and_free_priv() only did zfree(&evsel->priv), releasing the evsel_trace itself and leaking that array. Use evsel_trace__delete(), which frees fmt first, exactly as the out_delete path of evsel__syscall_arg_fmt() already does. The current callers are all error paths that run before fmt can have been allocated, so nothing leaks in practice today, but the helper is the obvious thing to reach for whenever an evsel is discarded and it should be safe for that. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 20fffc24507b..f67557e7a254 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -464,7 +464,13 @@ static int evsel__init_tp_ptr_field(struct evsel *evsel, struct tp_field *field, static void evsel__put_and_free_priv(struct evsel *evsel) { - zfree(&evsel->priv); + /* + * evsel->priv is always a struct evsel_trace here, so it has to go + * through evsel_trace__delete(): zfree() on its own would release the + * struct while leaking the syscall_arg_fmt array hanging off it. + */ + evsel_trace__delete(evsel->priv); + evsel->priv = NULL; evsel__put(evsel); } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 03/16] perf trace: Start BPF summary before starting workload 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-18 14:06 ` [PATCH v3 01/16] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers 2026-09-18 14:06 ` [PATCH v3 02/16] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 04/16] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers ` (13 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When using --bpf-summary, trace_start_bpf_summary() sets skel->bss->enabled = 1. In trace__run(), trace_start_bpf_summary() was previously invoked after evlist__start_workload(). Because evlist__start_workload() immediately unblocks the child process by writing to its go_pipe, short-lived workloads (such as `cat /dev/null`) can execute and complete their initial system calls before trace_start_bpf_summary() is reached by the parent process. Furthermore, under high system load, the child process may finish before the BPF summary tracking is enabled in the kernel at all, causing syscall summary tests to fail. Additionally, if initial_delay was configured, the workload was started before sleeping. Move trace_start_bpf_summary() to be invoked before evlist__start_workload(), matching evlist__enable(), and ensure it respects target.initial_delay. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f67557e7a254..f90c6bb4d8b4 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4844,17 +4844,19 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (!target__none(&trace->opts.target) && !trace->opts.target.initial_delay) evlist__enable(evlist); + if (trace->summary_bpf && !trace->opts.target.initial_delay) + trace_start_bpf_summary(); + if (forks) evlist__start_workload(evlist); if (trace->opts.target.initial_delay) { usleep(trace->opts.target.initial_delay * 1000); evlist__enable(evlist); + if (trace->summary_bpf) + trace_start_bpf_summary(); } - if (trace->summary_bpf) - trace_start_bpf_summary(); - trace->multiple_threads = perf_thread_map__pid(evlist__core(evlist)->threads, 0) == -1 || perf_thread_map__nr(evlist__core(evlist)->threads) > 1 || evlist__first(evlist)->core.attr.inherit; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 04/16] perf trace: Skip internal tracepoint fields in formatting and beauty map 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (2 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 03/16] perf trace: Start BPF summary before starting workload Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 05/16] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers ` (12 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Linux 6.19+ added __data_loc char[] internal fields for string arguments in syscalls:sys_enter_<name> tracepoints (e.g., __data_loc_oldname in sys_enter_renameat2). While is_internal_field() was added to detect them, several places did not properly account for them: 1. In syscall_arg_fmt__init_array(), when an internal field was skipped, the arg pointer was still incremented, causing the subsequent argument formatters to be mismatched. 2. In syscall__scnprintf_args(), internal fields were not skipped, causing spurious trailing arguments like ", 0, 16" to be formatted and printed. 3. In trace__bpf_sys_enter_beauty_map(), internal fields were not skipped, offsetting beauty array argument indices and breaking string and buffer augmentation. 4. In trace__find_usable_bpf_prog_entry(), candidate pointer checks matched on internal pointer fields, breaking signature compatibility matching between syscalls for augmenter sharing. Introduce next_user_arg() and advance both cursors with it, so that the two argument lists are always compared at a real argument and the walk ends when one syscall runs out of arguments rather than when one happens to have trailing internal fields. 5. syscall__augmented_args() computed the augmented payload as sample->raw_size - sc->args_size for any sys_enter style sample. sc->args_size deliberately stops at the last non-internal field, so on 6.19+ a native syscalls:sys_enter_<name> record leaves the __data_loc words and their string payloads in the remainder. Those bytes are not a struct augmented_arg, so syscall_arg__scnprintf_augmented_string() read a bogus length and walked arg->augmented.args out of bounds. This is reachable from trace__event_handler(), which calls trace__fprintf_sys_enter() for any evsel whose tracepoint name starts with "sys_enter_". 6. In syscall__read_info(), syscall__alloc_arg_fmts() was called before checking and dropping the leading __syscall_nr (or nr) field, using nr_fields - 1 unconditionally. If a tracepoint format lacks that leading field, the allocated arg_fmt array is one entry too small and syscall_arg_fmt__init_array() writes one entry past the end of the heap buffer. Drop __syscall_nr/nr first and size the allocation from the remaining fields. Update these functions to check and skip is_internal_field() so that arguments are correctly formatted and beauty map entries match the expected syscall signatures, restrict syscall__augmented_args() to the __augmented_syscalls__ bpf-output evsel, and size arg_fmt after dropping the syscall number field. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 153 ++++++++++++++++++++++++++++--------- 1 file changed, 115 insertions(+), 38 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f90c6bb4d8b4..2fbe1bca511c 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2283,15 +2283,20 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field struct tep_format_field *last_field = NULL; int len; - for (; field; field = field->next, ++arg) { - /* assume it's the last argument */ + for (; field; field = field->next) { + /* + * Skip internal tracepoint fields (e.g., __data_loc strings in + * Linux 6.19+) so they do not advance the syscall arg array index. + */ if (is_internal_field(field)) continue; last_field = field; - if (arg->scnprintf) + if (arg->scnprintf) { + ++arg; continue; + } len = strlen(field->name); @@ -2348,6 +2353,7 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field } } } + ++arg; } return last_field; @@ -2369,6 +2375,7 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) char tp_name[128]; const char *name; struct tep_format_field *field; + int nr_args; int err; if (sc->nonexistent) @@ -2407,24 +2414,25 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) return err; } - /* - * The tracepoint format contains __syscall_nr field, so it's one more - * than the actual number of syscall arguments. - */ - if (syscall__alloc_arg_fmts(sc, sc->tp_format->format.nr_fields - 1)) - return -ENOMEM; - sc->args = sc->tp_format->format.fields; + nr_args = sc->tp_format->format.nr_fields; /* * We need to check and discard the first variable '__syscall_nr' * or 'nr' that mean the syscall number. It is needless here. * So drop '__syscall_nr' or 'nr' field but does not exist on older kernels. + * + * Do this before allocating, and size the array from what is left, so + * that a format without the field does not leave + * syscall_arg_fmt__init_array() walking one entry past the end. */ if (sc->args && (!strcmp(sc->args->name, "__syscall_nr") || !strcmp(sc->args->name, "nr"))) { sc->args = sc->args->next; - --sc->nr_args; + --nr_args; } + if (syscall__alloc_arg_fmts(sc, nr_args)) + return -ENOMEM; + field = sc->args; while (field) { if (is_internal_field(field)) @@ -2642,11 +2650,17 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, if (sc->args != NULL) { struct tep_format_field *field; - for (field = sc->args; field; - field = field->next, ++arg.idx, bit <<= 1) { - if (arg.mask & bit) + for (field = sc->args; field; field = field->next) { + /* Skip internal fields so they are not printed as spurious arguments */ + if (is_internal_field(field)) continue; + if (arg.mask & bit) { + ++arg.idx; + bit <<= 1; + continue; + } + arg.fmt = &sc->arg_fmt[arg.idx]; val = syscall_arg__val(&arg, arg.idx); /* @@ -2664,8 +2678,11 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, */ if (val == 0 && !trace->show_zeros && !(sc->arg_fmt && sc->arg_fmt[arg.idx].show_zero) && - !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) + !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) { + ++arg.idx; + bit <<= 1; continue; + } printed += scnprintf(bf + printed, size - printed, "%s", printed ? ", " : ""); @@ -2680,12 +2697,16 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, size - printed, val, field->type); if (btf_printed) { printed += btf_printed; + ++arg.idx; + bit <<= 1; continue; } } printed += syscall_arg_fmt__scnprintf_val(&sc->arg_fmt[arg.idx], bf + printed, size - printed, &arg, val); + ++arg.idx; + bit <<= 1; } } else if (IS_ERR(sc->tp_format)) { /* @@ -2946,7 +2967,9 @@ static int trace__fprintf_sample(struct trace *trace, struct perf_sample *sample return printed; } -static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sample, int *augmented_args_size, int raw_augmented_args_size) +static void *syscall__augmented_args(struct trace *trace, struct syscall *sc, + struct perf_sample *sample, + int *augmented_args_size, int raw_augmented_args_size) { /* * For now with BPF raw_augmented we hook into raw_syscalls:sys_enter @@ -2964,6 +2987,24 @@ static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sam */ int args_size = raw_augmented_args_size ?: sc->args_size; + /* + * Augmented arguments are a perf trace specific payload, they are only + * ever appended to samples emitted by the BPF __augmented_syscalls__ + * bpf-output event. + * + * Native syscalls:sys_enter_NAME tracepoints may also carry trailing + * data of their own: since Linux 6.19 they append __data_loc char[] + * fields plus the string payloads they point at. Those bytes are not a + * struct augmented_arg, so treating them as one would make + * syscall_arg__scnprintf_augmented_string() read a bogus length and + * walk arg->augmented.args far out of bounds. + * + * So only look for augmented arguments on the event that can actually + * produce them. + */ + if (sample->evsel != trace->syscalls.events.bpf_output) + return NULL; + *augmented_args_size = sample->raw_size - args_size; if (*augmented_args_size > 0) { static uintptr_t argbuf[1024]; /* assuming single-threaded */ @@ -3022,17 +3063,13 @@ static int trace__sys_enter(struct trace *trace, if (!(trace->duration_filter || trace->summary_only || trace->min_stack)) trace__printf_interrupted_entry(trace); /* - * If this is raw_syscalls.sys_enter, then it always comes with the 6 possible - * arguments, even if the syscall being handled, say "openat", uses only 4 arguments - * this breaks syscall__augmented_args() check for augmented args, as we calculate - * syscall->args_size using each syscalls:sys_enter_NAME tracefs format file, - * so when handling, say the openat syscall, we end up getting 6 args for the - * raw_syscalls:sys_enter event, when we expected just 4, we end up mistakenly - * thinking that the extra 2 u64 args are the augmented filename, so just check - * here and avoid using augmented syscalls when the evsel is the raw_syscalls one. + * syscall__augmented_args() only returns a payload for the BPF + * __augmented_syscalls__ event, so raw_syscalls:sys_enter (which always + * carries all 6 possible arguments rather than sc->args_size worth) and + * the native syscalls:sys_enter_NAME tracepoints are both handled there. */ - if (evsel != trace->syscalls.events.sys_enter) - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); ttrace->entry_time = sample->time; ttrace->entry_cpu = sample->cpu; msg = ttrace->entry_str; @@ -3077,7 +3114,7 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam struct syscall *sc; char msg[1024]; void *args, *augmented_args = NULL; - int augmented_args_size, e_machine; + int augmented_args_size = 0, e_machine; size_t printed = 0; @@ -3095,7 +3132,8 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam goto out_put; args = perf_evsel__sc_tp_ptr(args, sample); - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); printed += syscall__scnprintf_args(sc, msg, sizeof(msg), args, augmented_args, augmented_args_size, trace, thread); fprintf(trace->output, "%.*s", (int)printed, msg); err = 0; @@ -4127,10 +4165,16 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i if (trace->btf == NULL) return -1; - for (i = 0, field = sc->args; field; ++i, field = field->next) { + for (i = 0, field = sc->args; field; field = field->next) { + /* Skip internal fields to keep beauty array index aligned with syscall arguments */ + if (is_internal_field(field)) + continue; + // XXX We're only collecting pointer payloads _from_ user space - if (!sc->arg_fmt[i].from_user) + if (!sc->arg_fmt[i].from_user) { + ++i; continue; + } struct_offset = strstr(field->type, "struct "); if (struct_offset == NULL) @@ -4149,8 +4193,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i name[cnt] = '\0'; /* cache struct's btf_type and type_id */ - if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) + if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) { + ++i; continue; + } bt = sc->arg_fmt[i].type; beauty_array[i] = bt->size; @@ -4176,7 +4222,9 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i struct tep_format_field *field_tmp; /* find the size of the buffer that appears in pairs with buf */ - for (j = 0, field_tmp = sc->args; field_tmp; ++j, field_tmp = field_tmp->next) { + for (j = 0, field_tmp = sc->args; field_tmp; field_tmp = field_tmp->next) { + if (is_internal_field(field_tmp)) + continue; if (!(field_tmp->flags & TEP_FIELD_IS_POINTER) && /* only integers */ (strstr(field_tmp->name, "count") || strstr(field_tmp->name, "siz") || /* size, bufsiz */ @@ -4186,8 +4234,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i can_augment = true; break; } + ++j; } } + ++i; } if (can_augment) @@ -4196,6 +4246,19 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i return -1; } +/* + * Advance to the first field that is a real syscall argument, so that callers + * walking two argument lists in step never have to reason about internal + * fields appearing in one list but not the other. + */ +static struct tep_format_field *next_user_arg(struct tep_format_field *field) +{ + while (field && is_internal_field(field)) + field = field->next; + + return field; +} + static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace, struct syscall *sc) { @@ -4203,7 +4266,7 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace /* * We're only interested in syscalls that have a pointer: */ - for (field = sc->args; field; field = field->next) { + for (field = next_user_arg(sc->args); field; field = next_user_arg(field->next)) { if (field->flags & TEP_FIELD_IS_POINTER) goto try_to_find_pair; } @@ -4221,21 +4284,31 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace pair->bpf_prog.sys_enter == unaugmented_prog) continue; - for (field = sc->args, candidate_field = pair->args; - field && candidate_field; field = field->next, candidate_field = candidate_field->next) { + /* + * Both cursors only ever point at real arguments, so the loop + * ends when one of the two syscalls runs out of them, rather + * than when one happens to have trailing internal fields. + */ + field = next_user_arg(sc->args); + candidate_field = next_user_arg(pair->args); + while (field && candidate_field) { bool is_pointer = field->flags & TEP_FIELD_IS_POINTER, candidate_is_pointer = candidate_field->flags & TEP_FIELD_IS_POINTER; if (is_pointer) { - if (!candidate_is_pointer) { + if (!candidate_is_pointer) { // The candidate just doesn't copies our pointer arg, might copy other pointers we want. + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; - } + } } else { if (candidate_is_pointer) { // The candidate might copy a pointer we don't have, skip it. goto next_candidate; } + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; } @@ -4256,6 +4329,8 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace goto next_candidate; is_candidate = true; + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); } if (!is_candidate) @@ -4267,7 +4342,9 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace * more than what is common to the two syscalls. */ if (candidate_field) { - for (candidate_field = candidate_field->next; candidate_field; candidate_field = candidate_field->next) + candidate_field = next_user_arg(candidate_field->next); + for (; candidate_field; + candidate_field = next_user_arg(candidate_field->next)) if (candidate_field->flags & TEP_FIELD_IS_POINTER) goto next_candidate; } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 05/16] perf trace: Do not set unaugmented BPF program on sys_exit map 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (3 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 04/16] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 06/16] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers ` (11 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz In trace__init_syscalls_bpf_prog_array_maps(), the BPF program array map for sys_exit (syscalls_sys_exit) was populated with the result of trace__bpf_prog_sys_exit_fd(). When a syscall had no specific exit augmenter, trace__find_syscall_bpf_prog() fell back to unaugmented_prog (syscall_unaugmented). However, syscall_unaugmented is a sys_enter program that outputs enter arguments to __augmented_syscalls__. As a consequence, when an unaugmented syscall exited, sys_exit tail-called syscall_unaugmented, which interpreted the exit arguments as enter arguments and emitted a duplicate, corrupt sys_enter event into __augmented_syscalls__ right as the syscall completed. Fix this by: 1. Returning NULL from trace__find_syscall_bpf_prog() when looking up exit augmenters and none is found. 2. Returning -1 from trace__bpf_prog_sys_exit_fd() when no exit program is present. 3. Only updating map_exit_fd when prog_fd >= 0. 4. Clearing err = 0 when trace__bpf_sys_enter_beauty_map() returns non-zero (indicating the syscall has no augmentable pointer arguments) before continuing the loop, so a trailing run of such syscalls (e.g. 'perf trace -e close') does not leave err non-zero on return and abort the session. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 2fbe1bca511c..2c62d38b19ac 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4123,7 +4123,12 @@ static struct bpf_program *trace__find_syscall_bpf_prog(struct trace *trace __ma pr_debug("Couldn't find BPF prog \"%s\" to associate with syscalls:sys_%s_%s, not augmenting it\n", prog_name, type, sc->name); out_unaugmented: - return unaugmented_prog; + /* + * Do not set unaugmented_prog for exit: syscall_unaugmented is a + * sys_enter program that outputs enter arguments. Exit without a + * specialized return augmenter returns 1 directly from sys_exit. + */ + return !strcmp(type, "exit") ? NULL : unaugmented_prog; } static void trace__init_syscall_bpf_progs(struct trace *trace, int e_machine, int id) @@ -4146,7 +4151,7 @@ static int trace__bpf_prog_sys_enter_fd(struct trace *trace, int e_machine, int static int trace__bpf_prog_sys_exit_fd(struct trace *trace, int e_machine, int id) { struct syscall *sc = trace__syscall_info(trace, NULL, e_machine, id); - return sc ? bpf_program__fd(sc->bpf_prog.sys_exit) : bpf_program__fd(unaugmented_prog); + return sc && sc->bpf_prog.sys_exit ? bpf_program__fd(sc->bpf_prog.sys_exit) : -1; } static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, int key, unsigned int *beauty_array) @@ -4401,16 +4406,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace, int e_m err = bpf_map_update_elem(map_enter_fd, &key, &prog_fd, BPF_ANY); if (err) break; + /* Only update the exit prog array map if an exit augmenter exists */ prog_fd = trace__bpf_prog_sys_exit_fd(trace, e_machine, key); - err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); - if (err) - break; + if (prog_fd >= 0) { + err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); + if (err) + break; + } /* use beauty_map to tell BPF how many bytes to collect, set beauty_map's value here */ memset(beauty_array, 0, sizeof(beauty_array)); err = trace__bpf_sys_enter_beauty_map(trace, e_machine, key, (unsigned int *)beauty_array); - if (err) + if (err) { + /* + * Not a failure: the syscall just has no augmentable + * arguments. Clear err, or a trailing run of such + * syscalls, e.g. all of them for 'perf trace -e close', + * would leave it set on return and abort the session. + */ + err = 0; continue; + } err = bpf_map_update_elem(beauty_map_fd, &key, beauty_array, BPF_ANY); if (err) break; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 06/16] perf trace: Filter events in BPF and avoid tracepoint vetoes 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (4 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 05/16] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 07/16] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers ` (10 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The BPF augmented_raw_syscalls sys_enter and sys_exit programs returned 0 for syscalls that were not of interest. Returning 0 from a tracepoint BPF program vetoes the event for the whole system, so an unrelated concurrent perf trace, perf record or ftrace session listening to raw_syscalls would silently lose events. This is a cross-session side effect and shows up as flaky failures when perf tests run in parallel. Furthermore, syscall_unaugmented previously returned 1 without writing anything to the __augmented_syscalls__ ring buffer. This forced userspace perf trace to listen to both raw_syscalls:sys_enter and __augmented_syscalls__ in its evlist, requiring userspace event deduplication. Address these issues: 1. In augmented_raw_syscalls.bpf.c, never return 0 from tracepoint handlers: return 1 so non-traced syscalls pass through without vetoing other concurrent listeners. 2. Introduce pids_to_trace and syscalls_to_trace BPF hash maps to perform targeted filtering directly in BPF. Unselected syscalls or PIDs return 1 immediately without writing to the buffer. 3. In syscall_unaugmented, output the unaugmented enter payload into __augmented_syscalls__ and return 1. Change its section from SEC("tp/raw_syscalls/sys_enter") to SEC("tp/syscalls/sys_enter_unaugmented") so libbpf does not attempt to auto-attach it to raw_syscalls:sys_enter. 4. In bpf_trace_augment.c, add helpers to configure target PIDs and syscalls in the BPF maps, setting the activation flags (has_pids_to_trace, has_syscalls_to_trace) only after the maps are fully populated so already-attached BPF programs do not filter against a half-filled map. Explicitly attach only sys_enter and sys_exit via an attach_prog() helper that saves -errno before calling pr_debug() or bpf_link__destroy(). Destroy the skeleton on every failure path. Leaving a loaded but unusable skeleton behind is not inert: the setters called later from trace__run() would program its maps, and a partial attach would leave a BPF program live on raw_syscalls for a session that never starts. Since augmented_syscalls__{prepare,create_bpf_output}() failures fall back to unaugmented tracing rather than aborting, those setters have to become no-ops, which they only do once skel is NULL again. errno is used directly here, so include <errno.h> rather than relying on it arriving via another header, which it does not under musl. 5. In builtin-trace.c, hook trace__set_ev_qualifier_filter() and PID filtering into the BPF maps. When __augmented_syscalls__ is active, remove raw_syscalls:sys_enter from trace.evlist since all traced enter events (both augmented and unaugmented) are now emitted by BPF into __augmented_syscalls__. Identify that evsel by comparing against trace.syscalls.events.sys_enter rather than by a strstr() of its name. The substring "syscalls:sys_enter" also matches the per syscall syscalls:sys_enter_SYSCALL tracepoints, which a user can ask for by name, and now that the match decides what is taken out of the evlist, claiming one of those would drop an event that was asked for and would describe __augmented_syscalls__ with its format rather than the raw one's. Restore tracking on the remaining evsel via evlist__set_tracking_event() so PERF_RECORD_COMM and tracking events continue to be recorded. Errors from augmented_syscalls__set_target_syscalls() are reported and propagated, the tracepoint filter string is freed on every exit path, and an allocation failure in trace__set_filter_pids() now returns -ENOMEM instead of being silently ignored. Note that in trace__set_filter_pids() the target pids and the filtered pids are two independent axes and both have to be programmed. Naming pids to leave out with --filter-pids does not widen -p/-t or a workload to the whole system, and a BPF tracepoint program is attached system wide rather than to the target's file descriptors, so pids_to_trace is the only thing keeping other tasks out. 6. Add --syscall-augment option (defaulting to true) to allow users to explicitly use --no-syscall-augment to run perf trace in the classic unaugmented tracepoint mode without BPF. When BPF is unavailable or disabled, ensure the non-augmented tracepoint path cleanly configures sys_enter and sys_exit without duplicate entries. Report a failure to program the pid filters with the error that caused it. trace__run() sent everything trace__set_filter_pids() returned to out_error_mem, which prints "Not enough memory to run!". That was already a guess, and becomes a wrong one now that the function also writes BPF maps: the update fails with -E2BIG when the target has more threads than pids_to_trace has room for, which has nothing to do with memory and is not something the user can act on from that message. Report a failure to program the syscall filters the same way. The pr_err() in trace__set_ev_qualifier_filter() ran before trace__run() reached out_errno and printed "%m", and anything called in between could have changed errno by then, so the two did not have to agree. The inner report becomes a pr_debug() and the caller prints the error it was given, which leaves out_errno without a user and it is removed. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 196 +++++++++++++++--- .../bpf_skel/augmented_raw_syscalls.bpf.c | 133 ++++++++++-- tools/perf/util/bpf_trace_augment.c | 135 +++++++++++- tools/perf/util/trace_augment.h | 34 +++ 5 files changed, 457 insertions(+), 46 deletions(-) diff --git a/tools/perf/Documentation/perf-trace.txt b/tools/perf/Documentation/perf-trace.txt index d20b43ea3d37..4680c69160d7 100644 --- a/tools/perf/Documentation/perf-trace.txt +++ b/tools/perf/Documentation/perf-trace.txt @@ -260,6 +260,11 @@ the thread executes on the designated CPUs. Default is to monitor all CPUs. Maximum number of lines in the summary mode. Note that this applies to each entry (thread or cgroup). +--syscall-augment:: + Augment syscalls with BPF. Enabled by default when BPF support is available. + Use --no-syscall-augment to disable BPF augmentation and fall back to the + unaugmented tracepoint approach. + PAGEFAULTS ---------- diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 2c62d38b19ac..e3c3f301e0b1 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -200,6 +200,7 @@ struct trace { int max_summary; int raw_augmented_syscalls_args_size; bool raw_augmented_syscalls; + bool syscall_augment; bool fd_path_disabled; bool sort_events; bool not_ev_qualifier; @@ -2060,6 +2061,23 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; + case PERF_RECORD_FORK: + if (trace->raw_augmented_syscalls && + (augmented_syscalls__has_target_pid(event->fork.ppid) || + augmented_syscalls__has_target_pid(event->fork.ptid))) { + augmented_syscalls__add_target_pid(event->fork.pid); + } + ret = machine__process_fork_event(machine, event, sample); + break; + case PERF_RECORD_EXIT: + if (trace->raw_augmented_syscalls) { + if (event->fork.pid == event->fork.tid) + augmented_syscalls__del_target_pid(event->fork.pid); + else + augmented_syscalls__del_target_pid(event->fork.tid); + } + ret = machine__process_exit_event(machine, event, sample); + break; default: ret = machine__process_event(machine, event, sample); break; @@ -4049,7 +4067,7 @@ static int trace__add_syscall_newtp(struct trace *trace) static int trace__set_ev_qualifier_tp_filter(struct trace *trace) { - int err = -1; + int err = 0; struct evsel *sys_exit; char *filter = asprintf_expr_inout_ints("id", !trace->not_ev_qualifier, trace->ev_qualifier_ids.nr, @@ -4058,15 +4076,27 @@ static int trace__set_ev_qualifier_tp_filter(struct trace *trace) if (filter == NULL) goto out_enomem; - if (!evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter)) { - sys_exit = trace->syscalls.events.sys_exit; + /* + * With BPF augmentation sys_enter is filtered in BPF and removed from + * the evlist, so only apply the tracepoint filter to the events that + * are actually present. + */ + if (trace->syscalls.events.sys_enter) + err = evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter); + + sys_exit = trace->syscalls.events.sys_exit; + if (!err && sys_exit) err = evsel__append_tp_filter(sys_exit, filter); - } free(filter); out: return err; out_enomem: + /* + * err defaults to 0 because either tracepoint may legitimately be + * absent, so the error has to be set explicitly here. + */ + err = -ENOMEM; errno = ENOMEM; goto out; } @@ -4508,7 +4538,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace __maybe_ static int trace__set_ev_qualifier_filter(struct trace *trace) { - if (trace->syscalls.events.sys_enter) + /* + * Synchronize syscall filter with BPF augmenter map: + * Pass trace->not_ev_qualifier to indicate blacklist mode ('!' prefix, + * e.g., -e !open,close) vs whitelist mode (-e open,close). + * + * A failure here would leave the BPF program filtering on a partially + * populated map, silently dropping or emitting the wrong syscalls, so + * propagate the error rather than continuing. + */ + if (trace->ev_qualifier_ids.nr > 0) { + int err = augmented_syscalls__set_target_syscalls(trace->ev_qualifier_ids.nr, + trace->ev_qualifier_ids.entries, + trace->not_ev_qualifier); + + if (err) { + pr_debug("Failed to set the syscalls to trace in the BPF map: %d\n", err); + return err; + } + } + + if (trace->syscalls.events.sys_enter || trace->syscalls.events.sys_exit) return trace__set_ev_qualifier_tp_filter(trace); return 0; } @@ -4549,13 +4599,21 @@ static int trace__set_filter_loop_pids(struct trace *trace) static int trace__set_filter_pids(struct trace *trace) { - int err = 0; + struct perf_thread_map *threads = evlist__core(trace->evlist)->threads; /* * Better not use !target__has_task() here because we need to cover the * case where no threads were specified in the command line, but a * workload was, and in that case we will fill in the thread_map when * we fork the workload in evlist__prepare_workload. */ + bool has_target = perf_thread_map__pid(threads, 0) != -1; + int err = 0; + + /* + * The exclusion list: --filter-pids names tasks to never report, and + * with no target at all we instead exclude perf itself so that tracing + * does not feed back into itself. + */ if (trace->filter_pids.nr > 0) { err = evlist__append_tp_filter_pids(trace->evlist, trace->filter_pids.nr, trace->filter_pids.entries); @@ -4563,10 +4621,37 @@ static int trace__set_filter_pids(struct trace *trace) err = augmented_syscalls__set_filter_pids(trace->filter_pids.nr, trace->filter_pids.entries); } - } else if (perf_thread_map__pid(evlist__core(trace->evlist)->threads, 0) == -1) { + } else if (!has_target) { err = trace__set_filter_loop_pids(trace); } + if (err) + return err; + + /* + * The inclusion list, which is a separate axis from the exclusion list + * above and so must be programmed even when --filter-pids was given: + * naming tasks to leave out does not widen -p/-t or a workload to the + * whole system. + * + * This matters more than it does on the tracepoint only path. A BPF + * tracepoint program is attached system wide rather than to the + * target's file descriptors, so pids_to_trace is the only thing + * keeping other tasks out. + */ + if (has_target) { + int nr = perf_thread_map__nr(threads); + pid_t *pids = malloc(nr * sizeof(pid_t)); + + if (pids == NULL) + return -ENOMEM; + + for (int i = 0; i < nr; i++) + pids[i] = perf_thread_map__pid(threads, i); + err = augmented_syscalls__set_target_pids(nr, pids); + free(pids); + } + return err; } @@ -4793,7 +4878,8 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } if (!trace->raw_augmented_syscalls) { - if (trace->trace_syscalls && trace__add_syscall_newtp(trace)) + if (trace->trace_syscalls && !trace->syscalls.events.sys_enter && + trace__add_syscall_newtp(trace)) goto out_error_raw_syscalls; if (trace->trace_syscalls) @@ -4888,7 +4974,7 @@ static int trace__run(struct trace *trace, int argc, const char **argv) err = trace__set_filter_pids(trace); if (err < 0) - goto out_error_mem; + goto out_error_filter_pids; /* * TODO: Initialize for all host binary machine types, not just @@ -4899,7 +4985,7 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (trace->ev_qualifier_ids.nr > 0) { err = trace__set_ev_qualifier_filter(trace); if (err < 0) - goto out_errno; + goto out_error_ev_qualifier; if (trace->syscalls.events.sys_exit) { pr_debug("event qualifier tracepoint filter: %s\n", @@ -5079,14 +5165,32 @@ static int trace__run(struct trace *trace, int argc, const char **argv) "Failed to set filter \"%s\" on event %s: %m\n", evsel->filter, evsel__name(evsel)); goto out_put_evlist; + +out_error_filter_pids: + /* + * Report what actually went wrong. Programming the pid filters + * allocates, but it also writes BPF maps, which fails for reasons of + * its own: -E2BIG when the target has more threads than pids_to_trace + * has room for, say. + */ + fprintf(trace->output, "Failed to set the pid filters: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; + +out_error_ev_qualifier: + /* + * Use the returned error, not errno. Reporting the failure on the way + * out of trace__set_ev_qualifier_filter() goes through the formatted + * output functions, which are free to leave errno describing + * something else by the time it is read here. + */ + fprintf(trace->output, "Failed to set the syscall filters: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; } out_error_mem: fprintf(trace->output, "Not enough memory to run!\n"); goto out_put_evlist; - -out_errno: - fprintf(trace->output, "%m\n"); - goto out_put_evlist; } static int trace__replay(struct trace *trace) @@ -5801,6 +5905,7 @@ int cmd_trace(int argc, const char **argv) .show_arg_names = true, .args_alignment = 70, .trace_syscalls = false, + .syscall_augment = true, .kernel_syscallchains = false, .max_stack = UINT_MAX, .max_events = ULONG_MAX, @@ -5856,6 +5961,8 @@ int cmd_trace(int argc, const char **argv) OPT_CALLBACK_DEFAULT('F', "pf", &trace.trace_pgfaults, "all|maj|min", "Trace pagefaults", parse_pagefaults, "maj"), OPT_BOOLEAN(0, "syscalls", &trace.trace_syscalls, "Trace syscalls"), + OPT_BOOLEAN(0, "syscall-augment", &trace.syscall_augment, + "Augment syscalls with BPF"), OPT_BOOLEAN('f', "force", &trace.force, "don't complain, do it"), OPT_CALLBACK(0, "call-graph", &trace.opts, "record_mode[,record_size]", record_callchain_help, @@ -5978,7 +6085,7 @@ int cmd_trace(int argc, const char **argv) "cgroup monitoring only available in system-wide mode"); } - if (!trace.trace_syscalls) + if (!trace.trace_syscalls || !trace.syscall_augment) goto skip_augmentation; if ((argc >= 1) && (strcmp(argv[0], "record") == 0)) { @@ -6003,8 +6110,19 @@ int cmd_trace(int argc, const char **argv) trace__add_syscall_newtp(&trace); err = augmented_syscalls__create_bpf_output(trace.evlist); - if (err == 0) + if (err == 0) { trace.syscalls.events.bpf_output = evlist__last(trace.evlist); + } else { + /* + * augmented_syscalls__prepare() already attached sys_enter and + * sys_exit, which are system wide. Falling through to + * skip_augmentation without undoing that would run a BPF + * program for every syscall on the machine, for the whole + * session, with nothing consuming the output. + */ + pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); + augmented_syscalls__cleanup(); + } skip_augmentation: err = -1; @@ -6060,7 +6178,9 @@ int cmd_trace(int argc, const char **argv) * syscall. */ if (trace.syscalls.events.bpf_output) { - evlist__for_each_entry(trace.evlist, evsel) { + struct evsel *n; + + evlist__for_each_entry_safe(trace.evlist, n, evsel) { bool raw_syscalls_sys_exit = evsel__name_is(evsel, "raw_syscalls:sys_exit"); if (raw_syscalls_sys_exit) { @@ -6068,28 +6188,44 @@ int cmd_trace(int argc, const char **argv) goto init_augmented_syscall_tp; } - if (trace.syscalls.events.bpf_output->priv == NULL && - strstr(evsel__name(evsel), "syscalls:sys_enter")) { + /* + * Match the evsel trace__add_syscall_newtp() made by + * identity rather than by name. It is called + * raw_syscalls:sys_enter, or syscalls:sys_enter on + * kernels too old to have the raw variant, and a + * substring test for the latter also matches the + * per syscall syscalls:sys_enter_SYSCALL tracepoints + * a user can ask for by name. Claiming one of those + * here would take the event the user asked for out of + * the evlist below and describe __augmented_syscalls__ + * with the wrong tracefs format. + */ + if (evsel == trace.syscalls.events.sys_enter) { struct evsel *augmented = trace.syscalls.events.bpf_output; if (evsel__init_augmented_syscall_tp(augmented, evsel) || evsel__init_augmented_syscall_tp_args(augmented)) goto out; /* - * Augmented is __augmented_syscalls__ BPF_OUTPUT event + * Augmented is __augmented_syscalls__ BPF_OUTPUT event. * Above we made sure we can get from the payload the tp fields * that we get from syscalls:sys_enter tracefs format file. + * Since BPF outputs all enter events (both augmented and + * unaugmented) into __augmented_syscalls__, we remove the raw + * sys_enter evsel from evlist so that perf trace only listens + * to __augmented_syscalls__, avoiding duplicate events and + * avoiding kernel tracepoint vetoes. + * + * Because evlist__remove() removes the first evsel (which had + * tracking=true by default), re-designate the tracking event + * so PERF_RECORD_COMM and fork tracking continue to be enabled. */ augmented->handler = trace__sys_enter; - /* - * Now we do the same for the *syscalls:sys_enter event so that - * if we handle it directly, i.e. if the BPF prog returns 0 so - * as not to filter it, then we'll handle it just like we would - * for the BPF_OUTPUT one: - */ - if (evsel__init_augmented_syscall_tp(evsel, evsel) || - evsel__init_augmented_syscall_tp_args(evsel)) - goto out; - evsel->handler = trace__sys_enter; + evlist__remove(trace.evlist, evsel); + evsel__put_and_free_priv(evsel); + trace.syscalls.events.sys_enter = NULL; + evlist__set_tracking_event(trace.evlist, + trace.syscalls.events.sys_exit ?: augmented); + continue; } if (strstarts(evsel__name(evsel), "syscalls:sys_exit_")) { diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 3bc9e28a9b8a..6ca9507ecc02 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -114,6 +114,41 @@ struct pids_filtered { __uint(max_entries, 64); } pids_filtered SEC(".maps"); +/* + * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when + * attached to a process with -p or tracing a specific command workload). + * + * has_pids_to_trace: Set to true if target PID filtering is active. + * When false, all processes are eligible for tracing. + */ +struct pids_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, pid_t); + __type(value, bool); + __uint(max_entries, 1024); +} pids_to_trace SEC(".maps"); + +bool has_pids_to_trace; + +/* + * Hash map storing syscall IDs for filtering (via 'perf trace -e ...'). + * + * has_syscalls_to_trace: Set to true if any syscall filter is active. + * not_syscalls_to_trace: Inverts matching when '!' prefix is used in -e + * (e.g., -e !open,close means trace everything EXCEPT + * open and close; an exclusion blacklist rather than + * an inclusion whitelist). + */ +struct syscalls_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, int); + __type(value, bool); + __uint(max_entries, 1024); +} syscalls_to_trace SEC(".maps"); + +bool has_syscalls_to_trace; +bool not_syscalls_to_trace; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -154,8 +189,8 @@ static inline struct augmented_args_payload *augmented_args_payload(void) static inline int augmented__output(void *ctx, struct augmented_args_payload *args, int len) { - /* If perf_event_output fails, return non-zero so that it gets recorded unaugmented */ - return bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + return 1; } static inline int augmented__beauty_output(void *ctx, void *data, int len) @@ -191,10 +226,21 @@ unsigned int augmented_arg__read_str(struct augmented_arg *augmented_arg, const return augmented_len; } -SEC("tp/raw_syscalls/sys_enter") +/* + * Default sys_enter program for syscalls without pointer argument augmentation. + * Writes the raw struct syscall_enter_args payload into __augmented_syscalls__ + * and returns 1 so the tracepoint is never vetoed in the kernel. + */ +SEC("tp/syscalls/sys_enter_unaugmented") int syscall_unaugmented(struct syscall_enter_args *args) { - return 1; + struct augmented_args_payload *augmented_args = augmented_args_payload(); + + if (augmented_args == NULL) + return 1; + + bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + return augmented__output(args, augmented_args, sizeof(augmented_args->args)); } /* @@ -424,11 +470,41 @@ static pid_t getpid(void) return bpf_get_current_pid_tgid(); } +/* + * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { return bpf_map_lookup_elem(pids, &pid) != NULL; } +/* + * Checks if the current task (thread PID or process TGID) is targeted for tracing. + * Checks both PID (thread ID) and TGID (process ID) so that all threads of a + * target process match. + */ +static inline bool pid_to_trace__has(pid_t pid) +{ + pid_t tgid = bpf_get_current_pid_tgid() >> 32; + + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || + bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; +} + +/* + * Determines if a syscall should be traced based on the filter map: + * - When not_syscalls_to_trace is true: blacklist mode (trace if NOT in map). + * - When not_syscalls_to_trace is false: whitelist mode (trace ONLY if IN map). + */ +static inline bool syscall_to_trace__enabled(int id) +{ + bool in_map = bpf_map_lookup_elem(&syscalls_to_trace, &id) != NULL; + + if (not_syscalls_to_trace) + return !in_map; + return in_map; +} + u64 ZERO = 0; /* @@ -562,6 +638,11 @@ static int augment_sys_enter(void *ctx, struct syscall_enter_args *args) return augmented__beauty_output(ctx, payload, sizeof(struct syscall_enter_args) + output); } +/* + * Main raw_syscalls:sys_enter tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel for + * other concurrent listeners. Filtered events simply do not output to the ring buffer. + */ SEC("tp/raw_syscalls/sys_enter") int sys_enter(struct syscall_enter_args *args) { @@ -576,8 +657,11 @@ int sys_enter(struct syscall_enter_args *args) * initial, non-augmented raw_syscalls:sys_enter payload. */ + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; augmented_args = augmented_args_payload(); if (augmented_args == NULL) @@ -585,25 +669,41 @@ int sys_enter(struct syscall_enter_args *args) bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + if (has_syscalls_to_trace && !syscall_to_trace__enabled(augmented_args->args.syscall_nr)) + return 1; + /* - * Jump to syscall specific augmenter, even if the default one, - * "!raw_syscalls:unaugmented" that will just return 1 to return the - * unaugmented tracepoint payload. + * Jump to syscall specific augmenter. If augmented, augment_sys_enter() + * outputs the payload to __augmented_syscalls__ and returns 0. + * Return 1 so we never veto the kernel tracepoint for other listeners. */ - if (augment_sys_enter(args, &augmented_args->args)) - bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + if (augment_sys_enter(args, &augmented_args->args) == 0) + return 1; - // If not found on the PROG_ARRAY syscalls map, then we're filtering it: - return 0; + bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + + /* + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. + */ + return 1; } +/* + * Main raw_syscalls:sys_exit tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel. + */ SEC("tp/raw_syscalls/sys_exit") int sys_exit(struct syscall_exit_args *args) { struct syscall_exit_args exit_args; + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); /* @@ -613,9 +713,12 @@ int sys_exit(struct syscall_exit_args *args) */ bpf_tail_call(args, &syscalls_sys_exit, exit_args.syscall_nr); /* - * If not found on the PROG_ARRAY syscalls map, then we're filtering it: + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. perf trace's own evsel filter will discard non-matching + * syscalls. */ - return 0; + return 1; } char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index ebb26225fb04..3a1f6e8b5d78 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,6 @@ #include <assert.h> #include <bpf/libbpf.h> +#include <errno.h> #include <internal/xyarray.h> #include <string.h> @@ -12,6 +13,23 @@ static struct augmented_raw_syscalls_bpf *skel; static struct evsel *bpf_output; +/* Set by attach_prog() so the first failure is what gets reported. */ +static int attach_err; + +static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const char *name) +{ + *link = bpf_program__attach(prog); + if (*link) + return 0; + /* + * Save errno before pr_debug(), which formats and writes output and so + * can overwrite it. + */ + attach_err = -errno; + pr_debug("Failed to attach %s BPF program\n", name); + return attach_err; +} + int augmented_syscalls__prepare(void) { struct bpf_program *prog; @@ -37,11 +55,35 @@ int augmented_syscalls__prepare(void) if (err < 0) { libbpf_strerror(err, buf, sizeof(buf)); pr_debug("Failed to load augmented syscalls BPF skeleton: %s\n", buf); + /* + * Tear the skeleton down rather than leaving a half initialized + * one behind. The caller falls back to unaugmented tracing and + * still calls the setters below, which must then do nothing + * instead of failing against a skeleton with no maps. + */ + augmented_syscalls__cleanup(); return err; } - augmented_raw_syscalls_bpf__attach(skel); + /* + * Only sys_enter and sys_exit are attached, the remaining programs are + * reached by tail calls. Attach them explicitly and, on failure, undo + * any partial attachment: leaving sys_enter live on + * raw_syscalls:sys_enter would keep running a BPF program for every + * syscall on the system for a perf trace session that never starts. + */ + if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) + goto out_cleanup; + if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) + goto out_cleanup; + return 0; + +out_cleanup: + err = attach_err; + /* Destroys every link attached above along with the skeleton. */ + augmented_syscalls__cleanup(); + return err; } int augmented_syscalls__create_bpf_output(struct evlist *evlist) @@ -100,6 +142,96 @@ int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) return err; } +/* + * Populate target PIDs in the BPF pids_to_trace map (e.g., for -p <PID> or + * when tracing a specified command workload). + */ +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) +{ + bool value = true; + int err = 0; + + if (skel == NULL || nr == 0) + return 0; + + for (size_t i = 0; i < nr; ++i) { + err = bpf_map__update_elem(skel->maps.pids_to_trace, &pids[i], + sizeof(*pids), &value, sizeof(value), + BPF_ANY); + if (err) + return err; + } + /* + * Set the flag only once every target is in the map. The BPF programs + * are attached by this point, so flipping it first would have them + * filter against a partially populated map and drop syscalls made by + * the targets that had not been added yet. + */ + skel->bss->has_pids_to_trace = true; + return 0; +} + +int augmented_syscalls__add_target_pid(pid_t pid) +{ + bool value = true; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), BPF_ANY); +} + +int augmented_syscalls__del_target_pid(pid_t pid) +{ + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); +} + +bool augmented_syscalls__has_target_pid(pid_t pid) +{ + bool value; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return false; + + return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), 0) == 0; +} + +/* + * Populate syscalls in the BPF syscalls_to_trace map: + * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace + * all syscalls EXCEPT these). + * false if whitelist mode (trace ONLY these syscalls). + */ +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls) +{ + bool value = true; + int err = 0; + + if (skel == NULL || nr == 0) + return 0; + + skel->bss->not_syscalls_to_trace = not_syscalls; + for (size_t i = 0; i < nr; ++i) { + err = bpf_map__update_elem(skel->maps.syscalls_to_trace, &syscall_ids[i], + sizeof(int), &value, sizeof(value), + BPF_ANY); + if (err) + return err; + } + /* + * As for the pid maps, publish the filter only once it is complete: + * in whitelist mode a half filled map would drop syscalls that were + * asked for but not added yet. + */ + skel->bss->has_syscalls_to_trace = true; + return 0; +} + int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd) { if (skel == NULL) @@ -142,4 +274,5 @@ struct bpf_program *augmented_syscalls__find_by_title(const char *name) void augmented_syscalls__cleanup(void) { augmented_raw_syscalls_bpf__destroy(skel); + skel = NULL; } diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index a1cd9a5e0213..5702eda3b469 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <stdbool.h> #include <sys/types.h> struct bpf_program; @@ -13,6 +14,11 @@ int augmented_syscalls__prepare(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__add_target_pid(pid_t pid); +int augmented_syscalls__del_target_pid(pid_t pid); +bool augmented_syscalls__has_target_pid(pid_t pid); +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); struct bpf_program *augmented_syscalls__unaugmented(void); @@ -40,6 +46,34 @@ static inline int augmented_syscalls__set_filter_pids(unsigned int nr __maybe_un return 0; } +static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_unused, + pid_t *pids __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) +{ + return false; +} + +static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, + int *syscall_ids __maybe_unused, + bool not_syscalls __maybe_unused) +{ + return 0; +} + static inline int augmented_syscalls__get_map_fds(int *enter_fd __maybe_unused, int *exit_fd __maybe_unused, int *beauty_fd __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 07/16] perf trace: Handle fork and exit directly in BPF filter maps 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (5 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 06/16] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 08/16] perf trace: Enumerate the target again once BPF is attached Ian Rogers ` (9 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Updating target or filtered PIDs in userspace upon processing PERF_RECORD_FORK and PERF_RECORD_EXIT events introduces latency between event occurrence and userspace BPF map updates. If a newly forked child executes system calls before userspace processes PERF_RECORD_FORK, those syscalls may be dropped by BPF PID filtering. Conversely, if userspace evicts PIDs asynchronously on PERF_RECORD_EXIT, the kernel may recycle a PID before userspace processes the exit event, causing the late eviction to silently drop a newly created task that received the recycled PID. Address this by attaching BTF-typed raw tracepoint BPF programs directly to the scheduler task lifetime tracepoints: 1. Attach SEC("tp_btf/sched_process_fork") (sched_process_fork), which runs in copy_process() in the parent's context before wake_up_new_task() wakes the child. Using tp_btf rather than SEC("tp/sched/sched_process_fork") receives the stable TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) rather than the tracepoint ring-buffer record (TP_STRUCT__entry), whose layout changed in Linux 6.16 when parent_comm and child_comm were converted from 16-byte arrays to 4-byte __data_loc strings. When inherit is enabled and the pid of the thread that called clone() is in pids_to_trace or pids_filtered, insert child->pid into the corresponding map immediately. Because child->pid is task_struct.pid (the global initial-namespace PID), this works accurately across PID namespaces without aliasing host PIDs, and covers both new processes and CLONE_THREAD threads without needing real_parent CO-RE walks or syscall-return heuristics. 2. Attach SEC("tp_btf/sched_process_exit") (sched_process_exit), which runs in do_exit() for every task in its own context, including tasks killed by signals (SIGKILL, SIGSEGV, etc.) and secondary threads torn down implicitly by exit_group. Delete the dying task's PID from pids_to_trace and pids_filtered immediately in kernel space, eliminating both map leaks and any asynchronous userspace eviction window where PID recycling could occur. 3. Attach SEC("tp_btf/sched_process_exec") (sched_process_exec) to follow the one case where a live task's pid changes underneath the maps. When a thread that is not the group leader execs, de_thread() kills the leader and hands the leader's pid, which is the tgid, to the exec'ing thread. The leader dies first, so sched_process_exit() has already dropped exactly the pid the survivor now holds, and the survivor's old entry would be stranded in the map for good. Move the entry from old_pid to p->pid. old_pid is sampled in bprm_execve() before de_thread() runs, so the ordinary group leader exec is a no-op here. Drop the old key before inserting the new one: the move is only a rename, but holding both keys at once needs a spare slot, and on a full map the insert would fail with -E2BIG while the delete still succeeded, losing the task instead of moving it. 4. With every live task registered before its first syscall and evicted in do_exit(), simplify pid_to_trace__has() and pid_filter__has() to single BPF hash map lookups, and move bpf_probe_read_kernel() in sys_exit back after the PID filter checks. 5. Pass the inherit flag from userspace to BPF .rodata via augmented_syscalls__prepare(!trace.opts.no_inherit), and split attaching out of it into augmented_syscalls__attach(), called from trace__run() once the pid, syscall and program array maps have all been programmed. These are system wide programs, so from the instant they attach they alone decide what is traced: attaching at load time, as before, left a window in which a target could fork without sched_process_fork() knowing the parent was a target, and with the userspace fork handling gone there was nothing left to recover it. The scheduler programs are attached ahead of sys_enter and sys_exit for the same reason. Set has_pids_filtered only after populating pids_filtered. A failure to attach ends the session, reporting the error that caused it. There is no falling back to unaugmented tracing by that point: cmd_trace() built the evlist around __augmented_syscalls__ and dropped the sys_enter evsel, so a session that carried on would report nothing at all. Neither did the code this replaces, which ignored the result of augmented_raw_syscalls_bpf__attach() altogether and ran on with programs that had never been attached. 6. Remove the userspace BPF map updates from PERF_RECORD_FORK and PERF_RECORD_EXIT in trace__process_event(), and delete the now-unused augmented_syscalls__{add,del,has}_target_pid() helpers. No coverage is lost with them: those records only come into being once the ring buffers are mapped by evlist__do_mmap() and the events are switched on by evlist__enable(), both of which run after augmented_syscalls__attach() in trace__run(), and they are then acted on later still, whenever the poll loop gets round to them. The scheduler programs therefore go live strictly earlier than the userspace path could ever have reacted. 7. Gate pid_filter__has() on a has_pids_filtered flag in .bss so the common case without --filter-pids performs no map lookups, and size pids_to_trace and pids_filtered at 16384 entries. pids_filtered is grown from 64 because it is no longer just the handful of pids userspace names: sched_process_fork() adds every descendant of those, so a --filter-pids target that forks or is heavily threaded needs the same headroom as a traced one. A fork or exit is still not seen if it happens before the programs are attached, that is between evlist__create_maps() scanning /proc for a -p target and augmented_syscalls__attach(). Such a window is inherent in programming a system wide filter before switching it on, and as above the userspace handling did not cover it either. The cost is not small though. Once the bpf-output event exists cmd_trace() removes the sys_enter evsel from the evlist, so __augmented_syscalls__ is the only source of enter events and a pid that is missing from pids_to_trace is not reported at all. sys_enter returning 1 keeps the kernel tracepoint alive for other subscribers, it does not give perf trace a second path to the event. A target that forks during perf trace's own startup can therefore have that child, and in turn everything the child forks, go untraced for the whole run. The tgid fallback that pid_to_trace__has() used to have would have masked part of this, since a thread missed in the window still shares the tgid of a target userspace did insert. It is not kept because it would also re-admit tasks that sched_process_fork() deliberately skipped: under --no-inherit a new thread of the target is not added to the map, yet it shares the target's tgid and a tgid test cannot tell it apart from one that was. sched_process_fork() does not consult the parent's tgid either, for the same reason. Every thread of a -p target is enumerated from /proc/<pid>/task and inserted under its own pid, and -t names a single thread, so a tgid test adds no reach. What it would add is a child inheriting from a thread that is not traced itself: a sibling of the thread -t selected, or one missed in the attach window whose own syscalls go unreported. Inheritance keys off the pid of the thread that called clone(), exactly as the lookups do. Two of the three ways of selecting what to trace are unaffected: 'perf trace -- cmd' cannot hit this because evlist__prepare_workload() leaves the child blocked on a pipe until evlist__start_workload(), well after the attach, and 'perf trace -a' never sets has_pids_to_trace so it does not filter at all. It is 'perf trace -p' against an already running target that is exposed. Closing that too needs the descendants of the target re-enumerated from /proc after the attach, which the next change does. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 55 +++-- .../bpf_skel/augmented_raw_syscalls.bpf.c | 199 +++++++++++++++++- tools/perf/util/bpf_trace_augment.c | 116 ++++++---- tools/perf/util/trace_augment.h | 28 +-- 4 files changed, 299 insertions(+), 99 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index e3c3f301e0b1..914e6e4b34f8 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2061,23 +2061,6 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; - case PERF_RECORD_FORK: - if (trace->raw_augmented_syscalls && - (augmented_syscalls__has_target_pid(event->fork.ppid) || - augmented_syscalls__has_target_pid(event->fork.ptid))) { - augmented_syscalls__add_target_pid(event->fork.pid); - } - ret = machine__process_fork_event(machine, event, sample); - break; - case PERF_RECORD_EXIT: - if (trace->raw_augmented_syscalls) { - if (event->fork.pid == event->fork.tid) - augmented_syscalls__del_target_pid(event->fork.pid); - else - augmented_syscalls__del_target_pid(event->fork.tid); - } - ret = machine__process_exit_event(machine, event, sample); - break; default: ret = machine__process_event(machine, event, sample); break; @@ -4993,6 +4976,22 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } } + /* + * Everything the BPF programs filter on is now in their maps, so it is + * safe to let them run. They are attached system wide, so anything + * before this point would have been filtered against a map that was + * still being built up. + * + * Falling back to unaugmented tracing is no longer possible here: the + * evlist was built around __augmented_syscalls__ back in cmd_trace(), + * which is where that decision is taken and where the sys_enter evsel + * was dropped. Fail the session rather than run one that can report + * nothing. + */ + err = augmented_syscalls__attach(); + if (err < 0) + goto out_error_attach; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the @@ -5187,6 +5186,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) fprintf(trace->output, "Failed to set the syscall filters: %s\n", str_error_r(-err, errbuf, sizeof(errbuf))); goto out_put_evlist; + +out_error_attach: + /* + * Use the returned error rather than errno: the failing attach is + * unwound before returning, and the libbpf calls that does can leave + * errno describing something else entirely. + */ + fprintf(trace->output, "Failed to attach the augmented syscalls BPF programs: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; } out_error_mem: fprintf(trace->output, "Not enough memory to run!\n"); @@ -6103,7 +6112,7 @@ int cmd_trace(int argc, const char **argv) goto skip_augmentation; } - err = augmented_syscalls__prepare(); + err = augmented_syscalls__prepare(!trace.opts.no_inherit); if (err < 0) goto skip_augmentation; @@ -6114,11 +6123,11 @@ int cmd_trace(int argc, const char **argv) trace.syscalls.events.bpf_output = evlist__last(trace.evlist); } else { /* - * augmented_syscalls__prepare() already attached sys_enter and - * sys_exit, which are system wide. Falling through to - * skip_augmentation without undoing that would run a BPF - * program for every syscall on the machine, for the whole - * session, with nothing consuming the output. + * Drop the loaded skeleton before falling back to unaugmented + * tracing. Otherwise the setters called from trace__run() would + * still program its maps, and augmented_syscalls__attach() would + * then put system wide BPF programs on raw_syscalls for a + * session with nothing consuming their output. */ pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); augmented_syscalls__cleanup(); diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 6ca9507ecc02..af04c4b3f445 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -9,6 +9,7 @@ #include "vmlinux.h" #include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> #include <linux/limits.h> #define PERF_ALIGN(x, a) __PERF_ALIGN_MASK(x, (typeof(x))(a)-1) @@ -107,25 +108,48 @@ struct augmented_arg { }; }; +/* + * Hash map of PIDs/TGIDs whose events must be discarded, e.g. perf trace's own + * pid, so that tracing doesn't feed back on itself. + * + * has_pids_filtered: set to true only when the map is populated. Checking a + * boolean is much cheaper than a map lookup, and sys_enter + * runs for every syscall on the system, so the common + * "no pids filtered" case must stay on a fast path. + * + * max_entries matches pids_to_trace: userspace only ever names a handful of + * pids here, but sched_process_fork() below adds every descendant of those, + * so a --filter-pids target that forks or is heavily threaded needs the same + * headroom as a traced one. + */ struct pids_filtered { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 64); + __uint(max_entries, 16384); } pids_filtered SEC(".maps"); +bool has_pids_filtered; + /* * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when * attached to a process with -p or tracing a specific command workload). * * has_pids_to_trace: Set to true if target PID filtering is active. * When false, all processes are eligible for tracing. + * + * max_entries bounds how many tasks can be tracked at once. sched_process_exit + * below evicts a task as it dies, whatever it died of, so the map holds live + * tasks rather than growing without bound. It is sized well + * above the thread count of realistic traced workloads; should a workload + * still exceed it, bpf_map_update_elem() fails with -E2BIG and the extra + * tasks are simply not traced rather than anything being corrupted. */ struct pids_to_trace { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 1024); + __uint(max_entries, 16384); } pids_to_trace SEC(".maps"); bool has_pids_to_trace; @@ -149,6 +173,9 @@ struct syscalls_to_trace { bool has_syscalls_to_trace; bool not_syscalls_to_trace; +/* Inherit tracing for child tasks (set to false if --no-inherit is specified) */ +const volatile bool inherit = true; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -471,24 +498,35 @@ static pid_t getpid(void) } /* - * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * Checks if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * + * Children of a filtered task are added to the map by sched_process_fork() + * below, so a plain lookup is all that is needed here. */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { + /* + * Fast path: this runs for every syscall on the system, so when no pid + * is filtered do no work at all rather than failing a lookup. + */ + if (!has_pids_filtered) + return false; + return bpf_map_lookup_elem(pids, &pid) != NULL; } /* - * Checks if the current task (thread PID or process TGID) is targeted for tracing. - * Checks both PID (thread ID) and TGID (process ID) so that all threads of a - * target process match. + * Checks if the current task is targeted for tracing. + * + * Every thread that existed when tracing started was named by the target and + * inserted from userspace, and every task created since was inserted by + * sched_process_fork() below, before it was able to run. So there is nothing + * to derive here, and in particular no need to consult the tgid or walk to the + * parent: a task is traced if and only if it is in the map. */ static inline bool pid_to_trace__has(pid_t pid) { - pid_t tgid = bpf_get_current_pid_tgid() >> 32; - - return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || - bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL; } /* @@ -706,6 +744,7 @@ int sys_exit(struct syscall_exit_args *args) return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); + /* * Jump to syscall specific return augmenter, even if the default one, * "!raw_syscalls:unaugmented" that will just return 1 to return the @@ -721,4 +760,144 @@ int sys_exit(struct syscall_exit_args *args) return 1; } +/* + * Propagate tracing to a newly created task. + * + * tp_btf/sched_process_fork is raised by copy_process(), in the parent's + * context and before the child is woken, so the child is in the maps before it + * can issue its first syscall. That removes the need to inspect real_parent + * when a syscall is seen from an unknown task, which could neither tell a + * genuine descendant from a task merely reparented to a traced init, nor keep + * following a descendant whose parent had already exited. + * + * Using tp_btf rather than tp/sched/sched_process_fork avoids depending on the + * tracepoint ring-buffer record layout (TP_STRUCT__entry), which changed in + * Linux 6.16 when parent_comm and child_comm were converted from fixed 16-byte + * arrays to 4-byte __data_loc strings (shrinking the tracepoint context from + * 48 to 24 bytes and causing BPF_PROG_TYPE_TRACEPOINT attachment to fail with + * -EACCES when accessing higher offsets). Instead, tp_btf receives the stable + * TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) + * directly. + * + * child->pid is task_struct.pid, i.e. the pid in the initial namespace, which + * is what the maps are keyed by. A clone() return value, in contrast, is the + * pid in the caller's namespace and would alias an unrelated host task when a + * containerised workload is traced. + * + * CLONE_THREAD needs no special handling: a new thread arrives here like any + * other task and is inserted under its own pid. + */ +SEC("tp_btf/sched_process_fork") +int BPF_PROG(sched_process_fork, struct task_struct *parent, struct task_struct *child) +{ + pid_t parent_pid, child_pid; + bool val = true; + + if (!inherit) + return 0; + + /* + * Inherit from the thread that called clone() and from nothing else. + * The maps name individual tasks: pid_to_trace__has() and + * pid_filter__has() look up a task's own pid and nothing more, and + * every thread of a -p target is enumerated from /proc/<pid>/task and + * inserted in its own right, so a traced thread is always here under + * its own key. Consulting the parent's tgid as well would let a child + * inherit from a thread that is not itself traced, which is precisely + * what 'perf trace -t <tid>' asked to leave out, and would re-admit + * descendants of a thread that sched_process_fork() skipped or that + * was missed while the programs were being attached, while still not + * tracing that thread itself. + */ + parent_pid = parent->pid; + child_pid = child->pid; + + if (has_pids_to_trace && + bpf_map_lookup_elem(&pids_to_trace, &parent_pid) != NULL) + bpf_map_update_elem(&pids_to_trace, &child_pid, &val, BPF_ANY); + + if (has_pids_filtered && + bpf_map_lookup_elem(&pids_filtered, &parent_pid) != NULL) + bpf_map_update_elem(&pids_filtered, &child_pid, &val, BPF_ANY); + + return 0; +} + +/* + * Drop a dying task from the maps. + * + * tp_btf/sched_process_exit is raised by do_exit() for every task, in its own + * context, so unlike hooking the exit and exit_group syscalls this also covers + * tasks killed by a signal and threads torn down implicitly by exit_group. + * + * Doing it here rather than from the userspace PERF_RECORD_EXIT handler also + * means there is no window between the task dying and the map being updated, + * during which the kernel could recycle the pid and the late eviction silently + * stop tracing whichever new task received it. + * + * Each thread is reported separately, including the group leader, whose pid is + * the thread group's tgid, so one delete per map covers both uses of the key. + */ +SEC("tp_btf/sched_process_exit") +int BPF_PROG(sched_process_exit, struct task_struct *p) +{ + pid_t pid = p->pid; + + bpf_map_delete_elem(&pids_to_trace, &pid); + bpf_map_delete_elem(&pids_filtered, &pid); + + return 0; +} + +/* + * Follow a task whose pid changed under it. + * + * When a thread that is not the thread group leader execs, de_thread() kills + * the rest of the group and then hands the leader's pid, which is the tgid, to + * the exec'ing thread. The leader dies first, so sched_process_exit() above + * has already dropped that pid from the maps, and the survivor is now keyed by + * a pid nothing knows about while its original entry is left behind for good. + * + * Move the entry across so the task stays tracked and nothing is leaked. + * old_pid is sampled in bprm_execve() before de_thread() runs, so for the + * common case of the group leader exec'ing it simply equals p->pid and there + * is nothing to do. + */ +SEC("tp_btf/sched_process_exec") +int BPF_PROG(sched_process_exec, struct task_struct *p, pid_t old_pid) +{ + pid_t pid = p->pid; + bool val = true; + + if (pid == old_pid) + return 0; + + /* + * Drop the old key before adding the new one. The maps are bounded and + * the move is only ever a rename, but inserting first needs a spare + * slot for as long as both keys are present: on a full map that insert + * fails with -E2BIG while the delete still succeeds, which would lose + * the task rather than move it. Deleting first frees the slot the + * insert goes on to use. The task is mid exec and issues no syscalls in + * between, so the gap is not observable. + * + * There is no atomic rename for a hash map, so on a map that is exactly + * full this narrows the window rather than closing it: a fork on + * another CPU can still take the freed slot before the insert below + * runs, and the task is then dropped just as any other task is once the + * map is full. + */ + if (bpf_map_lookup_elem(&pids_to_trace, &old_pid) != NULL) { + bpf_map_delete_elem(&pids_to_trace, &old_pid); + bpf_map_update_elem(&pids_to_trace, &pid, &val, BPF_ANY); + } + + if (bpf_map_lookup_elem(&pids_filtered, &old_pid) != NULL) { + bpf_map_delete_elem(&pids_filtered, &old_pid); + bpf_map_update_elem(&pids_filtered, &pid, &val, BPF_ANY); + } + + return 0; +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index 3a1f6e8b5d78..87c866d0365f 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -30,7 +30,7 @@ static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const c return attach_err; } -int augmented_syscalls__prepare(void) +int augmented_syscalls__prepare(bool inherit) { struct bpf_program *prog; char buf[128]; @@ -42,12 +42,18 @@ int augmented_syscalls__prepare(void) return -errno; } + skel->rodata->inherit = inherit; + /* - * Disable attaching the BPF programs except for sys_enter and - * sys_exit that tail call into this as necessary. + * Disable attaching the BPF programs other than those attached + * explicitly by augmented_syscalls__attach(), the rest are reached by + * tail calls. */ bpf_object__for_each_program(prog, skel->obj) { - if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit) + if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit && + prog != skel->progs.sched_process_fork && + prog != skel->progs.sched_process_exit && + prog != skel->progs.sched_process_exec) bpf_program__set_autoattach(prog, /*autoattach=*/false); } @@ -65,13 +71,42 @@ int augmented_syscalls__prepare(void) return err; } + return 0; +} + +int augmented_syscalls__attach(void) +{ + int err; + + if (skel == NULL) + return 0; + /* - * Only sys_enter and sys_exit are attached, the remaining programs are - * reached by tail calls. Attach them explicitly and, on failure, undo - * any partial attachment: leaving sys_enter live on - * raw_syscalls:sys_enter would keep running a BPF program for every - * syscall on the system for a perf trace session that never starts. + * Attaching is deliberately separate from, and a lot later than, + * loading: these are system wide tracepoint programs, so from the + * moment they are attached they are the only thing deciding which + * tasks and syscalls are traced. Going live before the pid and + * syscall maps are populated would mean a target that forked in the + * meantime was never picked up by sched_process_fork() below. + * + * Attach explicitly, so that a failure part way through can undo what + * came before it: leaving sys_enter live on raw_syscalls:sys_enter + * would keep running a BPF program for every syscall on the system for + * a perf trace session that never starts. + * + * The scheduler programs maintain the pid maps, and are attached first + * so that no fork, exit or exec can be missed between sys_enter going + * live and the maps being maintained. */ + if (attach_prog(&skel->links.sched_process_fork, skel->progs.sched_process_fork, + "sched_process_fork")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exit, skel->progs.sched_process_exit, + "sched_process_exit")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exec, skel->progs.sched_process_exec, + "sched_process_exec")) + goto out_cleanup; if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) goto out_cleanup; if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) @@ -83,6 +118,13 @@ int augmented_syscalls__prepare(void) err = attach_err; /* Destroys every link attached above along with the skeleton. */ augmented_syscalls__cleanup(); + /* + * Tearing the skeleton down closes file descriptors and frees memory, + * either of which may overwrite errno. Restore it so that a caller + * reporting this with "%m" describes the attach failure rather than + * whatever the teardown happened to do last. + */ + errno = -err; return err; } @@ -129,17 +171,29 @@ int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) bool value = true; int err = 0; - if (skel == NULL) + if (skel == NULL || nr == 0) return 0; + /* + * Tell the BPF program that the pids_filtered map is in use. Without + * this it would have to look up every task in an empty map, on every + * syscall on the system, to find out that nothing is filtered. + */ for (size_t i = 0; i < nr; ++i) { err = bpf_map__update_elem(skel->maps.pids_filtered, &pids[i], sizeof(*pids), &value, sizeof(value), BPF_ANY); if (err) - break; + return err; } - return err; + /* + * Publish the filter only now that the map is fully populated. + * augmented_syscalls__attach() has not run yet, so nothing is reading + * either of them, but keeping the flag and the map consistent means + * the ordering stays correct however the callers are rearranged. + */ + skel->bss->has_pids_filtered = true; + return 0; } /* @@ -162,45 +216,15 @@ int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) return err; } /* - * Set the flag only once every target is in the map. The BPF programs - * are attached by this point, so flipping it first would have them - * filter against a partially populated map and drop syscalls made by - * the targets that had not been added yet. + * Set the flag only once every target is in the map, so that the two + * are never inconsistent. Publishing it first would, once the + * programs are attached, have them filter against a partially + * populated map and drop syscalls made by targets not yet added. */ skel->bss->has_pids_to_trace = true; return 0; } -int augmented_syscalls__add_target_pid(pid_t pid) -{ - bool value = true; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), BPF_ANY); -} - -int augmented_syscalls__del_target_pid(pid_t pid) -{ - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); -} - -bool augmented_syscalls__has_target_pid(pid_t pid) -{ - bool value; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return false; - - return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), 0) == 0; -} - /* * Populate syscalls in the BPF syscalls_to_trace map: * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 5702eda3b469..ad992f5fa726 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -10,14 +10,12 @@ struct evlist; #ifdef HAVE_BPF_SKEL -int augmented_syscalls__prepare(void); +int augmented_syscalls__prepare(bool inherit); +int augmented_syscalls__attach(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); -int augmented_syscalls__add_target_pid(pid_t pid); -int augmented_syscalls__del_target_pid(pid_t pid); -bool augmented_syscalls__has_target_pid(pid_t pid); int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); @@ -26,11 +24,16 @@ void augmented_syscalls__cleanup(void); #else /* !HAVE_BPF_SKEL */ -static inline int augmented_syscalls__prepare(void) +static inline int augmented_syscalls__prepare(bool inherit __maybe_unused) { return -1; } +static inline int augmented_syscalls__attach(void) +{ + return 0; +} + static inline int augmented_syscalls__create_bpf_output(struct evlist *evlist __maybe_unused) { return -1; @@ -52,21 +55,6 @@ static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_un return 0; } -static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) -{ - return false; -} - static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, int *syscall_ids __maybe_unused, bool not_syscalls __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 08/16] perf trace: Enumerate the target again once BPF is attached 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (6 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 07/16] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 09/16] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers ` (8 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz evlist__create_maps() reads the target out of /proc, and the BPF sched_process_fork() program only sees what is cloned once it is attached. A task the target creates between the two is in neither, and since cmd_trace() drops the sys_enter evsel in favour of __augmented_syscalls__ there is no other source of enter events. Such a task, and in turn everything it forks, goes unreported for the rest of the session. Read the target out of /proc once more, after the attach. Whatever existed before the programs went live is there to be found, and whatever is created after it is sched_process_fork()'s to add, so between them nothing is left out. Doing this before the attach instead would only move the window rather than close it. The enumeration follows the same rule as the BPF programs, which is that the maps name individual tasks: - -p names a process, so its thread group is read from /proc/<pid>/task. - -t names a thread, which is taken on its own. Expanding it to its thread group would trace the siblings it asked to leave out. - Descendants come from task->children, read through /proc/<pid>/task/<tid>/children. A forked task leads a thread group of its own, so each one found is walked in turn and a tree of any depth is covered. New threads are not listed there, copy_process() gives a CLONE_THREAD child the real_parent of its creator rather than the creator itself, but the thread group walk above has them. The other two ways of choosing what to trace need nothing, for the same reasons the window never affected them: 'perf trace -a' does not filter on pid at all, and evlist__prepare_workload() keeps a workload blocked on a pipe until evlist__start_workload(), well after the attach. A target that exits during startup is not an error. Reading /proc for a task that has gone fails with ENOENT, and a task directory that is read but has nothing in it sets nothing at all, so errno is cleared before the enumeration and only an allocation failure is passed back. Anything else leaves the tasks that evlist__create_maps() already found in the map, which sched_process_exit() takes out again as they die, and the session runs on rather than being ended over a target that was going to stop producing events anyway. What is left is smaller and no longer lasts. A task found here may have made syscalls between sys_enter going live and it being added to the map, and those are not reported, but it is traced from that point on. On a kernel built without CONFIG_PROC_CHILDREN the children files are absent and descendants cannot be named, leaving the threads of the target, which are still picked up. pid_t, PATH_MAX, FILE and the directory reading are all used directly by the new code, so <sys/types.h>, <limits.h>, <stdio.h> and <dirent.h> are included rather than relied upon to arrive through another header. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 233 +++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 914e6e4b34f8..56bec2dfeb4e 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -15,6 +15,7 @@ */ #include "util/record.h" +#include <api/fs/fs.h> #include <api/fs/tracing_path.h> #ifdef HAVE_LIBBPF_SUPPORT #include <bpf/bpf.h> @@ -65,11 +66,15 @@ #include "trace_augment.h" #include "dwarf-regs.h" +#include <dirent.h> #include <errno.h> #include <sys/stat.h> +#include <sys/types.h> #include <inttypes.h> +#include <limits.h> #include <poll.h> #include <signal.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <linux/err.h> @@ -4638,6 +4643,224 @@ static int trace__set_filter_pids(struct trace *trace) return err; } +/* A list of pids that grows as it is added to, holding each pid once. */ +struct pid_list { + pid_t *entries; + size_t nr; + size_t allocated; +}; + +static bool pid_list__has(const struct pid_list *list, pid_t pid) +{ + for (size_t i = 0; i < list->nr; i++) { + if (list->entries[i] == pid) + return true; + } + return false; +} + +/* Append pid, unless it is already there. */ +static int pid_list__add(struct pid_list *list, pid_t pid) +{ + if (pid_list__has(list, pid)) + return 0; + + if (list->nr == list->allocated) { + size_t allocated = list->allocated ? list->allocated * 2 : 32; + pid_t *entries = realloc(list->entries, allocated * sizeof(*entries)); + + if (entries == NULL) + return -ENOMEM; + + list->entries = entries; + list->allocated = allocated; + } + + list->entries[list->nr++] = pid; + return 0; +} + +static void pid_list__exit(struct pid_list *list) +{ + zfree(&list->entries); + list->nr = 0; + list->allocated = 0; +} + +/* + * Append the tasks tid has forked to tgids. + * + * task->children holds what a task forked, and a forked task leads a thread + * group of its own, so each is something to expand in turn. New threads are + * not listed: copy_process() gives a CLONE_THREAD child the real_parent of + * its creator rather than the creator itself, so a thread is a sibling of the + * task that created it. Those are enumerated from the task directory instead. + */ +static int pid_list__add_children(struct pid_list *tgids, pid_t tid) +{ + char path[PATH_MAX]; + pid_t child; + FILE *fp; + int err = 0; + + scnprintf(path, sizeof(path), "%s/%d/task/%d/children", + procfs__mountpoint(), tid, tid); + fp = fopen(path, "r"); + /* + * Absent if the task exited, and on a kernel built without + * CONFIG_PROC_CHILDREN. Neither is worth failing for: what is missed + * is a task that has gone away, or descendants the kernel will not + * name. + */ + if (fp == NULL) + return 0; + + while (fscanf(fp, "%d", &child) == 1) { + err = pid_list__add(tgids, child); + if (err) + break; + } + + fclose(fp); + return err; +} + +/* + * Collect the tasks to trace: the target, its threads, and everything they + * have forked. + * + * tgids is the queue of thread groups still to expand. It is walked as it + * grows, so a child found here has its own children picked up in a later + * pass and the depth of the tree does not matter. The walk terminates + * because a task cannot be its own ancestor and pid_list__add() ignores a + * pid that is already listed. + */ +static int trace__collect_target_pids(struct trace *trace, struct pid_list *pids) +{ + struct target *target = &trace->opts.target; + /* + * -p names processes, so the whole thread group is a target. -t names + * threads, and expanding one to its group would trace the siblings + * that were deliberately left out. + */ + bool whole_group = target->pid != NULL; + struct perf_thread_map *threads; + struct pid_list tgids = {}; + int err = 0; + + /* Enumerate the target as evlist__create_maps() did, but now. */ + errno = 0; + threads = thread_map__new_str(target->pid, target->tid, target->per_thread); + if (threads == NULL) { + char bf[128]; + + /* + * A target that exited while perf trace was starting up shows + * up here as a failure to read /proc/<pid>/task, with scandir() + * setting ENOENT, or as a task directory that is read but has + * nothing in it, which sets nothing at all and is why errno is + * cleared above. Neither is worth ending the session for: the + * tasks the target had are already in the map from + * evlist__create_maps() and sched_process_exit() takes them out + * again as they die. Carry on with what is known and let only + * an allocation failure through, matching how the pid_list + * additions below are treated. + */ + if (errno == ENOMEM) + return -ENOMEM; + + pr_debug("Couldn't enumerate the target again (%s), tracing the tasks already known\n", + errno == 0 ? "it exited" : str_error_r(errno, bf, sizeof(bf))); + return 0; + } + + for (int i = 0; i < perf_thread_map__nr(threads); i++) { + pid_t pid = perf_thread_map__pid(threads, i); + + err = pid_list__add(whole_group ? &tgids : pids, pid); + /* A thread named by -t is not expanded, but its children are. */ + if (!err && !whole_group) + err = pid_list__add_children(&tgids, pid); + if (err) + goto out; + } + + for (size_t i = 0; i < tgids.nr; i++) { + pid_t tgid = tgids.entries[i]; + char path[PATH_MAX]; + struct dirent *dent; + DIR *tasks; + + scnprintf(path, sizeof(path), "%s/%d/task", procfs__mountpoint(), tgid); + tasks = opendir(path); + if (tasks == NULL) + continue; /* Exited between being named and being read. */ + + while ((dent = readdir(tasks)) != NULL) { + char *end; + pid_t tid = strtol(dent->d_name, &end, 10); + + /* Skip "." and "..". */ + if (*end != '\0') + continue; + + err = pid_list__add(pids, tid); + if (!err) + err = pid_list__add_children(&tgids, tid); + if (err) + break; + } + + closedir(tasks); + if (err) + goto out; + } +out: + perf_thread_map__put(threads); + pid_list__exit(&tgids); + return err; +} + +/* + * Trace the tasks that appeared while perf trace was starting up. + * + * evlist__create_maps() enumerated the target from /proc, and + * sched_process_fork() only sees what is cloned once it is attached. A task + * created in between is in neither, and since cmd_trace() drops the sys_enter + * evsel in favour of __augmented_syscalls__ it would go unreported for the + * whole session. + * + * Read the target out of /proc again now that the programs are attached. + * Whatever came before the attach is in /proc to be found, and whatever comes + * after it is sched_process_fork()'s to add, so between them nothing is left + * out. + * + * Doing this after the attach rather than before it is what makes that true; + * before it would only move the window. The cost is that a task found here + * may have made syscalls between sys_enter going live and it being added + * below, and those are not reported. It is traced from that point on. + */ +static int trace__set_startup_pids(struct trace *trace) +{ + struct pid_list pids = {}; + int err; + + /* + * Nothing to do without a target: 'perf trace -a' does not filter on + * pid at all, and evlist__prepare_workload() keeps a workload blocked + * on a pipe until evlist__start_workload(), well after the attach. + */ + if (!target__has_task(&trace->opts.target)) + return 0; + + err = trace__collect_target_pids(trace, &pids); + if (!err) + err = augmented_syscalls__set_target_pids(pids.nr, pids.entries); + + pid_list__exit(&pids); + return err; +} + static int __trace__deliver_event(struct trace *trace, union perf_event *event) { struct evlist *evlist = trace->evlist; @@ -4992,6 +5215,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (err < 0) goto out_error_attach; + /* + * With the programs live, anything the target created while they were + * being set up is now sched_process_fork()'s to keep track of, but it + * was not there to see it appear. Enumerate the target once more to + * pick those up. + */ + err = trace__set_startup_pids(trace); + if (err < 0) + goto out_error_filter_pids; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 09/16] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (7 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 08/16] perf trace: Enumerate the target again once BPF is attached Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 10/16] perf test common: Only disable probes in clear_all_probes Ian Rogers ` (7 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz test_task_analyzer.sh writes perf.data and temporary files directly into the current working directory, causing collisions when running tests in parallel. As a temporary measure until `perf script report` supports an input file option, resolve perfdir to an absolute path, change directory into $tmpdir for the test duration, and clean up in the exit trap. Remove the (exclusive) tag so the test runs in parallel. perfdir is derived from $0, which may be relative, so it has to be resolved before the cd into $tmpdir, otherwise both PERF_EXEC_PATH and the cleanup trap point at paths that no longer exist. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/test_task_analyzer.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/perf/tests/shell/test_task_analyzer.sh b/tools/perf/tests/shell/test_task_analyzer.sh index 0314412e63b4..443b88957b17 100755 --- a/tools/perf/tests/shell/test_task_analyzer.sh +++ b/tools/perf/tests/shell/test_task_analyzer.sh @@ -1,8 +1,18 @@ #!/bin/bash -# perf script task-analyzer tests (exclusive) +# perf script task-analyzer tests # SPDX-License-Identifier: GPL-2.0 -tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) +# Resolve the source directory before changing the working directory below, +# $0 may be a relative path and would no longer resolve from $tmpdir. +perfdir=$(cd "$(dirname "$0")/../.." && pwd) + +tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) || exit 1 +# The cleanup trap is only installed further down, once the functions it +# calls have been defined, so tidy up by hand if this cd fails. +cd "$tmpdir" || { + rmdir "$tmpdir" + exit 1 +} # TODO: perf script report only supports input from the CWD perf.data file, make # it support input from any file. perfdata="perf.data" @@ -11,7 +21,6 @@ csvsummary="$tmpdir/csvsummary" err=0 # set PERF_EXEC_PATH to find scripts in the source directory -perfdir=$(dirname "$0")/../.. if [ -e "$perfdir/scripts/python/Perf-Trace-Util" ]; then export PERF_EXEC_PATH=$perfdir fi @@ -20,8 +29,9 @@ fi export ASAN_OPTIONS=detect_leaks=0 cleanup() { - rm -f "${perfdata}" - rm -f "${perfdata}".old + # Step out of $tmpdir before removing it. The removal uses an absolute + # path and works from anywhere, so a failure to cd must not skip it. + cd "$perfdir" || cd / || true rm -rf "$tmpdir" trap - exit term int -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 10/16] perf test common: Only disable probes in clear_all_probes 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (8 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 09/16] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 11/16] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers ` (6 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz clear_all_probes() began by writing 0 to /sys/kernel/debug/tracing/events/enable, which disables every tracepoint on the system rather than just the probes the probe tests created. When tests run in parallel that also silences the events of any concurrent perf record, perf trace or ftrace session, so unrelated tests lose the events they are waiting for and fail. The write cannot simply be dropped. Clearing kprobe_events or uprobe_events is all or nothing: dyn_events_release_all() walks every probe of that type first and returns -EBUSY without removing any of them if it finds one that is still enabled, where enabled means TP_FLAG_TRACE from tracefs or TP_FLAG_PROFILE from a perf session. A probe left enabled through tracefs would therefore block the whole clear and leave stale probes behind to collide with the next run. Disable the probes and only the probes. The set to disable is taken from the kprobe_events and uprobe_events listings rather than from a guess at which groups perf uses, so it matches what dyn_events_release_all() is going to inspect: a probe an unrelated session left enabled in, say, the kprobes group would otherwise still abort the clear. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/common/init.sh | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/common/init.sh b/tools/perf/tests/shell/common/init.sh index cbfc78bec974..7c2ca74298ca 100644 --- a/tools/perf/tests/shell/common/init.sh +++ b/tools/perf/tests/shell/common/init.sh @@ -130,9 +130,40 @@ check_uprobes_available() test -e /sys/kernel/debug/tracing/uprobe_events } +# Disable every kprobe and uprobe event. The listings name each probe as +# "TYPE:GROUP/EVENT ARGS...", for instance "p:probe/vfs_read vfs_read", and +# events/GROUP/EVENT/enable is the switch for it. +disable_all_probes() +{ + PROBE_SPECS=`cat /sys/kernel/debug/tracing/kprobe_events \ + /sys/kernel/debug/tracing/uprobe_events 2> /dev/null | + cut -d ' ' -f 1` + for PROBE_SPEC in $PROBE_SPECS + do + case "$PROBE_SPEC" in + *:*/*) ;; + *) continue ;; + esac + PROBE_ENABLE="/sys/kernel/debug/tracing/events/${PROBE_SPEC#*:}/enable" + test -e "$PROBE_ENABLE" && echo 0 > "$PROBE_ENABLE" + done +} + clear_all_probes() { - echo 0 > /sys/kernel/debug/tracing/events/enable + # Disable the probes before removing them. Writing to kprobe_events or + # uprobe_events is all or nothing: dyn_events_release_all() walks every + # probe of that type first and returns -EBUSY without removing any of + # them if it finds one that is still enabled, which would leave stale + # probes behind to collide with the next run. That covers probes this + # test suite never created, so disable all of them and not just the + # ones in the groups perf uses. + # + # Only probes are disabled. Writing to events/enable would also silence + # the tracepoints of any perf record, perf trace or ftrace session + # sharing the machine, which breaks those tests when they run in + # parallel with this one. + disable_all_probes check_kprobes_available && echo > /sys/kernel/debug/tracing/kprobe_events check_uprobes_available && echo > /sys/kernel/debug/tracing/uprobe_events } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 11/16] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (9 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 10/16] perf test common: Only disable probes in clear_all_probes Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 12/16] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers ` (5 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The probe name `vfs_getname` was hardcoded, causing collisions when tests ran concurrently. Furthermore, `cleanup_probe_vfs_getname()` used `perf probe -d probe:vfs_getname*`, deleting probes registered by other parallel tests. Scope the probe name to the pid, and rename it to `getname_flags_$$` so that it no longer begins with "vfs_getname". perf trace calls evlist__add_vfs_getname(), which opens every event matching a hardcoded "probe:vfs_getname*" wildcard, so a perf trace run by any other test would otherwise pin this probe and make `perf probe -d` fail with -EBUSY. That also unblocks making the perf trace tests non-exclusive later in this series. Enumerate the probes to record and to delete from `perf probe -l`, matching `^probe:${vfs_getname}(_[[:digit:]]+)?$` exactly, rather than globbing on `${vfs_getname}*`. perf probe appends _1, _2, ... when getname_flags is inlined at more than one call site, so the variants do have to be matched, but since the name now ends in a pid a trailing wildcard would also match the probes of a test whose pid merely starts with this one's, e.g. 123 and 1234. Remove the `(exclusive)` tag from probe_vfs_getname.sh and record+script_probe_vfs_getname.sh so they run concurrently in pass 1. trace+probe_vfs_getname.sh has to stay exclusive: it is the one test that wants to be discovered by that wildcard, so it sets vfs_getname to a "vfs_getname_$$" name before sourcing the library, and would then pin its siblings' probes if it ran alongside them. A comment in the test records this. Delete the probes from an exit trap as well as on the way out. A pid scoped name is never seen again, so a run interrupted before cleanup_probe_vfs_getname() would leave its probes behind for good, accumulating a set per run. The fixed name at least meant the next run found, and went on to reuse, whatever the last one left. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/lib/probe_vfs_getname.sh | 51 +++++++++++++++++-- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+script_probe_vfs_getname.sh | 18 +++++-- .../tests/shell/trace+probe_vfs_getname.sh | 9 ++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/tools/perf/tests/shell/lib/probe_vfs_getname.sh b/tools/perf/tests/shell/lib/probe_vfs_getname.sh index 88cd0e26d5f6..4e915b6aefc8 100644 --- a/tools/perf/tests/shell/lib/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/lib/probe_vfs_getname.sh @@ -1,15 +1,58 @@ #!/bin/bash # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 -perf probe -l 2>&1 | grep -q probe:vfs_getname +# The name of the getname_flags probe added and removed below. +# +# It is scoped to the pid so that tests running in parallel do not collide, +# and it deliberately does not start with "vfs_getname": perf trace calls +# evlist__add_vfs_getname(), which opens everything matching the hardcoded +# "probe:vfs_getname*" wildcard, so a perf trace running in another test would +# otherwise pin this probe and make the 'perf probe -d' below fail with -EBUSY. +# +# trace+probe_vfs_getname.sh is the one test that does want to be found that +# way, so it sets vfs_getname itself before sourcing this file, and is +# (exclusive) as a result. +: "${vfs_getname:=getname_flags_$$}" + +# Print the probes add_probe_vfs_getname() created. perf probe appends _1, _2, +# ... when getname_flags is inlined at more than one call site, so there can be +# several. Match them exactly rather than with a "${vfs_getname}*" glob: the +# name ends in a pid, so such a glob would also match the probes of a test +# whose pid merely starts with this one's, e.g. 123 and 1234. +probes_vfs_getname() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe:${vfs_getname}(_[[:digit:]]+)?$" +} + +[ -n "$(probes_vfs_getname)" ] had_vfs_getname=$? cleanup_probe_vfs_getname() { if [ $had_vfs_getname -eq 1 ] ; then - perf probe -q -d probe:vfs_getname* + local probe + for probe in $(probes_vfs_getname); do + perf probe -q -d "$probe" + done fi } +# Delete the probes however the test ends, not just when it runs to +# completion. The name is scoped to the pid, so nothing that runs later +# reuses or tidies up a probe an interrupted test left behind, and they +# would accumulate one set per run. The fixed name used before was at least +# picked up again by the next run. +# +# Tests may still call cleanup_probe_vfs_getname directly. Doing so leaves +# nothing for probes_vfs_getname to find, so the trap below then does +# nothing. A test needing cleanup of its own should call +# cleanup_probe_vfs_getname from its own exit trap, since installing one +# replaces this rather than adding to it. +trap cleanup_probe_vfs_getname exit +# Turn a signal into an ordinary exit so that the exit trap above runs. An +# exit trap that returns leaves the exit status alone, so a test exiting 2 +# to skip still skips. +trap 'exit 1' term int + add_probe_vfs_getname() { add_probe_verbose=$1 if [ $had_vfs_getname -eq 1 ] ; then @@ -33,8 +76,8 @@ add_probe_vfs_getname() { return 2 fi - perf probe -q "vfs_getname=getname_flags:${line} pathname=result->name:string" || \ - perf probe $add_probe_verbose "vfs_getname=getname_flags:${line} pathname=filename:ustring" || return 1 + perf probe -q "${vfs_getname}=getname_flags:${line} pathname=result->name:string" || \ + perf probe $add_probe_verbose "${vfs_getname}=getname_flags:${line} pathname=filename:ustring" || return 1 fi } diff --git a/tools/perf/tests/shell/probe_vfs_getname.sh b/tools/perf/tests/shell/probe_vfs_getname.sh index 5fe5682c28ce..05f1d50732b6 100755 --- a/tools/perf/tests/shell/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/probe_vfs_getname.sh @@ -1,6 +1,5 @@ #!/bin/bash -# Add vfs_getname probe to get syscall args filenames (exclusive) - +# Add vfs_getname probe to get syscall args filenames # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 diff --git a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh index 002f7037f182..1d4fb4a4fbfe 100755 --- a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh +++ b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Use vfs_getname probe to get syscall args filenames (exclusive) +# Use vfs_getname probe to get syscall args filenames # Uses the 'perf test shell' library to add probe:vfs_getname to the system # then use it with 'perf record' using 'touch' to write to a temp file, then @@ -17,22 +17,32 @@ skip_if_no_perf_probe || exit 2 # shellcheck source=lib/probe_vfs_getname.sh . "$(dirname "$0")/lib/probe_vfs_getname.sh" +# shellcheck disable=SC2154 # vfs_getname is assigned in lib/probe_vfs_getname.sh record_open_file() { echo "Recording open file:" # Check presence of libtraceevent support to run perf record - skip_no_probe_record_support "probe:vfs_getname*" + skip_no_probe_record_support if [ $? -eq 2 ]; then echo "WARN: Skipping test record_open_file. No libtraceevent support" return 2 fi - perf record -o ${perfdata} -e probe:vfs_getname\* touch $file + # Record every probe the inlining of getname_flags produced, naming + # them exactly rather than with a "${vfs_getname}*" glob, which would + # also match the probes of a test whose pid starts with this one's. + local events + events=$(probes_vfs_getname | paste -sd, -) + if [ -z "${events}" ] ; then + echo "FAIL: no ${vfs_getname} probe to record" + return 1 + fi + perf record -o ${perfdata} -e "${events}" touch $file } perf_script_filenames() { echo "Looking at perf.data file for vfs_getname records for the file we touched:" perf script -i ${perfdata} | \ - grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:vfs_getname[_0-9]*: +\([[:xdigit:]]+\) +pathname=\"${file}\"" + grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:${vfs_getname}(_[0-9]+)?: +\([[:xdigit:]]+\) +pathname=\"${file}\"" } add_probe_vfs_getname diff --git a/tools/perf/tests/shell/trace+probe_vfs_getname.sh b/tools/perf/tests/shell/trace+probe_vfs_getname.sh index 7a0b1145d0cd..146305f4d549 100755 --- a/tools/perf/tests/shell/trace+probe_vfs_getname.sh +++ b/tools/perf/tests/shell/trace+probe_vfs_getname.sh @@ -10,6 +10,13 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 +# This test must stay exclusive, and is the only one of the probe tests that +# does: it does not name the event it uses. perf trace discovers it with the +# hardcoded "probe:vfs_getname*" wildcard in evlist__add_vfs_getname(), so the +# probe has to carry that prefix, and a parallel run of this test would then +# also match, and pin, the probes of the other tests. The sibling tests avoid +# all of this by using a name that the wildcard cannot reach. + # shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh @@ -17,6 +24,8 @@ skip_if_no_perf_probe || exit 2 skip_if_no_perf_trace || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# shellcheck disable=SC2034 # consumed by lib/probe_vfs_getname.sh +vfs_getname="vfs_getname_$$" . "$(dirname $0)"/lib/probe_vfs_getname.sh trace_open_vfs_getname() { -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 12/16] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (10 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 11/16] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 13/16] perf test trace_summary: Improve error diagnostics Ian Rogers ` (4 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The uprobe name was not scoped to PID, and concurrent writes to `/sys/kernel/debug/tracing/uprobe_events` can occasionally return `-EBUSY` when another process holds the tracefs inode lock. Scope the probe event name with `$$` (`inet_pton_$$=inet_pton`) and add a retry loop with backoff for uprobe addition. Drop the `(exclusive)` tag so the test can run in parallel during pass 1. A PID scoped probe is no longer cleaned up by any other test, so add an EXIT/TERM/INT trap to delete it, otherwise an interrupted run leaks the uprobe into the system. The trap is installed only after the root and IPv6 checks that `exit 2` to skip the test, as trap_cleanup() exits 1 and would otherwise turn those skips into failures. Deletion enumerates the probes from `perf probe -l`, matching `^probe_libc:inet_pton_$$(_[[:digit:]]+)?$` exactly, rather than reading $event_name: a signal arriving after perf probe injected the uprobe but before the assignment completed would leave that variable empty and leak the probe, and an `inet_pton_$$*` glob would reach the probe of a test whose pid merely starts with this one's. While here use mktemp rather than mktemp -u for the temporary files: this test runs as root in a world writable /tmp, and predicting a name without creating it allows another user to win the race and plant a symlink. The perf.data check becomes -s rather than -e as mktemp now pre-creates an empty file. Pre-create the temporary files with mktemp rather than reserving names with mktemp -u, and bail out if mktemp fails. The emptiness check on the recorded data quotes its path for the same reason: unquoted, an empty value would leave [ ! -s ] testing the string "-s", which is true, so the negation would skip the failure path and the test would go on to pass without having recorded anything. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../shell/record+probe_libc_inet_pton.sh | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh index eca629ee83f0..00367f26bfae 100755 --- a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh +++ b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh @@ -1,5 +1,5 @@ #!/bin/bash -# probe libc's inet_pton & backtrace it with ping (exclusive) +# probe libc's inet_pton & backtrace it with ping # Installs a probe on libc's inet_pton function, that will use uprobes, # then use 'perf trace' on a ping to localhost asking for just one packet @@ -21,20 +21,30 @@ nm -Dg $libc 2>/dev/null | grep -F -q inet_pton || exit 254 event_pattern='probe_libc:inet_pton(_[[:digit:]]+)?' add_libc_inet_pton_event() { + local attempts=0 + while [ $attempts -lt 3 ]; do + event_name=$(perf probe -f -x $libc -a "inet_pton_$$=inet_pton" 2>&1 | \ + awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ + ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) + + if [ -n "$event_name" ]; then + return 0 + fi + attempts=$((attempts + 1)) + sleep 0.1 + done - event_name=$(perf probe -f -x $libc -a inet_pton 2>&1 | \ - awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ - ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) - - if [ $? -ne 0 ] || [ -z "$event_name" ] ; then - printf "FAIL: could not add event\n" - return 1 - fi + printf "FAIL: could not add event\n" + return 1 } trace_libc_inet_pton_backtrace() { - expected=`mktemp -u /tmp/expected.XXX` + # Create the files rather than just reserving names with mktemp -u: + # this runs as root and /tmp is world writable, so a predictable name + # that is written to later can be pre-created as a symlink by an + # unprivileged user and used to clobber an arbitrary file. + expected=$(mktemp /tmp/expected.XXX) || return 1 echo "ping[][0-9 \.:]+$event_name: \([[:xdigit:]]+\)" > $expected echo ".*inet_pton\+0x[[:xdigit:]]+[[:space:]]\($libc|inlined\)$" >> $expected @@ -50,8 +60,8 @@ trace_libc_inet_pton_backtrace() { ;; esac - perf_data=`mktemp -u /tmp/perf.data.XXX` - perf_script=`mktemp -u /tmp/perf.script.XXX` + perf_data=$(mktemp /tmp/perf.data.XXX) || return 1 + perf_script=$(mktemp /tmp/perf.script.XXX) || return 1 # Check presence of libtraceevent support to run perf record skip_no_probe_record_support "$event_name/$eventattr/" @@ -61,9 +71,12 @@ trace_libc_inet_pton_backtrace() { fi perf record -e $event_name/$eventattr/ -o $perf_data ping -6 -c 1 ::1 > /dev/null 2>&1 - # check if perf data file got created in above step. - if [ ! -e $perf_data ]; then - printf "FAIL: perf record failed to create \"%s\" \n" "$perf_data" + # Check perf record actually wrote data. mktemp already created the + # file, so test that it is non-empty rather than that it exists. Quote + # the path: were it ever empty, [ ! -s ] would test the string "-s" + # instead and report success. + if [ ! -s "$perf_data" ]; then + printf "FAIL: perf record failed to write \"%s\" \n" "$perf_data" return 1 fi perf script -i $perf_data | tac | grep -m1 ^ping -B9 | tac > $perf_script @@ -97,21 +110,75 @@ trace_libc_inet_pton_backtrace() { # even if the perf script output does not match. } +# Print the pid scoped uprobes this test may have created. perf probe appends +# _1, _2, ... when the name is already taken, so match those too, but anchor +# the match: an "inet_pton_$$*" glob would also match the probe of a test whose +# pid merely starts with this one's, e.g. 123 and 1234. +libc_inet_pton_events() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe_libc:inet_pton_$$(_[[:digit:]]+)?$" +} + delete_libc_inet_pton_event() { + # Ask the kernel what is actually there rather than trusting + # $event_name: a signal arriving after perf probe injected the uprobe + # but before the assignment to event_name completed would otherwise + # leave the variable empty and leak the probe. + # + # Retry as the addition does. Deleting writes to uprobe_events just as + # adding does, so it can lose the same race with a concurrent test and + # fail with -EBUSY. Re-list rather than assume the delete worked, and + # only give up once the probes are really gone: the name is pid + # scoped, so one left behind here is never reused or overwritten by a + # later run and would sit in the kernel until reboot. + local attempts=0 + local probe + + while [ $attempts -lt 3 ]; do + for probe in $(libc_inet_pton_events); do + perf probe -q -d "$probe" + done - if [ -n "$event_name" ] ; then - perf probe -q -d $event_name - fi + if [ -z "$(libc_inet_pton_events)" ]; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 0.1 + done + + printf "WARN: could not delete event(s): %s\n" \ + "$(libc_inet_pton_events | tr '\n' ' ')" + return 1 +} + +cleanup() { + rm -f ${perf_data} ${perf_script} ${expected} + delete_libc_inet_pton_event + + trap - EXIT TERM INT +} + +trap_cleanup() { + cleanup + exit 1 } # Check for IPv6 interface existence ip a sh lo | grep -F -q inet6 || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# Install the trap only now that the skips above are out of the way: it exits +# 1, so arming it any earlier would turn an 'exit 2' skip into a failure. +# +# The event name is pid scoped, so unlike the old fixed name an orphan left +# behind by an interrupted run is never overwritten by a later run: it would +# stay in the kernel forever. Always clean up, including on a signal. +trap trap_cleanup EXIT TERM INT + skip_if_no_perf_probe && \ add_libc_inet_pton_event && \ trace_libc_inet_pton_backtrace err=$? -rm -f ${perf_data} ${perf_script} ${expected} -delete_libc_inet_pton_event +cleanup exit $err -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 13/16] perf test trace_summary: Improve error diagnostics 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (11 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 12/16] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 14/16] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers ` (3 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When pattern matching fails in test_perf_trace(), print the command that failed along with the actual match count, the matching lines found, and the last 20 lines of output to aid debugging. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index b80dea77cec6..4589b4581419 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -28,10 +28,16 @@ test_perf_trace() { count=$(grep -E -c -m 3 "${search}" ${OUTPUT}) if [ "${count}" != "3" ]; then - echo "Error: cannot find enough pattern ${search} in the output" - cat ${OUTPUT} - rm -f ${OUTPUT} - exit 1 + echo "Error: cannot find enough pattern ${search} (count=${count}) in output of:" + echo "Error: perf trace ${args} -- ${workload}" + echo "Error: matched lines:" + grep -E "${search}" ${OUTPUT} || echo "none" + echo "Error: last 20 lines of output:" + # The summary is printed after any trace output, so show the end of + # the file: with -S the head of it is the trace, not what is matched. + tail -n 20 ${OUTPUT} + rm -f ${OUTPUT} + exit 1 fi } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 14/16] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (12 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 13/16] perf test trace_summary: Improve error diagnostics Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 15/16] perf test trace_summary: Make non-exclusive Ian Rogers ` (2 subsequent siblings) 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_btf_general.sh used `--max-events=1` with `perf trace` on commands such as `mv`, `echo`, and `sleep`. When background activity occurs or tests run in parallel, `perf trace` can capture an event from an unrelated process and exit prematurely before recording the target command's syscalls. Drop `--max-events=1` and let tracing run until the command completes, checking for the expected output with grep (matching trace_btf_enum.sh). Remove the (exclusive) tag so the test runs in parallel. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_btf_general.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_btf_general.sh b/tools/perf/tests/shell/trace_btf_general.sh index 7a94a5743924..4d654b687a4e 100755 --- a/tools/perf/tests/shell/trace_btf_general.sh +++ b/tools/perf/tests/shell/trace_btf_general.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace BTF general tests (exclusive) +# perf trace BTF general tests # SPDX-License-Identifier: GPL-2.0 err=0 @@ -27,7 +27,7 @@ check_vmlinux() { trace_test_string() { echo "Testing perf trace's string augmentation" - output="$(perf trace --sort-events -e renameat* --max-events=1 -- mv ${file1} ${file2} 2>&1)" + output="$(perf trace --sort-events -e renameat* -- mv ${file1} ${file2} 2>&1)" if ! echo "$output" | grep -q -E "^mv/[0-9]+ renameat(2)?\(.*, \"${file1}\", .*, \"${file2}\", .*\) += +[0-9]+$" then printf "String augmentation test failed, output:\n$output\n" @@ -38,7 +38,7 @@ trace_test_string() { trace_test_buffer() { echo "Testing perf trace's buffer augmentation" # echo will insert a newline (\10) at the end of the buffer - output="$(perf trace --sort-events -e write --max-events=1 -- echo "${buffer}" 2>&1)" + output="$(perf trace --sort-events -e write -- echo "${buffer}" 2>&1)" if ! echo "$output" | grep -q -E "^echo/[0-9]+ write\([0-9]+, ${buffer}.*, [0-9]+\) += +[0-9]+$" then printf "Buffer augmentation test failed, output:\n$output\n" @@ -48,7 +48,7 @@ trace_test_buffer() { trace_test_struct_btf() { echo "Testing perf trace's struct augmentation" - output="$(perf trace --sort-events -e clock_nanosleep --force-btf --max-events=1 -- sleep 1 2>&1)" + output="$(perf trace --sort-events -e clock_nanosleep --force-btf -- sleep 1 2>&1)" if ! echo "$output" | grep -q -E "^sleep/[0-9]+ clock_nanosleep\(0, 0, \{1,.*\}, 0x[0-9a-f]+\) += +[0-9]+$" then printf "BTF struct augmentation test failed, output:\n$output\n" -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 15/16] perf test trace_summary: Make non-exclusive 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (13 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 14/16] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 14:06 ` [PATCH v3 16/16] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_summary.sh tests various summary modes of `perf trace`. It already directs output to a unique temporary file without polluting the current working directory. Remove the (exclusive) tag so it can run concurrently in parallel test runs. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index 4589b4581419..d0196351243d 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace summary (exclusive) +# perf trace summary # SPDX-License-Identifier: GPL-2.0 # Check that perf trace works with various summary mode -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v3 16/16] perf test uprobe_from_different_cu: Scope probe name to PID 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (14 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 15/16] perf test trace_summary: Make non-exclusive Ian Rogers @ 2026-09-18 14:06 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 16 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 14:06 UTC (permalink / raw) To: irogers, acme, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The test builds a binary in a per-run temporary directory and probes its foo function. The directory name is unique, but perf probe derives the event name from the probed function and the group name from the binary's basename, so every run registers the same probe_testfile:foo event. Running the test concurrently with itself, as 'perf test -r3' does, therefore fails in all but one of the runs with: Error: event "foo" already exists. Hint: Remove existing event by 'perf probe -d' and a losing run's cleanup goes on to delete the winning run's probe out from under it. Name the event after the pid, foo_$$, so that parallel runs no longer collide. This lets the test stay in the parallel pass rather than having to be marked (exclusive). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/test_uprobe_from_different_cu.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh index 7adf9755d6de..47c99d93436b 100755 --- a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh +++ b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh @@ -18,12 +18,19 @@ fi temp_dir=$(mktemp -d /tmp/perf-uprobe-different-cu-sh.XXXXXXXXXX) +# The name of the uprobe added and removed below. The probe is placed on +# ${temp_dir}/testfile, but perf probe derives the event name from the probed +# function and the group name from the binary's basename, so every run would +# otherwise share one probe_testfile:foo event, and a concurrent run would +# fail with 'event "foo" already exists'. Scope the event name to the pid. +probe_name="foo_$$" + cleanup() { trap - EXIT TERM INT if [[ "${temp_dir}" =~ ^/tmp/perf-uprobe-different-cu-sh.*$ ]]; then echo "--- Cleaning up ---" - perf probe -x ${temp_dir}/testfile -d foo || true + perf probe -x ${temp_dir}/testfile -d ${probe_name} || true rm -f "${temp_dir}/"* rmdir "${temp_dir}" fi @@ -84,6 +91,6 @@ gcc -g -Og -c ${temp_dir}/testfile-main.c -o ${temp_dir}/testfile-main.o gcc -g -Og -o ${temp_dir}/testfile ${temp_dir}/testfile-foo.o ${temp_dir}/testfile-main.o perf probe -x ${temp_dir}/testfile --funcs foo | grep "foo" -perf probe -x ${temp_dir}/testfile foo +perf probe -x ${temp_dir}/testfile ${probe_name}=foo cleanup -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (15 preceding siblings ...) 2026-09-18 14:06 ` [PATCH v3 16/16] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 01/18] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers ` (19 more replies) 16 siblings, 20 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz perf trace's BPF augmentation attaches to raw_syscalls:sys_enter and raw_syscalls:sys_exit system wide, and used the program return value to decide whether a syscall was interesting. Returning 0 from a BPF_PROG_TYPE_TRACEPOINT program makes perf_trace_run_bpf_submit() drop the event for every listener on that tracepoint, not just for the perf trace that installed the program. Any concurrent perf trace, perf record or ftrace session watching raw_syscalls therefore lost events, which is one of the reasons so many of the perf trace and perf probe shell tests had to be marked (exclusive) and run on their own. Patches 1 to 3 are independent fixes to the code the rest of the series goes on to rework or to rely on. Patches 4 to 10 fix perf trace. They stop the return value being used as a filter and do the filtering in BPF maps instead, fix argument handling for the __data_loc internal tracepoint fields that syscalls:sys_enter_* gained in 6.19, stop the sys_exit program array tail calling a sys_enter augmenter, and replace the userspace PERF_RECORD_FORK/PERF_RECORD_EXIT bookkeeping with BTF-typed raw tracepoint programs on sched_process_{fork,exit,exec}. A task is then registered before its first syscall and evicted in do_exit(), rather than whenever userspace next drains the ring buffer. Patches 11 to 18 deal with the tests. Several collided with each other through global state rather than through perf trace: fixed probe names, clear_all_probes() disabling every tracepoint on the system, and perf trace's hardcoded "probe:vfs_getname*" wildcard pinning probes belonging to other tests. With those scoped to a pid they can drop (exclusive) and run in parallel again. Tested on x86_64. The trace and probe tests pass under 'perf test -r3', which runs the repeats concurrently. Every patch builds individually, and the series also builds with BUILD_BPF_SKEL=0. Changes since v3: - New patch 3 returns -ENOMEM rather than -1 from evsel__set_filter(), evsel__append_filter(), evlist__set_tp_filter() and evlist__append_tp_filter(). An allocation is the only thing that can fail in any of them, and patch 7 goes on to print what they return, where -1 negated is EPERM and a failure to allocate would have been reported as "Operation not permitted". - Patch 5 sets sc->args only once the arg_fmt array that is indexed alongside it has been allocated. Publishing the field first left a syscall with args set and arg_fmt NULL, and since sc->name is already set by then a later syscall__read_info() returns success without retrying the allocation, leaving syscall_arg_fmt__mask_val() to dereference NULL. - Patch 7 adds the raw_syscalls tracepoints in trace__run() only if they are not in the evlist already, and stops ignoring the result of trace__add_syscall_newtp(). cmd_trace() adds them before it creates the bpf-output event, and on the paths where that creation fails they were added a second time, giving the session two enter and two exit evsels for the same pair of tracepoints. - Patch 7 no longer abandons the session when a target does not fit in the pid map. bpf_map__update_elem() answers -E2BIG once max_entries keys are present, so a target with more threads than the map has room for took perf trace down with it, on exactly the large workloads where there is least else to reach for. The tasks that fit are added and a warning says how many did not. - Patch 9 includes <sys/types.h> for the pid_t it uses rather than relying on the include chain to drag it in. - Patch 9 no longer ends the session when the target has already exited. thread_map__new_by_pid() fails with ENOENT once /proc/<pid>/task is gone, which is the ordinary outcome of 'perf trace -p' on a short lived process, so only ENOMEM is now propagated and everything else is logged and stepped over. - Patch 9 reads /proc once per thread group instead of once per thread. 'perf trace -p' names every thread of the target in the thread map and /proc/<tid>/task lists the whole group whichever thread it is asked through, so the enumeration was quadratic in the number of threads. On a 64 thread target it went from 65 directory reads and 4097 entries to 2 and 65, for the same set of pids. - New patch 10 removes from the BPF maps the tasks that died before userspace got round to writing them there. sched_process_exit() can only delete a pid that is already in the map, so a target that exits during startup leaves an entry behind for the rest of the session, and since pids are reused an unrelated task then gets traced, or silently filtered out, in its place. The map is walked with the cursor left on a task found alive, which is one the walk has decided to keep, so its own deletions can never leave the cursor on a key that htab_map_get_next_key() will answer by starting the walk over. Changes since v2: - Rebased onto the current perf-tools-next. - Patch 1 also includes <assert.h>, for the assert() in augmented_syscalls__create_bpf_output(). - New patch 2 makes evsel__put_and_free_priv() free the whole evsel_trace. It only zfree()d the struct, leaking the syscall_arg_fmt array hanging off it. No caller can reach that today, but patch 6 adds one that discards a fully set up evsel. - Patch 6 identifies the evsel to drop from the evlist by comparing against trace.syscalls.events.sys_enter instead of a strstr() for "syscalls:sys_enter". That substring also matches the per syscall syscalls:sys_enter_SYSCALL tracepoints, so a user asking for one of those by name would have had it removed from the evlist, and __augmented_syscalls__ described with its tracefs format rather than the raw tracepoint's. - Patch 6 reports a failure to program the pid filters with the error that caused it. trace__run() sent everything trace__set_filter_pids() returned to out_error_mem, which prints "Not enough memory to run!". That was already a guess, and a wrong one now the function writes BPF maps too. - Patch 7 moves the pid across in sched_process_exec() by deleting the old key before inserting the new one. The move is only a rename, but holding both keys at once needs a spare slot, and on a full map the insert failed with -E2BIG while the delete still succeeded, losing the task instead of moving it. - Patch 7 no longer claims that a task forked during the attach window is still traced unaugmented. That was wrong: cmd_trace() removes the sys_enter evsel once the bpf-output event exists, so __augmented_syscalls__ is the only source of enter events and such a task is not reported at all. Describe what is really lost, why the tgid fallback is not kept as a safety net for it, note that only 'perf trace -p' is exposed, and that closing it needs the target's descendants re-enumerated from /proc after the attach. - Patch 7 drops the parent tgid test from sched_process_fork(), so a child inherits from the pid of the thread that called clone() and from nothing else. The lookups only ever match a task's own pid, and every thread of a -p target is enumerated from /proc/<pid>/task and inserted in its own right, so the tgid added no reach. What it did add was a child inheriting from a thread that is not traced itself, such as a sibling of the thread 'perf trace -t' selected. - Patch 7 ends the session with the error that caused it when the BPF programs cannot be attached, rather than printing a bare errno left over from unwinding the attach. There is nothing to fall back to at that point, cmd_trace() has already built the evlist around __augmented_syscalls__ and dropped the sys_enter evsel, and the code being replaced did not fall back either: it ignored the result of augmented_raw_syscalls_bpf__attach() altogether. - New patch 8 reads the target out of /proc again once the BPF programs are attached, so a task the target created while perf trace was starting up is traced rather than missed for the whole session. This is the gap patch 7 describes and left for later. It covers the target's new threads and, through task->children, anything it or they forked, to any depth. What is left is a task that made syscalls between sys_enter going live and being added to the map, which is momentary rather than lasting for the session. - Patch 9 bails out if mktemp fails rather than carrying on with an empty $tmpdir. cd rejects the null directory, and the rmdir that was meant to undo the mktemp then failed to remove '' instead. Its cleanup() also no longer exits when it cannot cd out of the temporary directory, which would have skipped removing it. The removal takes an absolute path and does not need the cd to have succeeded. - Patch 10 now narrows the disable in clear_all_probes() to the probes themselves rather than dropping it. Clearing kprobe_events or uprobe_events is all or nothing: dyn_events_release_all() returns -EBUSY without removing anything if it finds a probe that is still enabled, so simply removing the write could leave stale probes behind to collide with the next run. The set to disable is read from the kprobe_events and uprobe_events listings rather than assumed to be the groups perf uses, since a probe left enabled in another group, such as the default kprobes group used when kprobe_events is written directly, would abort the clear just the same. - Patch 11 deletes the probes from an exit trap as well as on the way out. A pid scoped name is never seen again, so a run interrupted before cleanup_probe_vfs_getname() left its probes behind for good, a set per run, where the fixed name was at least found and reused by the next run. - Patch 12 retries deleting the uprobe. Deletion writes uprobe_events just as addition does and can lose the same race with a concurrent test, and because the event name is now pid scoped a probe left behind is never overwritten by a later run. It also bails out if mktemp fails and quotes the path in the emptiness check, which unquoted would have tested the string "-s" and reported success. - Patch 13 prints the tail of the output rather than the head. The pattern it matches only appears in the summary, which is printed after any trace output, so the head of the file is not the part that failed to match. Changes since v1: - New patch 1 includes <sys/types.h> and <string.h> for the pid_t and strcmp() uses that were relying on the include chain happening to drag them in, which does not hold on libcs such as musl. - Patch 6 no longer returns success when the event qualifier filter string fails to allocate. err now defaults to 0 because either tracepoint may legitimately be absent, so the ENOMEM path has to set the error itself rather than rely on that default. It also includes <stdbool.h> for the bool parameters it adds to trace_augment.h. - Patch 9 removes the temporary directory if the cd into it fails. That happens before the cleanup trap is installed, so the directory would otherwise be left behind in /tmp. Ian Rogers (18): perf trace: Include the headers declaring pid_t, strcmp and assert perf trace: Free the whole evsel_trace in evsel__put_and_free_priv perf evsel: Report an allocation failure as ENOMEM when setting filters perf trace: Start BPF summary before starting workload perf trace: Skip internal tracepoint fields in formatting and beauty map perf trace: Do not set unaugmented BPF program on sys_exit map perf trace: Filter events in BPF and avoid tracepoint vetoes perf trace: Handle fork and exit directly in BPF filter maps perf trace: Enumerate the target again once BPF is attached perf trace: Drop targets that died before they were filtered perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive perf test common: Only disable probes in clear_all_probes perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive perf test trace_summary: Improve error diagnostics perf test trace_btf_general: Drop --max-events=1 and make non-exclusive perf test trace_summary: Make non-exclusive perf test uprobe_from_different_cu: Scope probe name to PID tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 756 ++++++++++++++++-- tools/perf/tests/shell/common/init.sh | 33 +- .../perf/tests/shell/lib/probe_vfs_getname.sh | 51 +- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+probe_libc_inet_pton.sh | 107 ++- .../shell/record+script_probe_vfs_getname.sh | 18 +- tools/perf/tests/shell/test_task_analyzer.sh | 20 +- .../shell/test_uprobe_from_different_cu.sh | 11 +- .../tests/shell/trace+probe_vfs_getname.sh | 9 + tools/perf/tests/shell/trace_btf_general.sh | 8 +- tools/perf/tests/shell/trace_summary.sh | 16 +- .../bpf_skel/augmented_raw_syscalls.bpf.c | 312 +++++++- tools/perf/util/bpf_trace_augment.c | 288 ++++++- tools/perf/util/evlist.c | 12 +- tools/perf/util/evsel.c | 4 +- tools/perf/util/trace_augment.h | 33 +- 17 files changed, 1524 insertions(+), 162 deletions(-) base-commit: 86a27811675a415bd351efca1a194a1a94c082dd -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 01/18] perf trace: Include the headers declaring pid_t, strcmp and assert 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 02/18] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers ` (18 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_augment.h uses pid_t in the augmented_syscalls__set_filter_pids() prototype and in the !HAVE_BPF_SKEL stub. bpf_trace_augment.c calls strcmp() in augmented_syscalls__find_by_title() and assert() in augmented_syscalls__create_bpf_output(), but neither pulls in the header that declares what it uses. Both happen to build today only because something else in the include chain drags <sys/types.h>, <string.h> and <assert.h> in first, which is not guaranteed and does not hold on libcs such as musl that keep the POSIX namespaces strictly separated. Include <sys/types.h>, <string.h> and <assert.h> explicitly. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/util/bpf_trace_augment.c | 2 ++ tools/perf/util/trace_augment.h | 1 + 2 files changed, 3 insertions(+) diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index a9cf2a77ded1..ebb26225fb04 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,7 @@ +#include <assert.h> #include <bpf/libbpf.h> #include <internal/xyarray.h> +#include <string.h> #include "bpf_skel/augmented_raw_syscalls.skel.h" #include "debug.h" diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 4f729bc67753..a1cd9a5e0213 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <sys/types.h> struct bpf_program; struct evlist; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 02/18] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-18 21:19 ` [PATCH v4 01/18] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 03/18] perf evsel: Report an allocation failure as ENOMEM when setting filters Ian Rogers ` (17 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Every evsel->priv in builtin-trace.c is a struct evsel_trace, allocated by evsel_trace__new(). It holds a syscall_arg_fmt array in its fmt member, which evsel__syscall_arg_fmt() allocates on demand for the syscalls:sys_{enter,exit}_SYSCALL tracepoints and for every other tracepoint that gets its arguments pretty printed. evsel__put_and_free_priv() only did zfree(&evsel->priv), releasing the evsel_trace itself and leaking that array. Use evsel_trace__delete(), which frees fmt first, exactly as the out_delete path of evsel__syscall_arg_fmt() already does. The current callers are all error paths that run before fmt can have been allocated, so nothing leaks in practice today, but the helper is the obvious thing to reach for whenever an evsel is discarded and it should be safe for that. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 20fffc24507b..f67557e7a254 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -464,7 +464,13 @@ static int evsel__init_tp_ptr_field(struct evsel *evsel, struct tp_field *field, static void evsel__put_and_free_priv(struct evsel *evsel) { - zfree(&evsel->priv); + /* + * evsel->priv is always a struct evsel_trace here, so it has to go + * through evsel_trace__delete(): zfree() on its own would release the + * struct while leaking the syscall_arg_fmt array hanging off it. + */ + evsel_trace__delete(evsel->priv); + evsel->priv = NULL; evsel__put(evsel); } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 03/18] perf evsel: Report an allocation failure as ENOMEM when setting filters 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-18 21:19 ` [PATCH v4 01/18] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers 2026-09-18 21:19 ` [PATCH v4 02/18] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 04/18] perf trace: Start BPF summary before starting workload Ian Rogers ` (16 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz evsel__set_filter() and evsel__append_filter() return -1 when the strdup() or asprintf() that builds the new filter string fails, and evlist__set_tp_filter() and evlist__append_tp_filter() do the same when handed the NULL that asprintf__tp_filter_pids() returns for the same reason. In every case the only thing that can have gone wrong is an allocation. Callers all test the result with "< 0" or for being non-zero, so -1 has been as good as any other error so far, but it is not an errno and so cannot be printed as one. A caller that does, such as str_error_r(-err, errbuf, sizeof(errbuf)) turns it into EPERM and reports a failure to allocate as "Operation not permitted", which is no help to anyone trying to work out what happened. Return -ENOMEM instead, which is what these failures are. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/util/evlist.c | 12 ++++++++++-- tools/perf/util/evsel.c | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tools/perf/util/evlist.c b/tools/perf/util/evlist.c index c3d784727810..d403ab610bd3 100644 --- a/tools/perf/util/evlist.c +++ b/tools/perf/util/evlist.c @@ -1232,8 +1232,12 @@ int evlist__set_tp_filter(struct evlist *evlist, const char *filter) struct evsel *evsel; int err = 0; + /* + * The only caller that passes NULL is evlist__set_tp_filter_pids(), + * where it means asprintf__tp_filter_pids() failed to allocate. + */ if (filter == NULL) - return -1; + return -ENOMEM; evlist__for_each_entry(evlist, evsel) { if (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) @@ -1252,8 +1256,12 @@ int evlist__append_tp_filter(struct evlist *evlist, const char *filter) struct evsel *evsel; int err = 0; + /* + * As above, a NULL filter is asprintf__tp_filter_pids() having failed + * to allocate in evlist__append_tp_filter_pids(). + */ if (filter == NULL) - return -1; + return -ENOMEM; evlist__for_each_entry(evlist, evsel) { if (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index c663aafa88b2..6836156c2a7d 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -1872,7 +1872,7 @@ int evsel__set_filter(struct evsel *evsel, const char *filter) return 0; } - return -1; + return -ENOMEM; } static int evsel__append_filter(struct evsel *evsel, const char *fmt, const char *filter) @@ -1888,7 +1888,7 @@ static int evsel__append_filter(struct evsel *evsel, const char *fmt, const char return 0; } - return -1; + return -ENOMEM; } int evsel__append_tp_filter(struct evsel *evsel, const char *filter) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 04/18] perf trace: Start BPF summary before starting workload 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (2 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 03/18] perf evsel: Report an allocation failure as ENOMEM when setting filters Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 05/18] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers ` (15 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When using --bpf-summary, trace_start_bpf_summary() sets skel->bss->enabled = 1. In trace__run(), trace_start_bpf_summary() was previously invoked after evlist__start_workload(). Because evlist__start_workload() immediately unblocks the child process by writing to its go_pipe, short-lived workloads (such as `cat /dev/null`) can execute and complete their initial system calls before trace_start_bpf_summary() is reached by the parent process. Furthermore, under high system load, the child process may finish before the BPF summary tracking is enabled in the kernel at all, causing syscall summary tests to fail. Additionally, if initial_delay was configured, the workload was started before sleeping. Move trace_start_bpf_summary() to be invoked before evlist__start_workload(), matching evlist__enable(), and ensure it respects target.initial_delay. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f67557e7a254..f90c6bb4d8b4 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4844,17 +4844,19 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (!target__none(&trace->opts.target) && !trace->opts.target.initial_delay) evlist__enable(evlist); + if (trace->summary_bpf && !trace->opts.target.initial_delay) + trace_start_bpf_summary(); + if (forks) evlist__start_workload(evlist); if (trace->opts.target.initial_delay) { usleep(trace->opts.target.initial_delay * 1000); evlist__enable(evlist); + if (trace->summary_bpf) + trace_start_bpf_summary(); } - if (trace->summary_bpf) - trace_start_bpf_summary(); - trace->multiple_threads = perf_thread_map__pid(evlist__core(evlist)->threads, 0) == -1 || perf_thread_map__nr(evlist__core(evlist)->threads) > 1 || evlist__first(evlist)->core.attr.inherit; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 05/18] perf trace: Skip internal tracepoint fields in formatting and beauty map 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (3 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 04/18] perf trace: Start BPF summary before starting workload Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 06/18] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers ` (14 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Linux 6.19+ added __data_loc char[] internal fields for string arguments in syscalls:sys_enter_<name> tracepoints (e.g., __data_loc_oldname in sys_enter_renameat2). While is_internal_field() was added to detect them, several places did not properly account for them: 1. In syscall_arg_fmt__init_array(), when an internal field was skipped, the arg pointer was still incremented, causing the subsequent argument formatters to be mismatched. 2. In syscall__scnprintf_args(), internal fields were not skipped, causing spurious trailing arguments like ", 0, 16" to be formatted and printed. 3. In trace__bpf_sys_enter_beauty_map(), internal fields were not skipped, offsetting beauty array argument indices and breaking string and buffer augmentation. 4. In trace__find_usable_bpf_prog_entry(), candidate pointer checks matched on internal pointer fields, breaking signature compatibility matching between syscalls for augmenter sharing. Introduce next_user_arg() and advance both cursors with it, so that the two argument lists are always compared at a real argument and the walk ends when one syscall runs out of arguments rather than when one happens to have trailing internal fields. 5. syscall__augmented_args() computed the augmented payload as sample->raw_size - sc->args_size for any sys_enter style sample. sc->args_size deliberately stops at the last non-internal field, so on 6.19+ a native syscalls:sys_enter_<name> record leaves the __data_loc words and their string payloads in the remainder. Those bytes are not a struct augmented_arg, so syscall_arg__scnprintf_augmented_string() read a bogus length and walked arg->augmented.args out of bounds. This is reachable from trace__event_handler(), which calls trace__fprintf_sys_enter() for any evsel whose tracepoint name starts with "sys_enter_". 6. In syscall__read_info(), syscall__alloc_arg_fmts() was called before checking and dropping the leading __syscall_nr (or nr) field, using nr_fields - 1 unconditionally. If a tracepoint format lacks that leading field, the allocated arg_fmt array is one entry too small and syscall_arg_fmt__init_array() writes one entry past the end of the heap buffer. Drop __syscall_nr/nr first and size the allocation from the remaining fields. Update these functions to check and skip is_internal_field() so that arguments are correctly formatted and beauty map entries match the expected syscall signatures, restrict syscall__augmented_args() to the __augmented_syscalls__ bpf-output evsel, and size arg_fmt after dropping the syscall number field. Publish sc->args only once that allocation has succeeded. sc->name is set earlier in syscall__read_info(), and a later call takes a syscall with a name to have been read already and returns it as it stands, so a syscall left with arguments and no arg_fmt to describe them would be printed by walking the arguments and indexing an array that was never allocated. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 171 ++++++++++++++++++++++++++++--------- 1 file changed, 129 insertions(+), 42 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f90c6bb4d8b4..de3108dcef8a 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2283,15 +2283,20 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field struct tep_format_field *last_field = NULL; int len; - for (; field; field = field->next, ++arg) { - /* assume it's the last argument */ + for (; field; field = field->next) { + /* + * Skip internal tracepoint fields (e.g., __data_loc strings in + * Linux 6.19+) so they do not advance the syscall arg array index. + */ if (is_internal_field(field)) continue; last_field = field; - if (arg->scnprintf) + if (arg->scnprintf) { + ++arg; continue; + } len = strlen(field->name); @@ -2348,6 +2353,7 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field } } } + ++arg; } return last_field; @@ -2368,7 +2374,8 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) { char tp_name[128]; const char *name; - struct tep_format_field *field; + struct tep_format_field *args, *field; + int nr_args; int err; if (sc->nonexistent) @@ -2407,24 +2414,35 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) return err; } - /* - * The tracepoint format contains __syscall_nr field, so it's one more - * than the actual number of syscall arguments. - */ - if (syscall__alloc_arg_fmts(sc, sc->tp_format->format.nr_fields - 1)) - return -ENOMEM; - - sc->args = sc->tp_format->format.fields; + args = sc->tp_format->format.fields; + nr_args = sc->tp_format->format.nr_fields; /* * We need to check and discard the first variable '__syscall_nr' * or 'nr' that mean the syscall number. It is needless here. * So drop '__syscall_nr' or 'nr' field but does not exist on older kernels. + * + * Do this before allocating, and size the array from what is left, so + * that a format without the field does not leave + * syscall_arg_fmt__init_array() walking one entry past the end. */ - if (sc->args && (!strcmp(sc->args->name, "__syscall_nr") || !strcmp(sc->args->name, "nr"))) { - sc->args = sc->args->next; - --sc->nr_args; + if (args && (!strcmp(args->name, "__syscall_nr") || !strcmp(args->name, "nr"))) { + args = args->next; + --nr_args; } + if (syscall__alloc_arg_fmts(sc, nr_args)) + return -ENOMEM; + + /* + * Only now that there is an arg_fmt for each of them are the arguments + * published. sc->name was set above, so a later syscall__read_info() + * takes this syscall to be read already and returns it as it stands; + * were sc->args set with sc->arg_fmt still NULL, the printing of that + * syscall would walk the arguments and index an array that does not + * exist. + */ + sc->args = args; + field = sc->args; while (field) { if (is_internal_field(field)) @@ -2642,11 +2660,17 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, if (sc->args != NULL) { struct tep_format_field *field; - for (field = sc->args; field; - field = field->next, ++arg.idx, bit <<= 1) { - if (arg.mask & bit) + for (field = sc->args; field; field = field->next) { + /* Skip internal fields so they are not printed as spurious arguments */ + if (is_internal_field(field)) continue; + if (arg.mask & bit) { + ++arg.idx; + bit <<= 1; + continue; + } + arg.fmt = &sc->arg_fmt[arg.idx]; val = syscall_arg__val(&arg, arg.idx); /* @@ -2664,8 +2688,11 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, */ if (val == 0 && !trace->show_zeros && !(sc->arg_fmt && sc->arg_fmt[arg.idx].show_zero) && - !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) + !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) { + ++arg.idx; + bit <<= 1; continue; + } printed += scnprintf(bf + printed, size - printed, "%s", printed ? ", " : ""); @@ -2680,12 +2707,16 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, size - printed, val, field->type); if (btf_printed) { printed += btf_printed; + ++arg.idx; + bit <<= 1; continue; } } printed += syscall_arg_fmt__scnprintf_val(&sc->arg_fmt[arg.idx], bf + printed, size - printed, &arg, val); + ++arg.idx; + bit <<= 1; } } else if (IS_ERR(sc->tp_format)) { /* @@ -2946,7 +2977,9 @@ static int trace__fprintf_sample(struct trace *trace, struct perf_sample *sample return printed; } -static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sample, int *augmented_args_size, int raw_augmented_args_size) +static void *syscall__augmented_args(struct trace *trace, struct syscall *sc, + struct perf_sample *sample, + int *augmented_args_size, int raw_augmented_args_size) { /* * For now with BPF raw_augmented we hook into raw_syscalls:sys_enter @@ -2964,6 +2997,24 @@ static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sam */ int args_size = raw_augmented_args_size ?: sc->args_size; + /* + * Augmented arguments are a perf trace specific payload, they are only + * ever appended to samples emitted by the BPF __augmented_syscalls__ + * bpf-output event. + * + * Native syscalls:sys_enter_NAME tracepoints may also carry trailing + * data of their own: since Linux 6.19 they append __data_loc char[] + * fields plus the string payloads they point at. Those bytes are not a + * struct augmented_arg, so treating them as one would make + * syscall_arg__scnprintf_augmented_string() read a bogus length and + * walk arg->augmented.args far out of bounds. + * + * So only look for augmented arguments on the event that can actually + * produce them. + */ + if (sample->evsel != trace->syscalls.events.bpf_output) + return NULL; + *augmented_args_size = sample->raw_size - args_size; if (*augmented_args_size > 0) { static uintptr_t argbuf[1024]; /* assuming single-threaded */ @@ -3022,17 +3073,13 @@ static int trace__sys_enter(struct trace *trace, if (!(trace->duration_filter || trace->summary_only || trace->min_stack)) trace__printf_interrupted_entry(trace); /* - * If this is raw_syscalls.sys_enter, then it always comes with the 6 possible - * arguments, even if the syscall being handled, say "openat", uses only 4 arguments - * this breaks syscall__augmented_args() check for augmented args, as we calculate - * syscall->args_size using each syscalls:sys_enter_NAME tracefs format file, - * so when handling, say the openat syscall, we end up getting 6 args for the - * raw_syscalls:sys_enter event, when we expected just 4, we end up mistakenly - * thinking that the extra 2 u64 args are the augmented filename, so just check - * here and avoid using augmented syscalls when the evsel is the raw_syscalls one. + * syscall__augmented_args() only returns a payload for the BPF + * __augmented_syscalls__ event, so raw_syscalls:sys_enter (which always + * carries all 6 possible arguments rather than sc->args_size worth) and + * the native syscalls:sys_enter_NAME tracepoints are both handled there. */ - if (evsel != trace->syscalls.events.sys_enter) - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); ttrace->entry_time = sample->time; ttrace->entry_cpu = sample->cpu; msg = ttrace->entry_str; @@ -3077,7 +3124,7 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam struct syscall *sc; char msg[1024]; void *args, *augmented_args = NULL; - int augmented_args_size, e_machine; + int augmented_args_size = 0, e_machine; size_t printed = 0; @@ -3095,7 +3142,8 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam goto out_put; args = perf_evsel__sc_tp_ptr(args, sample); - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); printed += syscall__scnprintf_args(sc, msg, sizeof(msg), args, augmented_args, augmented_args_size, trace, thread); fprintf(trace->output, "%.*s", (int)printed, msg); err = 0; @@ -4127,10 +4175,16 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i if (trace->btf == NULL) return -1; - for (i = 0, field = sc->args; field; ++i, field = field->next) { + for (i = 0, field = sc->args; field; field = field->next) { + /* Skip internal fields to keep beauty array index aligned with syscall arguments */ + if (is_internal_field(field)) + continue; + // XXX We're only collecting pointer payloads _from_ user space - if (!sc->arg_fmt[i].from_user) + if (!sc->arg_fmt[i].from_user) { + ++i; continue; + } struct_offset = strstr(field->type, "struct "); if (struct_offset == NULL) @@ -4149,8 +4203,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i name[cnt] = '\0'; /* cache struct's btf_type and type_id */ - if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) + if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) { + ++i; continue; + } bt = sc->arg_fmt[i].type; beauty_array[i] = bt->size; @@ -4176,7 +4232,9 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i struct tep_format_field *field_tmp; /* find the size of the buffer that appears in pairs with buf */ - for (j = 0, field_tmp = sc->args; field_tmp; ++j, field_tmp = field_tmp->next) { + for (j = 0, field_tmp = sc->args; field_tmp; field_tmp = field_tmp->next) { + if (is_internal_field(field_tmp)) + continue; if (!(field_tmp->flags & TEP_FIELD_IS_POINTER) && /* only integers */ (strstr(field_tmp->name, "count") || strstr(field_tmp->name, "siz") || /* size, bufsiz */ @@ -4186,8 +4244,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i can_augment = true; break; } + ++j; } } + ++i; } if (can_augment) @@ -4196,6 +4256,19 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i return -1; } +/* + * Advance to the first field that is a real syscall argument, so that callers + * walking two argument lists in step never have to reason about internal + * fields appearing in one list but not the other. + */ +static struct tep_format_field *next_user_arg(struct tep_format_field *field) +{ + while (field && is_internal_field(field)) + field = field->next; + + return field; +} + static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace, struct syscall *sc) { @@ -4203,7 +4276,7 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace /* * We're only interested in syscalls that have a pointer: */ - for (field = sc->args; field; field = field->next) { + for (field = next_user_arg(sc->args); field; field = next_user_arg(field->next)) { if (field->flags & TEP_FIELD_IS_POINTER) goto try_to_find_pair; } @@ -4221,21 +4294,31 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace pair->bpf_prog.sys_enter == unaugmented_prog) continue; - for (field = sc->args, candidate_field = pair->args; - field && candidate_field; field = field->next, candidate_field = candidate_field->next) { + /* + * Both cursors only ever point at real arguments, so the loop + * ends when one of the two syscalls runs out of them, rather + * than when one happens to have trailing internal fields. + */ + field = next_user_arg(sc->args); + candidate_field = next_user_arg(pair->args); + while (field && candidate_field) { bool is_pointer = field->flags & TEP_FIELD_IS_POINTER, candidate_is_pointer = candidate_field->flags & TEP_FIELD_IS_POINTER; if (is_pointer) { - if (!candidate_is_pointer) { + if (!candidate_is_pointer) { // The candidate just doesn't copies our pointer arg, might copy other pointers we want. + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; - } + } } else { if (candidate_is_pointer) { // The candidate might copy a pointer we don't have, skip it. goto next_candidate; } + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; } @@ -4256,6 +4339,8 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace goto next_candidate; is_candidate = true; + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); } if (!is_candidate) @@ -4267,7 +4352,9 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace * more than what is common to the two syscalls. */ if (candidate_field) { - for (candidate_field = candidate_field->next; candidate_field; candidate_field = candidate_field->next) + candidate_field = next_user_arg(candidate_field->next); + for (; candidate_field; + candidate_field = next_user_arg(candidate_field->next)) if (candidate_field->flags & TEP_FIELD_IS_POINTER) goto next_candidate; } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 06/18] perf trace: Do not set unaugmented BPF program on sys_exit map 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (4 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 05/18] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 07/18] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers ` (13 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz In trace__init_syscalls_bpf_prog_array_maps(), the BPF program array map for sys_exit (syscalls_sys_exit) was populated with the result of trace__bpf_prog_sys_exit_fd(). When a syscall had no specific exit augmenter, trace__find_syscall_bpf_prog() fell back to unaugmented_prog (syscall_unaugmented). However, syscall_unaugmented is a sys_enter program that outputs enter arguments to __augmented_syscalls__. As a consequence, when an unaugmented syscall exited, sys_exit tail-called syscall_unaugmented, which interpreted the exit arguments as enter arguments and emitted a duplicate, corrupt sys_enter event into __augmented_syscalls__ right as the syscall completed. Fix this by: 1. Returning NULL from trace__find_syscall_bpf_prog() when looking up exit augmenters and none is found. 2. Returning -1 from trace__bpf_prog_sys_exit_fd() when no exit program is present. 3. Only updating map_exit_fd when prog_fd >= 0. 4. Clearing err = 0 when trace__bpf_sys_enter_beauty_map() returns non-zero (indicating the syscall has no augmentable pointer arguments) before continuing the loop, so a trailing run of such syscalls (e.g. 'perf trace -e close') does not leave err non-zero on return and abort the session. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index de3108dcef8a..f6fb6ffd654a 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4133,7 +4133,12 @@ static struct bpf_program *trace__find_syscall_bpf_prog(struct trace *trace __ma pr_debug("Couldn't find BPF prog \"%s\" to associate with syscalls:sys_%s_%s, not augmenting it\n", prog_name, type, sc->name); out_unaugmented: - return unaugmented_prog; + /* + * Do not set unaugmented_prog for exit: syscall_unaugmented is a + * sys_enter program that outputs enter arguments. Exit without a + * specialized return augmenter returns 1 directly from sys_exit. + */ + return !strcmp(type, "exit") ? NULL : unaugmented_prog; } static void trace__init_syscall_bpf_progs(struct trace *trace, int e_machine, int id) @@ -4156,7 +4161,7 @@ static int trace__bpf_prog_sys_enter_fd(struct trace *trace, int e_machine, int static int trace__bpf_prog_sys_exit_fd(struct trace *trace, int e_machine, int id) { struct syscall *sc = trace__syscall_info(trace, NULL, e_machine, id); - return sc ? bpf_program__fd(sc->bpf_prog.sys_exit) : bpf_program__fd(unaugmented_prog); + return sc && sc->bpf_prog.sys_exit ? bpf_program__fd(sc->bpf_prog.sys_exit) : -1; } static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, int key, unsigned int *beauty_array) @@ -4411,16 +4416,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace, int e_m err = bpf_map_update_elem(map_enter_fd, &key, &prog_fd, BPF_ANY); if (err) break; + /* Only update the exit prog array map if an exit augmenter exists */ prog_fd = trace__bpf_prog_sys_exit_fd(trace, e_machine, key); - err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); - if (err) - break; + if (prog_fd >= 0) { + err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); + if (err) + break; + } /* use beauty_map to tell BPF how many bytes to collect, set beauty_map's value here */ memset(beauty_array, 0, sizeof(beauty_array)); err = trace__bpf_sys_enter_beauty_map(trace, e_machine, key, (unsigned int *)beauty_array); - if (err) + if (err) { + /* + * Not a failure: the syscall just has no augmentable + * arguments. Clear err, or a trailing run of such + * syscalls, e.g. all of them for 'perf trace -e close', + * would leave it set on return and abort the session. + */ + err = 0; continue; + } err = bpf_map_update_elem(beauty_map_fd, &key, beauty_array, BPF_ANY); if (err) break; -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 07/18] perf trace: Filter events in BPF and avoid tracepoint vetoes 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (5 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 06/18] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 08/18] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers ` (12 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The BPF augmented_raw_syscalls sys_enter and sys_exit programs returned 0 for syscalls that were not of interest. Returning 0 from a tracepoint BPF program vetoes the event for the whole system, so an unrelated concurrent perf trace, perf record or ftrace session listening to raw_syscalls would silently lose events. This is a cross-session side effect and shows up as flaky failures when perf tests run in parallel. Furthermore, syscall_unaugmented previously returned 1 without writing anything to the __augmented_syscalls__ ring buffer. This forced userspace perf trace to listen to both raw_syscalls:sys_enter and __augmented_syscalls__ in its evlist, requiring userspace event deduplication. Address these issues: 1. In augmented_raw_syscalls.bpf.c, never return 0 from tracepoint handlers: return 1 so non-traced syscalls pass through without vetoing other concurrent listeners. 2. Introduce pids_to_trace and syscalls_to_trace BPF hash maps to perform targeted filtering directly in BPF. Unselected syscalls or PIDs return 1 immediately without writing to the buffer. 3. In syscall_unaugmented, output the unaugmented enter payload into __augmented_syscalls__ and return 1. Change its section from SEC("tp/raw_syscalls/sys_enter") to SEC("tp/syscalls/sys_enter_unaugmented") so libbpf does not attempt to auto-attach it to raw_syscalls:sys_enter. 4. In bpf_trace_augment.c, add helpers to configure target PIDs and syscalls in the BPF maps, setting the activation flags (has_pids_to_trace, has_syscalls_to_trace) only after the maps are fully populated so already-attached BPF programs do not filter against a half-filled map. Explicitly attach only sys_enter and sys_exit via an attach_prog() helper that saves -errno before calling pr_debug() or bpf_link__destroy(). Destroy the skeleton on every failure path. Leaving a loaded but unusable skeleton behind is not inert: the setters called later from trace__run() would program its maps, and a partial attach would leave a BPF program live on raw_syscalls for a session that never starts. Since augmented_syscalls__{prepare,create_bpf_output}() failures fall back to unaugmented tracing rather than aborting, those setters have to become no-ops, which they only do once skel is NULL again. errno is used directly here, so include <errno.h> rather than relying on it arriving via another header, which it does not under musl. 5. In builtin-trace.c, hook trace__set_ev_qualifier_filter() and PID filtering into the BPF maps. When __augmented_syscalls__ is active, remove raw_syscalls:sys_enter from trace.evlist since all traced enter events (both augmented and unaugmented) are now emitted by BPF into __augmented_syscalls__. Identify that evsel by comparing against trace.syscalls.events.sys_enter rather than by a strstr() of its name. The substring "syscalls:sys_enter" also matches the per syscall syscalls:sys_enter_SYSCALL tracepoints, which a user can ask for by name, and now that the match decides what is taken out of the evlist, claiming one of those would drop an event that was asked for and would describe __augmented_syscalls__ with its format rather than the raw one's. Restore tracking on the remaining evsel via evlist__set_tracking_event() so PERF_RECORD_COMM and tracking events continue to be recorded. Errors from augmented_syscalls__set_target_syscalls() are reported and propagated, the tracepoint filter string is freed on every exit path, and an allocation failure in trace__set_filter_pids() now returns -ENOMEM instead of being silently ignored. Note that in trace__set_filter_pids() the target pids and the filtered pids are two independent axes and both have to be programmed. Naming pids to leave out with --filter-pids does not widen -p/-t or a workload to the whole system, and a BPF tracepoint program is attached system wide rather than to the target's file descriptors, so pids_to_trace is the only thing keeping other tasks out. 6. Add --syscall-augment option (defaulting to true) to allow users to explicitly use --no-syscall-augment to run perf trace in the classic unaugmented tracepoint mode without BPF. When BPF is unavailable or disabled, ensure the non-augmented tracepoint path cleanly configures sys_enter and sys_exit without duplicate entries. In trace__run(), add those tracepoints only when neither of them is in the evlist already. trace__add_syscall_newtp() adds the pair whatever is there, and cmd_trace() may have left either or both behind: both when it prepared augmentation and then gave up on it, the exit one alone when __augmented_syscalls__ took over the enters. Neither is covered by trace.raw_augmented_syscalls, which is only set when the exit evsel is named raw_syscalls:sys_exit, and not on a kernel old enough for perf_evsel__raw_syscall_newtp() to have fallen back to syscalls:sys_exit. Adding the pair again reports the events that were there twice, which the fallback after a bpf-output failure already did before this. Act on trace__add_syscall_newtp()'s result in cmd_trace() rather than discarding it. Those tracepoints are what the augmented events are described and paired with, so without them there is nothing to augment: the skeleton is dropped and the session carries on with plain tracepoints, which is what the bpf-output failure beside it already does, and what lets the evlist test above rely on the exit tracepoint being there whenever the bpf-output event is. A target that does not fit in the map is not a reason to give up on the session. bpf_map__update_elem() answers -E2BIG once max_entries keys are present, so a target with more threads than pids_to_trace has room for would take the whole of perf trace down with it, on exactly the large workloads where there is least else to reach for. The tasks that fit are added, a warning says how many did not, and the ones left out are in the position they would have been in had they been created once the map was already full, which sched_process_fork() has to allow for regardless. Report a failure to program the pid filters with the error that caused it. trace__run() sent everything trace__set_filter_pids() returned to out_error_mem, which prints "Not enough memory to run!". That was already a guess, and becomes a wrong one now that the function also writes BPF maps, which fail for reasons of their own that have nothing to do with memory and that the message gives the user no way to act on. Report a failure to program the syscall filters the same way. The pr_err() in trace__set_ev_qualifier_filter() ran before trace__run() reached out_errno and printed "%m", and anything called in between could have changed errno by then, so the two did not have to agree. The inner report becomes a pr_debug() and the caller prints the error it was given, which leaves out_errno without a user and it is removed. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 222 +++++++++++++++--- .../bpf_skel/augmented_raw_syscalls.bpf.c | 133 +++++++++-- tools/perf/util/bpf_trace_augment.c | 169 ++++++++++++- tools/perf/util/trace_augment.h | 34 +++ 5 files changed, 511 insertions(+), 52 deletions(-) diff --git a/tools/perf/Documentation/perf-trace.txt b/tools/perf/Documentation/perf-trace.txt index d20b43ea3d37..4680c69160d7 100644 --- a/tools/perf/Documentation/perf-trace.txt +++ b/tools/perf/Documentation/perf-trace.txt @@ -260,6 +260,11 @@ the thread executes on the designated CPUs. Default is to monitor all CPUs. Maximum number of lines in the summary mode. Note that this applies to each entry (thread or cgroup). +--syscall-augment:: + Augment syscalls with BPF. Enabled by default when BPF support is available. + Use --no-syscall-augment to disable BPF augmentation and fall back to the + unaugmented tracepoint approach. + PAGEFAULTS ---------- diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f6fb6ffd654a..d403f1a318e3 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -200,6 +200,7 @@ struct trace { int max_summary; int raw_augmented_syscalls_args_size; bool raw_augmented_syscalls; + bool syscall_augment; bool fd_path_disabled; bool sort_events; bool not_ev_qualifier; @@ -2060,6 +2061,23 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; + case PERF_RECORD_FORK: + if (trace->raw_augmented_syscalls && + (augmented_syscalls__has_target_pid(event->fork.ppid) || + augmented_syscalls__has_target_pid(event->fork.ptid))) { + augmented_syscalls__add_target_pid(event->fork.pid); + } + ret = machine__process_fork_event(machine, event, sample); + break; + case PERF_RECORD_EXIT: + if (trace->raw_augmented_syscalls) { + if (event->fork.pid == event->fork.tid) + augmented_syscalls__del_target_pid(event->fork.pid); + else + augmented_syscalls__del_target_pid(event->fork.tid); + } + ret = machine__process_exit_event(machine, event, sample); + break; default: ret = machine__process_event(machine, event, sample); break; @@ -4059,7 +4077,7 @@ static int trace__add_syscall_newtp(struct trace *trace) static int trace__set_ev_qualifier_tp_filter(struct trace *trace) { - int err = -1; + int err = 0; struct evsel *sys_exit; char *filter = asprintf_expr_inout_ints("id", !trace->not_ev_qualifier, trace->ev_qualifier_ids.nr, @@ -4068,15 +4086,27 @@ static int trace__set_ev_qualifier_tp_filter(struct trace *trace) if (filter == NULL) goto out_enomem; - if (!evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter)) { - sys_exit = trace->syscalls.events.sys_exit; + /* + * With BPF augmentation sys_enter is filtered in BPF and removed from + * the evlist, so only apply the tracepoint filter to the events that + * are actually present. + */ + if (trace->syscalls.events.sys_enter) + err = evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter); + + sys_exit = trace->syscalls.events.sys_exit; + if (!err && sys_exit) err = evsel__append_tp_filter(sys_exit, filter); - } free(filter); out: return err; out_enomem: + /* + * err defaults to 0 because either tracepoint may legitimately be + * absent, so the error has to be set explicitly here. + */ + err = -ENOMEM; errno = ENOMEM; goto out; } @@ -4518,7 +4548,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace __maybe_ static int trace__set_ev_qualifier_filter(struct trace *trace) { - if (trace->syscalls.events.sys_enter) + /* + * Synchronize syscall filter with BPF augmenter map: + * Pass trace->not_ev_qualifier to indicate blacklist mode ('!' prefix, + * e.g., -e !open,close) vs whitelist mode (-e open,close). + * + * A failure here would leave the BPF program filtering on a partially + * populated map, silently dropping or emitting the wrong syscalls, so + * propagate the error rather than continuing. + */ + if (trace->ev_qualifier_ids.nr > 0) { + int err = augmented_syscalls__set_target_syscalls(trace->ev_qualifier_ids.nr, + trace->ev_qualifier_ids.entries, + trace->not_ev_qualifier); + + if (err) { + pr_debug("Failed to set the syscalls to trace in the BPF map: %d\n", err); + return err; + } + } + + if (trace->syscalls.events.sys_enter || trace->syscalls.events.sys_exit) return trace__set_ev_qualifier_tp_filter(trace); return 0; } @@ -4559,13 +4609,21 @@ static int trace__set_filter_loop_pids(struct trace *trace) static int trace__set_filter_pids(struct trace *trace) { - int err = 0; + struct perf_thread_map *threads = evlist__core(trace->evlist)->threads; /* * Better not use !target__has_task() here because we need to cover the * case where no threads were specified in the command line, but a * workload was, and in that case we will fill in the thread_map when * we fork the workload in evlist__prepare_workload. */ + bool has_target = perf_thread_map__pid(threads, 0) != -1; + int err = 0; + + /* + * The exclusion list: --filter-pids names tasks to never report, and + * with no target at all we instead exclude perf itself so that tracing + * does not feed back into itself. + */ if (trace->filter_pids.nr > 0) { err = evlist__append_tp_filter_pids(trace->evlist, trace->filter_pids.nr, trace->filter_pids.entries); @@ -4573,10 +4631,37 @@ static int trace__set_filter_pids(struct trace *trace) err = augmented_syscalls__set_filter_pids(trace->filter_pids.nr, trace->filter_pids.entries); } - } else if (perf_thread_map__pid(evlist__core(trace->evlist)->threads, 0) == -1) { + } else if (!has_target) { err = trace__set_filter_loop_pids(trace); } + if (err) + return err; + + /* + * The inclusion list, which is a separate axis from the exclusion list + * above and so must be programmed even when --filter-pids was given: + * naming tasks to leave out does not widen -p/-t or a workload to the + * whole system. + * + * This matters more than it does on the tracepoint only path. A BPF + * tracepoint program is attached system wide rather than to the + * target's file descriptors, so pids_to_trace is the only thing + * keeping other tasks out. + */ + if (has_target) { + int nr = perf_thread_map__nr(threads); + pid_t *pids = malloc(nr * sizeof(pid_t)); + + if (pids == NULL) + return -ENOMEM; + + for (int i = 0; i < nr; i++) + pids[i] = perf_thread_map__pid(threads, i); + err = augmented_syscalls__set_target_pids(nr, pids); + free(pids); + } + return err; } @@ -4803,7 +4888,20 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } if (!trace->raw_augmented_syscalls) { - if (trace->trace_syscalls && trace__add_syscall_newtp(trace)) + /* + * The syscall tracepoints may be in the evlist already: + * cmd_trace() adds the pair whenever it prepares BPF + * augmentation, and it then either keeps them, gives up on + * augmentation and leaves them behind, or has + * __augmented_syscalls__ take over the enters and removes only + * the enter one. trace__add_syscall_newtp() adds the pair + * whatever is there, so it is called only when neither is, and + * every event it adds is one the evlist does not already have. + */ + bool have_syscall_tp = trace->syscalls.events.sys_enter != NULL || + trace->syscalls.events.sys_exit != NULL; + + if (trace->trace_syscalls && !have_syscall_tp && trace__add_syscall_newtp(trace)) goto out_error_raw_syscalls; if (trace->trace_syscalls) @@ -4898,7 +4996,7 @@ static int trace__run(struct trace *trace, int argc, const char **argv) err = trace__set_filter_pids(trace); if (err < 0) - goto out_error_mem; + goto out_error_filter_pids; /* * TODO: Initialize for all host binary machine types, not just @@ -4909,7 +5007,7 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (trace->ev_qualifier_ids.nr > 0) { err = trace__set_ev_qualifier_filter(trace); if (err < 0) - goto out_errno; + goto out_error_ev_qualifier; if (trace->syscalls.events.sys_exit) { pr_debug("event qualifier tracepoint filter: %s\n", @@ -5089,14 +5187,32 @@ static int trace__run(struct trace *trace, int argc, const char **argv) "Failed to set filter \"%s\" on event %s: %m\n", evsel->filter, evsel__name(evsel)); goto out_put_evlist; + +out_error_filter_pids: + /* + * Report what actually went wrong. Programming the pid filters + * allocates, but it also writes BPF maps, which fails for reasons of + * its own: -E2BIG when the target has more threads than pids_to_trace + * has room for, say. + */ + fprintf(trace->output, "Failed to set the pid filters: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; + +out_error_ev_qualifier: + /* + * Use the returned error, not errno. Reporting the failure on the way + * out of trace__set_ev_qualifier_filter() goes through the formatted + * output functions, which are free to leave errno describing + * something else by the time it is read here. + */ + fprintf(trace->output, "Failed to set the syscall filters: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; } out_error_mem: fprintf(trace->output, "Not enough memory to run!\n"); goto out_put_evlist; - -out_errno: - fprintf(trace->output, "%m\n"); - goto out_put_evlist; } static int trace__replay(struct trace *trace) @@ -5811,6 +5927,7 @@ int cmd_trace(int argc, const char **argv) .show_arg_names = true, .args_alignment = 70, .trace_syscalls = false, + .syscall_augment = true, .kernel_syscallchains = false, .max_stack = UINT_MAX, .max_events = ULONG_MAX, @@ -5866,6 +5983,8 @@ int cmd_trace(int argc, const char **argv) OPT_CALLBACK_DEFAULT('F', "pf", &trace.trace_pgfaults, "all|maj|min", "Trace pagefaults", parse_pagefaults, "maj"), OPT_BOOLEAN(0, "syscalls", &trace.trace_syscalls, "Trace syscalls"), + OPT_BOOLEAN(0, "syscall-augment", &trace.syscall_augment, + "Augment syscalls with BPF"), OPT_BOOLEAN('f', "force", &trace.force, "don't complain, do it"), OPT_CALLBACK(0, "call-graph", &trace.opts, "record_mode[,record_size]", record_callchain_help, @@ -5988,7 +6107,7 @@ int cmd_trace(int argc, const char **argv) "cgroup monitoring only available in system-wide mode"); } - if (!trace.trace_syscalls) + if (!trace.trace_syscalls || !trace.syscall_augment) goto skip_augmentation; if ((argc >= 1) && (strcmp(argv[0], "record") == 0)) { @@ -6010,11 +6129,34 @@ int cmd_trace(int argc, const char **argv) if (err < 0) goto skip_augmentation; - trace__add_syscall_newtp(&trace); + /* + * The syscall tracepoints are what the augmented events are described + * and paired with: the enter one supplies the format + * __augmented_syscalls__ is read with, and the exit one reports the + * returns, since BPF only takes over the enters. Without them there is + * nothing to augment, so drop the skeleton and carry on with plain + * tracepoints, as the bpf-output failure below does. + */ + if (trace__add_syscall_newtp(&trace)) { + pr_debug("Failed to set up the syscall tracepoints, disabling augmentation\n"); + augmented_syscalls__cleanup(); + goto skip_augmentation; + } err = augmented_syscalls__create_bpf_output(trace.evlist); - if (err == 0) + if (err == 0) { trace.syscalls.events.bpf_output = evlist__last(trace.evlist); + } else { + /* + * augmented_syscalls__prepare() already attached sys_enter and + * sys_exit, which are system wide. Falling through to + * skip_augmentation without undoing that would run a BPF + * program for every syscall on the machine, for the whole + * session, with nothing consuming the output. + */ + pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); + augmented_syscalls__cleanup(); + } skip_augmentation: err = -1; @@ -6070,7 +6212,9 @@ int cmd_trace(int argc, const char **argv) * syscall. */ if (trace.syscalls.events.bpf_output) { - evlist__for_each_entry(trace.evlist, evsel) { + struct evsel *n; + + evlist__for_each_entry_safe(trace.evlist, n, evsel) { bool raw_syscalls_sys_exit = evsel__name_is(evsel, "raw_syscalls:sys_exit"); if (raw_syscalls_sys_exit) { @@ -6078,28 +6222,44 @@ int cmd_trace(int argc, const char **argv) goto init_augmented_syscall_tp; } - if (trace.syscalls.events.bpf_output->priv == NULL && - strstr(evsel__name(evsel), "syscalls:sys_enter")) { + /* + * Match the evsel trace__add_syscall_newtp() made by + * identity rather than by name. It is called + * raw_syscalls:sys_enter, or syscalls:sys_enter on + * kernels too old to have the raw variant, and a + * substring test for the latter also matches the + * per syscall syscalls:sys_enter_SYSCALL tracepoints + * a user can ask for by name. Claiming one of those + * here would take the event the user asked for out of + * the evlist below and describe __augmented_syscalls__ + * with the wrong tracefs format. + */ + if (evsel == trace.syscalls.events.sys_enter) { struct evsel *augmented = trace.syscalls.events.bpf_output; if (evsel__init_augmented_syscall_tp(augmented, evsel) || evsel__init_augmented_syscall_tp_args(augmented)) goto out; /* - * Augmented is __augmented_syscalls__ BPF_OUTPUT event + * Augmented is __augmented_syscalls__ BPF_OUTPUT event. * Above we made sure we can get from the payload the tp fields * that we get from syscalls:sys_enter tracefs format file. + * Since BPF outputs all enter events (both augmented and + * unaugmented) into __augmented_syscalls__, we remove the raw + * sys_enter evsel from evlist so that perf trace only listens + * to __augmented_syscalls__, avoiding duplicate events and + * avoiding kernel tracepoint vetoes. + * + * Because evlist__remove() removes the first evsel (which had + * tracking=true by default), re-designate the tracking event + * so PERF_RECORD_COMM and fork tracking continue to be enabled. */ augmented->handler = trace__sys_enter; - /* - * Now we do the same for the *syscalls:sys_enter event so that - * if we handle it directly, i.e. if the BPF prog returns 0 so - * as not to filter it, then we'll handle it just like we would - * for the BPF_OUTPUT one: - */ - if (evsel__init_augmented_syscall_tp(evsel, evsel) || - evsel__init_augmented_syscall_tp_args(evsel)) - goto out; - evsel->handler = trace__sys_enter; + evlist__remove(trace.evlist, evsel); + evsel__put_and_free_priv(evsel); + trace.syscalls.events.sys_enter = NULL; + evlist__set_tracking_event(trace.evlist, + trace.syscalls.events.sys_exit ?: augmented); + continue; } if (strstarts(evsel__name(evsel), "syscalls:sys_exit_")) { diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 3bc9e28a9b8a..6ca9507ecc02 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -114,6 +114,41 @@ struct pids_filtered { __uint(max_entries, 64); } pids_filtered SEC(".maps"); +/* + * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when + * attached to a process with -p or tracing a specific command workload). + * + * has_pids_to_trace: Set to true if target PID filtering is active. + * When false, all processes are eligible for tracing. + */ +struct pids_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, pid_t); + __type(value, bool); + __uint(max_entries, 1024); +} pids_to_trace SEC(".maps"); + +bool has_pids_to_trace; + +/* + * Hash map storing syscall IDs for filtering (via 'perf trace -e ...'). + * + * has_syscalls_to_trace: Set to true if any syscall filter is active. + * not_syscalls_to_trace: Inverts matching when '!' prefix is used in -e + * (e.g., -e !open,close means trace everything EXCEPT + * open and close; an exclusion blacklist rather than + * an inclusion whitelist). + */ +struct syscalls_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, int); + __type(value, bool); + __uint(max_entries, 1024); +} syscalls_to_trace SEC(".maps"); + +bool has_syscalls_to_trace; +bool not_syscalls_to_trace; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -154,8 +189,8 @@ static inline struct augmented_args_payload *augmented_args_payload(void) static inline int augmented__output(void *ctx, struct augmented_args_payload *args, int len) { - /* If perf_event_output fails, return non-zero so that it gets recorded unaugmented */ - return bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + return 1; } static inline int augmented__beauty_output(void *ctx, void *data, int len) @@ -191,10 +226,21 @@ unsigned int augmented_arg__read_str(struct augmented_arg *augmented_arg, const return augmented_len; } -SEC("tp/raw_syscalls/sys_enter") +/* + * Default sys_enter program for syscalls without pointer argument augmentation. + * Writes the raw struct syscall_enter_args payload into __augmented_syscalls__ + * and returns 1 so the tracepoint is never vetoed in the kernel. + */ +SEC("tp/syscalls/sys_enter_unaugmented") int syscall_unaugmented(struct syscall_enter_args *args) { - return 1; + struct augmented_args_payload *augmented_args = augmented_args_payload(); + + if (augmented_args == NULL) + return 1; + + bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + return augmented__output(args, augmented_args, sizeof(augmented_args->args)); } /* @@ -424,11 +470,41 @@ static pid_t getpid(void) return bpf_get_current_pid_tgid(); } +/* + * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { return bpf_map_lookup_elem(pids, &pid) != NULL; } +/* + * Checks if the current task (thread PID or process TGID) is targeted for tracing. + * Checks both PID (thread ID) and TGID (process ID) so that all threads of a + * target process match. + */ +static inline bool pid_to_trace__has(pid_t pid) +{ + pid_t tgid = bpf_get_current_pid_tgid() >> 32; + + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || + bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; +} + +/* + * Determines if a syscall should be traced based on the filter map: + * - When not_syscalls_to_trace is true: blacklist mode (trace if NOT in map). + * - When not_syscalls_to_trace is false: whitelist mode (trace ONLY if IN map). + */ +static inline bool syscall_to_trace__enabled(int id) +{ + bool in_map = bpf_map_lookup_elem(&syscalls_to_trace, &id) != NULL; + + if (not_syscalls_to_trace) + return !in_map; + return in_map; +} + u64 ZERO = 0; /* @@ -562,6 +638,11 @@ static int augment_sys_enter(void *ctx, struct syscall_enter_args *args) return augmented__beauty_output(ctx, payload, sizeof(struct syscall_enter_args) + output); } +/* + * Main raw_syscalls:sys_enter tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel for + * other concurrent listeners. Filtered events simply do not output to the ring buffer. + */ SEC("tp/raw_syscalls/sys_enter") int sys_enter(struct syscall_enter_args *args) { @@ -576,8 +657,11 @@ int sys_enter(struct syscall_enter_args *args) * initial, non-augmented raw_syscalls:sys_enter payload. */ + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; augmented_args = augmented_args_payload(); if (augmented_args == NULL) @@ -585,25 +669,41 @@ int sys_enter(struct syscall_enter_args *args) bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + if (has_syscalls_to_trace && !syscall_to_trace__enabled(augmented_args->args.syscall_nr)) + return 1; + /* - * Jump to syscall specific augmenter, even if the default one, - * "!raw_syscalls:unaugmented" that will just return 1 to return the - * unaugmented tracepoint payload. + * Jump to syscall specific augmenter. If augmented, augment_sys_enter() + * outputs the payload to __augmented_syscalls__ and returns 0. + * Return 1 so we never veto the kernel tracepoint for other listeners. */ - if (augment_sys_enter(args, &augmented_args->args)) - bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + if (augment_sys_enter(args, &augmented_args->args) == 0) + return 1; - // If not found on the PROG_ARRAY syscalls map, then we're filtering it: - return 0; + bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + + /* + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. + */ + return 1; } +/* + * Main raw_syscalls:sys_exit tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel. + */ SEC("tp/raw_syscalls/sys_exit") int sys_exit(struct syscall_exit_args *args) { struct syscall_exit_args exit_args; + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); /* @@ -613,9 +713,12 @@ int sys_exit(struct syscall_exit_args *args) */ bpf_tail_call(args, &syscalls_sys_exit, exit_args.syscall_nr); /* - * If not found on the PROG_ARRAY syscalls map, then we're filtering it: + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. perf trace's own evsel filter will discard non-matching + * syscalls. */ - return 0; + return 1; } char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index ebb26225fb04..5f15b27264e9 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,6 @@ #include <assert.h> #include <bpf/libbpf.h> +#include <errno.h> #include <internal/xyarray.h> #include <string.h> @@ -12,6 +13,23 @@ static struct augmented_raw_syscalls_bpf *skel; static struct evsel *bpf_output; +/* Set by attach_prog() so the first failure is what gets reported. */ +static int attach_err; + +static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const char *name) +{ + *link = bpf_program__attach(prog); + if (*link) + return 0; + /* + * Save errno before pr_debug(), which formats and writes output and so + * can overwrite it. + */ + attach_err = -errno; + pr_debug("Failed to attach %s BPF program\n", name); + return attach_err; +} + int augmented_syscalls__prepare(void) { struct bpf_program *prog; @@ -37,11 +55,35 @@ int augmented_syscalls__prepare(void) if (err < 0) { libbpf_strerror(err, buf, sizeof(buf)); pr_debug("Failed to load augmented syscalls BPF skeleton: %s\n", buf); + /* + * Tear the skeleton down rather than leaving a half initialized + * one behind. The caller falls back to unaugmented tracing and + * still calls the setters below, which must then do nothing + * instead of failing against a skeleton with no maps. + */ + augmented_syscalls__cleanup(); return err; } - augmented_raw_syscalls_bpf__attach(skel); + /* + * Only sys_enter and sys_exit are attached, the remaining programs are + * reached by tail calls. Attach them explicitly and, on failure, undo + * any partial attachment: leaving sys_enter live on + * raw_syscalls:sys_enter would keep running a BPF program for every + * syscall on the system for a perf trace session that never starts. + */ + if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) + goto out_cleanup; + if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) + goto out_cleanup; + return 0; + +out_cleanup: + err = attach_err; + /* Destroys every link attached above along with the skeleton. */ + augmented_syscalls__cleanup(); + return err; } int augmented_syscalls__create_bpf_output(struct evlist *evlist) @@ -82,22 +124,136 @@ void augmented_syscalls__setup_bpf_output(void) } } +/* + * Add every pid to a pid keyed filter map. + * + * A map with no room left is not treated as a failure. bpf_map__update_elem() + * answers -E2BIG once max_entries keys are present, and a task that did not + * fit is then one the filter does not know about, which is exactly the + * position it would be in had it been created after the map filled up. + * Refusing to trace at all instead would make perf trace unusable against the + * very large targets that are the only way to reach the limit, so say how many + * missed out and carry on with those that did fit. + */ +static int add_pids_to_map(struct bpf_map *map, const char *missing_out_on, + unsigned int nr, pid_t *pids) +{ + unsigned int nr_no_room = 0; + bool value = true; + + for (unsigned int i = 0; i < nr; ++i) { + int err = bpf_map__update_elem(map, &pids[i], sizeof(*pids), + &value, sizeof(value), BPF_ANY); + + if (err == -E2BIG) { + nr_no_room++; + continue; + } + if (err) + return err; + } + + if (nr_no_room) { + pr_warning("Only %u of %u tasks fit in the %s BPF map, %u will not be %s.\n", + nr - nr_no_room, nr, bpf_map__name(map), nr_no_room, + missing_out_on); + } + + return 0; +} + int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) +{ + if (skel == NULL) + return 0; + + return add_pids_to_map(skel->maps.pids_filtered, "filtered out", nr, pids); +} + +/* + * Populate target PIDs in the BPF pids_to_trace map (e.g., for -p <PID> or + * when tracing a specified command workload). + */ +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) +{ + int err; + + if (skel == NULL || nr == 0) + return 0; + + err = add_pids_to_map(skel->maps.pids_to_trace, "traced", nr, pids); + if (err) + return err; + + /* + * Set the flag only once every target is in the map. The BPF programs + * are attached by this point, so flipping it first would have them + * filter against a partially populated map and drop syscalls made by + * the targets that had not been added yet. + */ + skel->bss->has_pids_to_trace = true; + return 0; +} + +int augmented_syscalls__add_target_pid(pid_t pid) +{ + bool value = true; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), BPF_ANY); +} + +int augmented_syscalls__del_target_pid(pid_t pid) +{ + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); +} + +bool augmented_syscalls__has_target_pid(pid_t pid) +{ + bool value; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return false; + + return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), 0) == 0; +} + +/* + * Populate syscalls in the BPF syscalls_to_trace map: + * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace + * all syscalls EXCEPT these). + * false if whitelist mode (trace ONLY these syscalls). + */ +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls) { bool value = true; int err = 0; - if (skel == NULL) + if (skel == NULL || nr == 0) return 0; + skel->bss->not_syscalls_to_trace = not_syscalls; for (size_t i = 0; i < nr; ++i) { - err = bpf_map__update_elem(skel->maps.pids_filtered, &pids[i], - sizeof(*pids), &value, sizeof(value), + err = bpf_map__update_elem(skel->maps.syscalls_to_trace, &syscall_ids[i], + sizeof(int), &value, sizeof(value), BPF_ANY); if (err) - break; + return err; } - return err; + /* + * As for the pid maps, publish the filter only once it is complete: + * in whitelist mode a half filled map would drop syscalls that were + * asked for but not added yet. + */ + skel->bss->has_syscalls_to_trace = true; + return 0; } int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd) @@ -142,4 +298,5 @@ struct bpf_program *augmented_syscalls__find_by_title(const char *name) void augmented_syscalls__cleanup(void) { augmented_raw_syscalls_bpf__destroy(skel); + skel = NULL; } diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index a1cd9a5e0213..5702eda3b469 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <stdbool.h> #include <sys/types.h> struct bpf_program; @@ -13,6 +14,11 @@ int augmented_syscalls__prepare(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__add_target_pid(pid_t pid); +int augmented_syscalls__del_target_pid(pid_t pid); +bool augmented_syscalls__has_target_pid(pid_t pid); +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); struct bpf_program *augmented_syscalls__unaugmented(void); @@ -40,6 +46,34 @@ static inline int augmented_syscalls__set_filter_pids(unsigned int nr __maybe_un return 0; } +static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_unused, + pid_t *pids __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) +{ + return false; +} + +static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, + int *syscall_ids __maybe_unused, + bool not_syscalls __maybe_unused) +{ + return 0; +} + static inline int augmented_syscalls__get_map_fds(int *enter_fd __maybe_unused, int *exit_fd __maybe_unused, int *beauty_fd __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 08/18] perf trace: Handle fork and exit directly in BPF filter maps 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (6 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 07/18] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 09/18] perf trace: Enumerate the target again once BPF is attached Ian Rogers ` (11 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Updating target or filtered PIDs in userspace upon processing PERF_RECORD_FORK and PERF_RECORD_EXIT events introduces latency between event occurrence and userspace BPF map updates. If a newly forked child executes system calls before userspace processes PERF_RECORD_FORK, those syscalls may be dropped by BPF PID filtering. Conversely, if userspace evicts PIDs asynchronously on PERF_RECORD_EXIT, the kernel may recycle a PID before userspace processes the exit event, causing the late eviction to silently drop a newly created task that received the recycled PID. Address this by attaching BTF-typed raw tracepoint BPF programs directly to the scheduler task lifetime tracepoints: 1. Attach SEC("tp_btf/sched_process_fork") (sched_process_fork), which runs in copy_process() in the parent's context before wake_up_new_task() wakes the child. Using tp_btf rather than SEC("tp/sched/sched_process_fork") receives the stable TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) rather than the tracepoint ring-buffer record (TP_STRUCT__entry), whose layout changed in Linux 6.16 when parent_comm and child_comm were converted from 16-byte arrays to 4-byte __data_loc strings. When inherit is enabled and the pid of the thread that called clone() is in pids_to_trace or pids_filtered, insert child->pid into the corresponding map immediately. Because child->pid is task_struct.pid (the global initial-namespace PID), this works accurately across PID namespaces without aliasing host PIDs, and covers both new processes and CLONE_THREAD threads without needing real_parent CO-RE walks or syscall-return heuristics. 2. Attach SEC("tp_btf/sched_process_exit") (sched_process_exit), which runs in do_exit() for every task in its own context, including tasks killed by signals (SIGKILL, SIGSEGV, etc.) and secondary threads torn down implicitly by exit_group. Delete the dying task's PID from pids_to_trace and pids_filtered immediately in kernel space, eliminating both map leaks and any asynchronous userspace eviction window where PID recycling could occur. 3. Attach SEC("tp_btf/sched_process_exec") (sched_process_exec) to follow the one case where a live task's pid changes underneath the maps. When a thread that is not the group leader execs, de_thread() kills the leader and hands the leader's pid, which is the tgid, to the exec'ing thread. The leader dies first, so sched_process_exit() has already dropped exactly the pid the survivor now holds, and the survivor's old entry would be stranded in the map for good. Move the entry from old_pid to p->pid. old_pid is sampled in bprm_execve() before de_thread() runs, so the ordinary group leader exec is a no-op here. Drop the old key before inserting the new one: the move is only a rename, but holding both keys at once needs a spare slot, and on a full map the insert would fail with -E2BIG while the delete still succeeded, losing the task instead of moving it. 4. With every live task registered before its first syscall and evicted in do_exit(), simplify pid_to_trace__has() and pid_filter__has() to single BPF hash map lookups, and move bpf_probe_read_kernel() in sys_exit back after the PID filter checks. 5. Pass the inherit flag from userspace to BPF .rodata via augmented_syscalls__prepare(!trace.opts.no_inherit), and split attaching out of it into augmented_syscalls__attach(), called from trace__run() once the pid, syscall and program array maps have all been programmed. These are system wide programs, so from the instant they attach they alone decide what is traced: attaching at load time, as before, left a window in which a target could fork without sched_process_fork() knowing the parent was a target, and with the userspace fork handling gone there was nothing left to recover it. The scheduler programs are attached ahead of sys_enter and sys_exit for the same reason. Set has_pids_filtered only after populating pids_filtered. A failure to attach ends the session, reporting the error that caused it. There is no falling back to unaugmented tracing by that point: cmd_trace() built the evlist around __augmented_syscalls__ and dropped the sys_enter evsel, so a session that carried on would report nothing at all. Neither did the code this replaces, which ignored the result of augmented_raw_syscalls_bpf__attach() altogether and ran on with programs that had never been attached. 6. Remove the userspace BPF map updates from PERF_RECORD_FORK and PERF_RECORD_EXIT in trace__process_event(), and delete the now-unused augmented_syscalls__{add,del,has}_target_pid() helpers. No coverage is lost with them: those records only come into being once the ring buffers are mapped by evlist__do_mmap() and the events are switched on by evlist__enable(), both of which run after augmented_syscalls__attach() in trace__run(), and they are then acted on later still, whenever the poll loop gets round to them. The scheduler programs therefore go live strictly earlier than the userspace path could ever have reacted. 7. Gate pid_filter__has() on a has_pids_filtered flag in .bss so the common case without --filter-pids performs no map lookups, and size pids_to_trace and pids_filtered at 16384 entries. pids_filtered is grown from 64 because it is no longer just the handful of pids userspace names: sched_process_fork() adds every descendant of those, so a --filter-pids target that forks or is heavily threaded needs the same headroom as a traced one. A fork or exit is still not seen if it happens before the programs are attached, that is between evlist__create_maps() scanning /proc for a -p target and augmented_syscalls__attach(). Such a window is inherent in programming a system wide filter before switching it on, and as above the userspace handling did not cover it either. The cost is not small though. Once the bpf-output event exists cmd_trace() removes the sys_enter evsel from the evlist, so __augmented_syscalls__ is the only source of enter events and a pid that is missing from pids_to_trace is not reported at all. sys_enter returning 1 keeps the kernel tracepoint alive for other subscribers, it does not give perf trace a second path to the event. A target that forks during perf trace's own startup can therefore have that child, and in turn everything the child forks, go untraced for the whole run. The tgid fallback that pid_to_trace__has() used to have would have masked part of this, since a thread missed in the window still shares the tgid of a target userspace did insert. It is not kept because it would also re-admit tasks that sched_process_fork() deliberately skipped: under --no-inherit a new thread of the target is not added to the map, yet it shares the target's tgid and a tgid test cannot tell it apart from one that was. sched_process_fork() does not consult the parent's tgid either, for the same reason. Every thread of a -p target is enumerated from /proc/<pid>/task and inserted under its own pid, and -t names a single thread, so a tgid test adds no reach. What it would add is a child inheriting from a thread that is not traced itself: a sibling of the thread -t selected, or one missed in the attach window whose own syscalls go unreported. Inheritance keys off the pid of the thread that called clone(), exactly as the lookups do. Two of the three ways of selecting what to trace are unaffected: 'perf trace -- cmd' cannot hit this because evlist__prepare_workload() leaves the child blocked on a pipe until evlist__start_workload(), well after the attach, and 'perf trace -a' never sets has_pids_to_trace so it does not filter at all. It is 'perf trace -p' against an already running target that is exposed. Closing that too needs the descendants of the target re-enumerated from /proc after the attach, which the next change does. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 55 +++-- .../bpf_skel/augmented_raw_syscalls.bpf.c | 199 +++++++++++++++++- tools/perf/util/bpf_trace_augment.c | 120 +++++++---- tools/perf/util/trace_augment.h | 28 +-- 4 files changed, 304 insertions(+), 98 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index d403f1a318e3..003fc13ab6d5 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2061,23 +2061,6 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; - case PERF_RECORD_FORK: - if (trace->raw_augmented_syscalls && - (augmented_syscalls__has_target_pid(event->fork.ppid) || - augmented_syscalls__has_target_pid(event->fork.ptid))) { - augmented_syscalls__add_target_pid(event->fork.pid); - } - ret = machine__process_fork_event(machine, event, sample); - break; - case PERF_RECORD_EXIT: - if (trace->raw_augmented_syscalls) { - if (event->fork.pid == event->fork.tid) - augmented_syscalls__del_target_pid(event->fork.pid); - else - augmented_syscalls__del_target_pid(event->fork.tid); - } - ret = machine__process_exit_event(machine, event, sample); - break; default: ret = machine__process_event(machine, event, sample); break; @@ -5015,6 +4998,22 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } } + /* + * Everything the BPF programs filter on is now in their maps, so it is + * safe to let them run. They are attached system wide, so anything + * before this point would have been filtered against a map that was + * still being built up. + * + * Falling back to unaugmented tracing is no longer possible here: the + * evlist was built around __augmented_syscalls__ back in cmd_trace(), + * which is where that decision is taken and where the sys_enter evsel + * was dropped. Fail the session rather than run one that can report + * nothing. + */ + err = augmented_syscalls__attach(); + if (err < 0) + goto out_error_attach; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the @@ -5209,6 +5208,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) fprintf(trace->output, "Failed to set the syscall filters: %s\n", str_error_r(-err, errbuf, sizeof(errbuf))); goto out_put_evlist; + +out_error_attach: + /* + * Use the returned error rather than errno: the failing attach is + * unwound before returning, and the libbpf calls that does can leave + * errno describing something else entirely. + */ + fprintf(trace->output, "Failed to attach the augmented syscalls BPF programs: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; } out_error_mem: fprintf(trace->output, "Not enough memory to run!\n"); @@ -6125,7 +6134,7 @@ int cmd_trace(int argc, const char **argv) goto skip_augmentation; } - err = augmented_syscalls__prepare(); + err = augmented_syscalls__prepare(!trace.opts.no_inherit); if (err < 0) goto skip_augmentation; @@ -6148,11 +6157,11 @@ int cmd_trace(int argc, const char **argv) trace.syscalls.events.bpf_output = evlist__last(trace.evlist); } else { /* - * augmented_syscalls__prepare() already attached sys_enter and - * sys_exit, which are system wide. Falling through to - * skip_augmentation without undoing that would run a BPF - * program for every syscall on the machine, for the whole - * session, with nothing consuming the output. + * Drop the loaded skeleton before falling back to unaugmented + * tracing. Otherwise the setters called from trace__run() would + * still program its maps, and augmented_syscalls__attach() would + * then put system wide BPF programs on raw_syscalls for a + * session with nothing consuming their output. */ pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); augmented_syscalls__cleanup(); diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 6ca9507ecc02..af04c4b3f445 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -9,6 +9,7 @@ #include "vmlinux.h" #include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> #include <linux/limits.h> #define PERF_ALIGN(x, a) __PERF_ALIGN_MASK(x, (typeof(x))(a)-1) @@ -107,25 +108,48 @@ struct augmented_arg { }; }; +/* + * Hash map of PIDs/TGIDs whose events must be discarded, e.g. perf trace's own + * pid, so that tracing doesn't feed back on itself. + * + * has_pids_filtered: set to true only when the map is populated. Checking a + * boolean is much cheaper than a map lookup, and sys_enter + * runs for every syscall on the system, so the common + * "no pids filtered" case must stay on a fast path. + * + * max_entries matches pids_to_trace: userspace only ever names a handful of + * pids here, but sched_process_fork() below adds every descendant of those, + * so a --filter-pids target that forks or is heavily threaded needs the same + * headroom as a traced one. + */ struct pids_filtered { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 64); + __uint(max_entries, 16384); } pids_filtered SEC(".maps"); +bool has_pids_filtered; + /* * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when * attached to a process with -p or tracing a specific command workload). * * has_pids_to_trace: Set to true if target PID filtering is active. * When false, all processes are eligible for tracing. + * + * max_entries bounds how many tasks can be tracked at once. sched_process_exit + * below evicts a task as it dies, whatever it died of, so the map holds live + * tasks rather than growing without bound. It is sized well + * above the thread count of realistic traced workloads; should a workload + * still exceed it, bpf_map_update_elem() fails with -E2BIG and the extra + * tasks are simply not traced rather than anything being corrupted. */ struct pids_to_trace { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 1024); + __uint(max_entries, 16384); } pids_to_trace SEC(".maps"); bool has_pids_to_trace; @@ -149,6 +173,9 @@ struct syscalls_to_trace { bool has_syscalls_to_trace; bool not_syscalls_to_trace; +/* Inherit tracing for child tasks (set to false if --no-inherit is specified) */ +const volatile bool inherit = true; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -471,24 +498,35 @@ static pid_t getpid(void) } /* - * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * Checks if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * + * Children of a filtered task are added to the map by sched_process_fork() + * below, so a plain lookup is all that is needed here. */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { + /* + * Fast path: this runs for every syscall on the system, so when no pid + * is filtered do no work at all rather than failing a lookup. + */ + if (!has_pids_filtered) + return false; + return bpf_map_lookup_elem(pids, &pid) != NULL; } /* - * Checks if the current task (thread PID or process TGID) is targeted for tracing. - * Checks both PID (thread ID) and TGID (process ID) so that all threads of a - * target process match. + * Checks if the current task is targeted for tracing. + * + * Every thread that existed when tracing started was named by the target and + * inserted from userspace, and every task created since was inserted by + * sched_process_fork() below, before it was able to run. So there is nothing + * to derive here, and in particular no need to consult the tgid or walk to the + * parent: a task is traced if and only if it is in the map. */ static inline bool pid_to_trace__has(pid_t pid) { - pid_t tgid = bpf_get_current_pid_tgid() >> 32; - - return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || - bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL; } /* @@ -706,6 +744,7 @@ int sys_exit(struct syscall_exit_args *args) return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); + /* * Jump to syscall specific return augmenter, even if the default one, * "!raw_syscalls:unaugmented" that will just return 1 to return the @@ -721,4 +760,144 @@ int sys_exit(struct syscall_exit_args *args) return 1; } +/* + * Propagate tracing to a newly created task. + * + * tp_btf/sched_process_fork is raised by copy_process(), in the parent's + * context and before the child is woken, so the child is in the maps before it + * can issue its first syscall. That removes the need to inspect real_parent + * when a syscall is seen from an unknown task, which could neither tell a + * genuine descendant from a task merely reparented to a traced init, nor keep + * following a descendant whose parent had already exited. + * + * Using tp_btf rather than tp/sched/sched_process_fork avoids depending on the + * tracepoint ring-buffer record layout (TP_STRUCT__entry), which changed in + * Linux 6.16 when parent_comm and child_comm were converted from fixed 16-byte + * arrays to 4-byte __data_loc strings (shrinking the tracepoint context from + * 48 to 24 bytes and causing BPF_PROG_TYPE_TRACEPOINT attachment to fail with + * -EACCES when accessing higher offsets). Instead, tp_btf receives the stable + * TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) + * directly. + * + * child->pid is task_struct.pid, i.e. the pid in the initial namespace, which + * is what the maps are keyed by. A clone() return value, in contrast, is the + * pid in the caller's namespace and would alias an unrelated host task when a + * containerised workload is traced. + * + * CLONE_THREAD needs no special handling: a new thread arrives here like any + * other task and is inserted under its own pid. + */ +SEC("tp_btf/sched_process_fork") +int BPF_PROG(sched_process_fork, struct task_struct *parent, struct task_struct *child) +{ + pid_t parent_pid, child_pid; + bool val = true; + + if (!inherit) + return 0; + + /* + * Inherit from the thread that called clone() and from nothing else. + * The maps name individual tasks: pid_to_trace__has() and + * pid_filter__has() look up a task's own pid and nothing more, and + * every thread of a -p target is enumerated from /proc/<pid>/task and + * inserted in its own right, so a traced thread is always here under + * its own key. Consulting the parent's tgid as well would let a child + * inherit from a thread that is not itself traced, which is precisely + * what 'perf trace -t <tid>' asked to leave out, and would re-admit + * descendants of a thread that sched_process_fork() skipped or that + * was missed while the programs were being attached, while still not + * tracing that thread itself. + */ + parent_pid = parent->pid; + child_pid = child->pid; + + if (has_pids_to_trace && + bpf_map_lookup_elem(&pids_to_trace, &parent_pid) != NULL) + bpf_map_update_elem(&pids_to_trace, &child_pid, &val, BPF_ANY); + + if (has_pids_filtered && + bpf_map_lookup_elem(&pids_filtered, &parent_pid) != NULL) + bpf_map_update_elem(&pids_filtered, &child_pid, &val, BPF_ANY); + + return 0; +} + +/* + * Drop a dying task from the maps. + * + * tp_btf/sched_process_exit is raised by do_exit() for every task, in its own + * context, so unlike hooking the exit and exit_group syscalls this also covers + * tasks killed by a signal and threads torn down implicitly by exit_group. + * + * Doing it here rather than from the userspace PERF_RECORD_EXIT handler also + * means there is no window between the task dying and the map being updated, + * during which the kernel could recycle the pid and the late eviction silently + * stop tracing whichever new task received it. + * + * Each thread is reported separately, including the group leader, whose pid is + * the thread group's tgid, so one delete per map covers both uses of the key. + */ +SEC("tp_btf/sched_process_exit") +int BPF_PROG(sched_process_exit, struct task_struct *p) +{ + pid_t pid = p->pid; + + bpf_map_delete_elem(&pids_to_trace, &pid); + bpf_map_delete_elem(&pids_filtered, &pid); + + return 0; +} + +/* + * Follow a task whose pid changed under it. + * + * When a thread that is not the thread group leader execs, de_thread() kills + * the rest of the group and then hands the leader's pid, which is the tgid, to + * the exec'ing thread. The leader dies first, so sched_process_exit() above + * has already dropped that pid from the maps, and the survivor is now keyed by + * a pid nothing knows about while its original entry is left behind for good. + * + * Move the entry across so the task stays tracked and nothing is leaked. + * old_pid is sampled in bprm_execve() before de_thread() runs, so for the + * common case of the group leader exec'ing it simply equals p->pid and there + * is nothing to do. + */ +SEC("tp_btf/sched_process_exec") +int BPF_PROG(sched_process_exec, struct task_struct *p, pid_t old_pid) +{ + pid_t pid = p->pid; + bool val = true; + + if (pid == old_pid) + return 0; + + /* + * Drop the old key before adding the new one. The maps are bounded and + * the move is only ever a rename, but inserting first needs a spare + * slot for as long as both keys are present: on a full map that insert + * fails with -E2BIG while the delete still succeeds, which would lose + * the task rather than move it. Deleting first frees the slot the + * insert goes on to use. The task is mid exec and issues no syscalls in + * between, so the gap is not observable. + * + * There is no atomic rename for a hash map, so on a map that is exactly + * full this narrows the window rather than closing it: a fork on + * another CPU can still take the freed slot before the insert below + * runs, and the task is then dropped just as any other task is once the + * map is full. + */ + if (bpf_map_lookup_elem(&pids_to_trace, &old_pid) != NULL) { + bpf_map_delete_elem(&pids_to_trace, &old_pid); + bpf_map_update_elem(&pids_to_trace, &pid, &val, BPF_ANY); + } + + if (bpf_map_lookup_elem(&pids_filtered, &old_pid) != NULL) { + bpf_map_delete_elem(&pids_filtered, &old_pid); + bpf_map_update_elem(&pids_filtered, &pid, &val, BPF_ANY); + } + + return 0; +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index 5f15b27264e9..44f30dba5469 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -30,7 +30,7 @@ static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const c return attach_err; } -int augmented_syscalls__prepare(void) +int augmented_syscalls__prepare(bool inherit) { struct bpf_program *prog; char buf[128]; @@ -42,12 +42,18 @@ int augmented_syscalls__prepare(void) return -errno; } + skel->rodata->inherit = inherit; + /* - * Disable attaching the BPF programs except for sys_enter and - * sys_exit that tail call into this as necessary. + * Disable attaching the BPF programs other than those attached + * explicitly by augmented_syscalls__attach(), the rest are reached by + * tail calls. */ bpf_object__for_each_program(prog, skel->obj) { - if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit) + if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit && + prog != skel->progs.sched_process_fork && + prog != skel->progs.sched_process_exit && + prog != skel->progs.sched_process_exec) bpf_program__set_autoattach(prog, /*autoattach=*/false); } @@ -65,13 +71,42 @@ int augmented_syscalls__prepare(void) return err; } + return 0; +} + +int augmented_syscalls__attach(void) +{ + int err; + + if (skel == NULL) + return 0; + /* - * Only sys_enter and sys_exit are attached, the remaining programs are - * reached by tail calls. Attach them explicitly and, on failure, undo - * any partial attachment: leaving sys_enter live on - * raw_syscalls:sys_enter would keep running a BPF program for every - * syscall on the system for a perf trace session that never starts. + * Attaching is deliberately separate from, and a lot later than, + * loading: these are system wide tracepoint programs, so from the + * moment they are attached they are the only thing deciding which + * tasks and syscalls are traced. Going live before the pid and + * syscall maps are populated would mean a target that forked in the + * meantime was never picked up by sched_process_fork() below. + * + * Attach explicitly, so that a failure part way through can undo what + * came before it: leaving sys_enter live on raw_syscalls:sys_enter + * would keep running a BPF program for every syscall on the system for + * a perf trace session that never starts. + * + * The scheduler programs maintain the pid maps, and are attached first + * so that no fork, exit or exec can be missed between sys_enter going + * live and the maps being maintained. */ + if (attach_prog(&skel->links.sched_process_fork, skel->progs.sched_process_fork, + "sched_process_fork")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exit, skel->progs.sched_process_exit, + "sched_process_exit")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exec, skel->progs.sched_process_exec, + "sched_process_exec")) + goto out_cleanup; if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) goto out_cleanup; if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) @@ -83,6 +118,13 @@ int augmented_syscalls__prepare(void) err = attach_err; /* Destroys every link attached above along with the skeleton. */ augmented_syscalls__cleanup(); + /* + * Tearing the skeleton down closes file descriptors and frees memory, + * either of which may overwrite errno. Restore it so that a caller + * reporting this with "%m" describes the attach failure rather than + * whatever the teardown happened to do last. + */ + errno = -err; return err; } @@ -164,10 +206,28 @@ static int add_pids_to_map(struct bpf_map *map, const char *missing_out_on, int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) { - if (skel == NULL) + int err; + + if (skel == NULL || nr == 0) return 0; - return add_pids_to_map(skel->maps.pids_filtered, "filtered out", nr, pids); + err = add_pids_to_map(skel->maps.pids_filtered, "filtered out", nr, pids); + if (err) + return err; + + /* + * Publish the filter only now that the map is populated. + * augmented_syscalls__attach() has not run yet, so nothing is reading + * either of them, but keeping the flag and the map consistent means + * the ordering stays correct however the callers are rearranged. + * + * The flag also tells the BPF program that the pids_filtered map is in + * use. Without it the program would have to look up every task in an + * empty map, on every syscall on the system, to find out that nothing + * is filtered. + */ + skel->bss->has_pids_filtered = true; + return 0; } /* @@ -186,45 +246,15 @@ int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) return err; /* - * Set the flag only once every target is in the map. The BPF programs - * are attached by this point, so flipping it first would have them - * filter against a partially populated map and drop syscalls made by - * the targets that had not been added yet. + * Set the flag only once every target is in the map, so that the two + * are never inconsistent. Publishing it first would, once the + * programs are attached, have them filter against a partially + * populated map and drop syscalls made by targets not yet added. */ skel->bss->has_pids_to_trace = true; return 0; } -int augmented_syscalls__add_target_pid(pid_t pid) -{ - bool value = true; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), BPF_ANY); -} - -int augmented_syscalls__del_target_pid(pid_t pid) -{ - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); -} - -bool augmented_syscalls__has_target_pid(pid_t pid) -{ - bool value; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return false; - - return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), 0) == 0; -} - /* * Populate syscalls in the BPF syscalls_to_trace map: * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 5702eda3b469..ad992f5fa726 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -10,14 +10,12 @@ struct evlist; #ifdef HAVE_BPF_SKEL -int augmented_syscalls__prepare(void); +int augmented_syscalls__prepare(bool inherit); +int augmented_syscalls__attach(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); -int augmented_syscalls__add_target_pid(pid_t pid); -int augmented_syscalls__del_target_pid(pid_t pid); -bool augmented_syscalls__has_target_pid(pid_t pid); int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); @@ -26,11 +24,16 @@ void augmented_syscalls__cleanup(void); #else /* !HAVE_BPF_SKEL */ -static inline int augmented_syscalls__prepare(void) +static inline int augmented_syscalls__prepare(bool inherit __maybe_unused) { return -1; } +static inline int augmented_syscalls__attach(void) +{ + return 0; +} + static inline int augmented_syscalls__create_bpf_output(struct evlist *evlist __maybe_unused) { return -1; @@ -52,21 +55,6 @@ static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_un return 0; } -static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) -{ - return false; -} - static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, int *syscall_ids __maybe_unused, bool not_syscalls __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 09/18] perf trace: Enumerate the target again once BPF is attached 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (7 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 08/18] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 10/18] perf trace: Drop targets that died before they were filtered Ian Rogers ` (10 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz evlist__create_maps() reads the target out of /proc, and the BPF sched_process_fork() program only sees what is cloned once it is attached. A task the target creates between the two is in neither, and since cmd_trace() drops the sys_enter evsel in favour of __augmented_syscalls__ there is no other source of enter events. Such a task, and in turn everything it forks, goes unreported for the rest of the session. Read the target out of /proc once more, after the attach. Whatever existed before the programs went live is there to be found, and whatever is created after it is sched_process_fork()'s to add, so between them nothing is left out. Doing this before the attach instead would only move the window rather than close it. The enumeration follows the same rule as the BPF programs, which is that the maps name individual tasks: - -p names a process, so its thread group is read from /proc/<pid>/task. - -t names a thread, which is taken on its own. Expanding it to its thread group would trace the siblings it asked to leave out. - Descendants come from task->children, read through /proc/<pid>/task/<tid>/children. A forked task leads a thread group of its own, so each one found is walked in turn and a tree of any depth is covered. New threads are not listed there, copy_process() gives a CLONE_THREAD child the real_parent of its creator rather than the creator itself, but the thread group walk above has them. The other two ways of choosing what to trace need nothing, for the same reasons the window never affected them: 'perf trace -a' does not filter on pid at all, and evlist__prepare_workload() keeps a workload blocked on a pipe until evlist__start_workload(), well after the attach. A target that exits during startup is not an error. Reading /proc for a task that has gone fails with ENOENT, and a task directory that is read but has nothing in it sets nothing at all, so errno is cleared before the enumeration and only an allocation failure is passed back. Anything else leaves the tasks that evlist__create_maps() already found in the map, which sched_process_exit() takes out again as they die, and the session runs on rather than being ended over a target that was going to stop producing events anyway. What is left is smaller and no longer lasts. A task found here may have made syscalls between sys_enter going live and it being added to the map, and those are not reported, but it is traced from that point on. On a kernel built without CONFIG_PROC_CHILDREN the children files are absent and descendants cannot be named, leaving the threads of the target, which are still picked up. pid_t, PATH_MAX, FILE and the directory reading are all used directly by the new code, so <sys/types.h>, <limits.h>, <stdio.h> and <dirent.h> are included rather than relied upon to arrive through another header. Nothing is read out of /proc twice. A pid is in the collection only because the task directory holding it was read, and that directory holds the whole thread group, so both the directory and the children files of everything in it have been read already. 'perf trace -p' needs this: the thread map names every thread of the target, each of them is queued as something to expand, and they all stand for the same directory, so a target of N threads was read N times over and a children file was opened N squared times. On a 64 thread target that is 4097 of them, against 65 once the ones already read are left alone. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 248 +++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 003fc13ab6d5..8a5dbf144540 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -15,6 +15,7 @@ */ #include "util/record.h" +#include <api/fs/fs.h> #include <api/fs/tracing_path.h> #ifdef HAVE_LIBBPF_SUPPORT #include <bpf/bpf.h> @@ -65,11 +66,15 @@ #include "trace_augment.h" #include "dwarf-regs.h" +#include <dirent.h> #include <errno.h> #include <sys/stat.h> +#include <sys/types.h> #include <inttypes.h> +#include <limits.h> #include <poll.h> #include <signal.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <linux/err.h> @@ -4648,6 +4653,239 @@ static int trace__set_filter_pids(struct trace *trace) return err; } +/* A list of pids that grows as it is added to, holding each pid once. */ +struct pid_list { + pid_t *entries; + size_t nr; + size_t allocated; +}; + +static bool pid_list__has(const struct pid_list *list, pid_t pid) +{ + for (size_t i = 0; i < list->nr; i++) { + if (list->entries[i] == pid) + return true; + } + return false; +} + +/* Append pid, unless it is already there. */ +static int pid_list__add(struct pid_list *list, pid_t pid) +{ + if (pid_list__has(list, pid)) + return 0; + + if (list->nr == list->allocated) { + size_t allocated = list->allocated ? list->allocated * 2 : 32; + pid_t *entries = realloc(list->entries, allocated * sizeof(*entries)); + + if (entries == NULL) + return -ENOMEM; + + list->entries = entries; + list->allocated = allocated; + } + + list->entries[list->nr++] = pid; + return 0; +} + +static void pid_list__exit(struct pid_list *list) +{ + zfree(&list->entries); + list->nr = 0; + list->allocated = 0; +} + +/* + * Append the tasks tid has forked to tgids. + * + * task->children holds what a task forked, and a forked task leads a thread + * group of its own, so each is something to expand in turn. New threads are + * not listed: copy_process() gives a CLONE_THREAD child the real_parent of + * its creator rather than the creator itself, so a thread is a sibling of the + * task that created it. Those are enumerated from the task directory instead. + */ +static int pid_list__add_children(struct pid_list *tgids, pid_t tid) +{ + char path[PATH_MAX]; + pid_t child; + FILE *fp; + int err = 0; + + scnprintf(path, sizeof(path), "%s/%d/task/%d/children", + procfs__mountpoint(), tid, tid); + fp = fopen(path, "r"); + /* + * Absent if the task exited, and on a kernel built without + * CONFIG_PROC_CHILDREN. Neither is worth failing for: what is missed + * is a task that has gone away, or descendants the kernel will not + * name. + */ + if (fp == NULL) + return 0; + + while (fscanf(fp, "%d", &child) == 1) { + err = pid_list__add(tgids, child); + if (err) + break; + } + + fclose(fp); + return err; +} + +/* + * Collect the tasks to trace: the target, its threads, and everything they + * have forked. + * + * tgids is the queue of thread groups still to expand. It is walked as it + * grows, so a child found here has its own children picked up in a later + * pass and the depth of the tree does not matter. The walk terminates + * because a task cannot be its own ancestor and pid_list__add() ignores a + * pid that is already listed. + */ +static int trace__collect_target_pids(struct trace *trace, struct pid_list *pids) +{ + struct target *target = &trace->opts.target; + /* + * -p names processes, so the whole thread group is a target. -t names + * threads, and expanding one to its group would trace the siblings + * that were deliberately left out. + */ + bool whole_group = target->pid != NULL; + struct perf_thread_map *threads; + struct pid_list tgids = {}; + int err = 0; + + /* Enumerate the target as evlist__create_maps() did, but now. */ + errno = 0; + threads = thread_map__new_str(target->pid, target->tid, target->per_thread); + if (threads == NULL) { + char bf[128]; + + /* + * A target that exited while perf trace was starting up shows + * up here as a failure to read /proc/<pid>/task, with scandir() + * setting ENOENT, or as a task directory that is read but has + * nothing in it, which sets nothing at all and is why errno is + * cleared above. Neither is worth ending the session for: the + * tasks the target had are already in the map from + * evlist__create_maps() and sched_process_exit() takes them out + * again as they die. Carry on with what is known and let only + * an allocation failure through, matching how the pid_list + * additions below are treated. + */ + if (errno == ENOMEM) + return -ENOMEM; + + pr_debug("Couldn't enumerate the target again (%s), tracing the tasks already known\n", + errno == 0 ? "it exited" : str_error_r(errno, bf, sizeof(bf))); + return 0; + } + + for (int i = 0; i < perf_thread_map__nr(threads); i++) { + pid_t pid = perf_thread_map__pid(threads, i); + + err = pid_list__add(whole_group ? &tgids : pids, pid); + /* A thread named by -t is not expanded, but its children are. */ + if (!err && !whole_group) + err = pid_list__add_children(&tgids, pid); + if (err) + goto out; + } + + for (size_t i = 0; i < tgids.nr; i++) { + pid_t tgid = tgids.entries[i]; + char path[PATH_MAX]; + struct dirent *dent; + DIR *tasks; + + /* + * A pid is in pids only because the task directory holding it + * was read, and that directory holds the whole thread group, + * so this one has been read already. 'perf trace -p' relies on + * this: the thread map names every thread of the target, each + * of them is queued here, and they all stand for the same + * directory, which would otherwise be read once per thread. + */ + if (pid_list__has(pids, tgid)) + continue; + + scnprintf(path, sizeof(path), "%s/%d/task", procfs__mountpoint(), tgid); + tasks = opendir(path); + if (tasks == NULL) + continue; /* Exited between being named and being read. */ + + while ((dent = readdir(tasks)) != NULL) { + char *end; + pid_t tid = strtol(dent->d_name, &end, 10); + + /* Skip "." and "..". */ + if (*end != '\0') + continue; + + /* Seen before, so its children have been read too. */ + if (pid_list__has(pids, tid)) + continue; + + err = pid_list__add(pids, tid); + if (!err) + err = pid_list__add_children(&tgids, tid); + if (err) + break; + } + + closedir(tasks); + if (err) + goto out; + } +out: + perf_thread_map__put(threads); + pid_list__exit(&tgids); + return err; +} + +/* + * Trace the tasks that appeared while perf trace was starting up. + * + * evlist__create_maps() enumerated the target from /proc, and + * sched_process_fork() only sees what is cloned once it is attached. A task + * created in between is in neither, and since cmd_trace() drops the sys_enter + * evsel in favour of __augmented_syscalls__ it would go unreported for the + * whole session. + * + * Read the target out of /proc again now that the programs are attached. + * Whatever came before the attach is in /proc to be found, and whatever comes + * after it is sched_process_fork()'s to add, so between them nothing is left + * out. + * + * Doing this after the attach rather than before it is what makes that true; + * before it would only move the window. The cost is that a task found here + * may have made syscalls between sys_enter going live and it being added + * below, and those are not reported. It is traced from that point on. + */ +static int trace__set_startup_pids(struct trace *trace) +{ + struct pid_list pids = {}; + int err; + + /* + * Nothing to do without a target: 'perf trace -a' does not filter on + * pid at all, and evlist__prepare_workload() keeps a workload blocked + * on a pipe until evlist__start_workload(), well after the attach. + */ + if (!target__has_task(&trace->opts.target)) + return 0; + + err = trace__collect_target_pids(trace, &pids); + if (!err) + err = augmented_syscalls__set_target_pids(pids.nr, pids.entries); + + pid_list__exit(&pids); + return err; +} + static int __trace__deliver_event(struct trace *trace, union perf_event *event) { struct evlist *evlist = trace->evlist; @@ -5014,6 +5252,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (err < 0) goto out_error_attach; + /* + * With the programs live, anything the target created while they were + * being set up is now sched_process_fork()'s to keep track of, but it + * was not there to see it appear. Enumerate the target once more to + * pick those up. + */ + err = trace__set_startup_pids(trace); + if (err < 0) + goto out_error_filter_pids; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 10/18] perf trace: Drop targets that died before they were filtered 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (8 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 09/18] perf trace: Enumerate the target again once BPF is attached Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 11/18] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers ` (9 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz sched_process_exit() takes a task out of pids_to_trace and pids_filtered as it dies, which keeps the maps holding live tasks rather than growing for the length of the session. It only finds a task there if userspace put it there first, and userspace writes a pid some time after reading it: - trace__set_filter_pids() writes the thread map and the --filter-pids list before the programs are attached, so for those there is nothing watching them die at all. - trace__set_startup_pids() writes what it enumerated after the attach, which is a much smaller gap but still a gap. A task that dies inside one of those windows is never evicted, because the delete that would have done it ran while the map had nothing to delete. Its pid then sits in the map for the rest of the session: it holds one of a fixed number of entries, and pids are reused, so the unrelated task that eventually receives it is treated as the target, or as a task the user asked to leave out and is then silently not reported. Walk both maps once the last write to them is done and remove the tasks that are not alive. Sweeping the maps rather than the lists that were written to them covers every way a pid can get in, including the pid of perf itself and of the terminal it was started from, which trace__set_filter_loop_pids() adds without recording anywhere. A task found alive here and dying later is one sched_process_exit() can see and evict, so a single pass is enough, and the tasks the BPF programs add for themselves need nothing: sched_process_fork() inserts a task before it has run, so it cannot have died beforehand. Deleting the key the walk is standing on leaves bpf_map__get_next_key() to resume from a key that is no longer there, which htab_map_get_next_key() answers with the first key of the whole map, starting the walk over. The walk therefore only ever stands on a key it has found alive, which is one it has just decided to keep, so nothing it deletes is ever the key it would go on to ask from. A key deleted while the walk stands on nothing is not a problem either: the next request is for the first key of the map, and the deleted one is no longer it. sched_process_exit() deletes keys while this runs and can take out the key the walk is standing on, which there is no way to prevent or to notice. The walk is bounded at max_entries steps so that it ends however often that happens: the map holds no more than that many keys, so a walk that has taken as many steps has either seen them all or been restarted, and the bound is what stops a walk that keeps being restarted from going round for ever. Ending on the bound can leave a dead task behind, which costs one entry of a great many until the session ends, where not ending would cost the session itself. A task is taken to be gone when its /proc entry is not there, and also when /proc reports it as Z or X. Both mean it is in or past do_exit(), which is where sched_process_exit() runs, so waiting for a zombie to be reaped before dropping it would only keep a dead task in the map for longer. Everything else, including not being able to tell, leaves the task in the map. The two mistakes are not equal: a dead task left there holds an entry nothing needs, while a live one taken out stops being traced for the rest of the session, so a read that fails for perf's own reasons, being out of file descriptors or of memory, is not read as the task having exited. Reading that state means finding the end of the command name, which do_task_stat() writes out unescaped, unlike /proc/<pid>/status: it holds whatever the task called itself, ')' and newlines included. The whole of /proc/<pid>/stat is read rather than a line of it, because a line stops at the first embedded newline, short of the name's closing ')', and the last ')' in the file is taken as that one, since no field after the name has parentheses in it. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 76 ++++++++++++++++++++++++--- tools/perf/util/bpf_trace_augment.c | 79 +++++++++++++++++++++++++++++ tools/perf/util/trace_augment.h | 6 +++ 3 files changed, 153 insertions(+), 8 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 8a5dbf144540..f2425a291eeb 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4846,6 +4846,59 @@ static int trace__collect_target_pids(struct trace *trace, struct pid_list *pids return err; } +/* + * Is the task still running? + * + * A pid with no /proc entry is gone, and one reported as Z (zombie) or X (dead) + * is in or past do_exit(), which is where sched_process_exit() runs. Either way + * the BPF programs will never hear about it again. + * + * Anything that is not one of those, including being unable to tell, is taken + * as alive. The two mistakes are not equal: a dead task left in the map holds + * an entry that nothing needs, while a live one taken out of it stops being + * traced for the rest of the session. + */ +static bool trace__task_is_alive(pid_t pid) +{ + char path[PATH_MAX]; + const char *state; + char *stat = NULL; + bool alive = true; + size_t len; + int err; + + scnprintf(path, sizeof(path), "%s/%d/stat", procfs__mountpoint(), pid); + err = filename__read_str(path, &stat, &len); + if (err) { + /* + * ENOENT and ESRCH are the task being gone, which is what this + * is looking for. Any other error is perf's own, running out + * of file descriptors or of memory, and says nothing about the + * task. + */ + return err != -ENOENT && err != -ESRCH; + } + + /* + * The state is the field after the command name. do_task_stat() writes + * that name out as it is, so it can hold anything a task cares to call + * itself, ')' and newlines included. Nothing after it has parentheses, + * so the last ')' in the file is the one that closes it; the whole file + * is read rather than a line of it because a line stops at the first of + * those newlines, short of the ')' that is being looked for. + * + * A read that returns something the state cannot be read out of, such + * as the empty result of the task exiting midway through it, leaves the + * task alive by the rule above. + */ + state = strrchr(stat, ')'); + if (state != NULL && state[1] == ' ') + alive = state[2] != 'Z' && state[2] != 'X'; + + free(stat); + return alive; +} + /* * Trace the tasks that appeared while perf trace was starting up. * @@ -4868,19 +4921,26 @@ static int trace__collect_target_pids(struct trace *trace, struct pid_list *pids static int trace__set_startup_pids(struct trace *trace) { struct pid_list pids = {}; - int err; + int err = 0; /* - * Nothing to do without a target: 'perf trace -a' does not filter on - * pid at all, and evlist__prepare_workload() keeps a workload blocked - * on a pipe until evlist__start_workload(), well after the attach. + * Only a target is enumerated: 'perf trace -a' does not filter on pid + * at all, and evlist__prepare_workload() keeps a workload blocked on a + * pipe until evlist__start_workload(), well after the attach. */ - if (!target__has_task(&trace->opts.target)) - return 0; + if (target__has_task(&trace->opts.target)) { + err = trace__collect_target_pids(trace, &pids); + if (!err) + err = augmented_syscalls__set_target_pids(pids.nr, pids.entries); + } - err = trace__collect_target_pids(trace, &pids); + /* + * Every session gets the sweep, target or not: --filter-pids names + * tasks to leave out with no target of its own, and those are written + * to a map of their own, before the attach and with the same race. + */ if (!err) - err = augmented_syscalls__set_target_pids(pids.nr, pids.entries); + err = augmented_syscalls__prune_dead_pids(trace__task_is_alive); pid_list__exit(&pids); return err; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index 44f30dba5469..4f93f5c062c1 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -255,6 +255,85 @@ int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) return 0; } +/* + * Remove from a pid keyed map every task that is no longer alive. + * + * Deleting the key the walk is standing on leaves bpf_map__get_next_key() to + * resume from a key that is no longer there, which htab_map_get_next_key() + * answers with the first key of the whole map, starting the walk over. The + * cursor is therefore only ever moved onto a key that has been found alive, + * which is a key this function has just decided to keep, so nothing it does + * can send itself back to the beginning. + * + * sched_process_exit() deletes keys while this runs and can take out the + * cursor, which there is no way to prevent or to notice. The walk is bounded + * at max_entries steps so that it ends however often that happens: the map + * holds no more than that many keys, so a walk that has taken as many steps + * has either seen them all or been restarted, and the bound is what stops a + * repeatedly restarted walk from going round for ever. Stopping there can + * leave a dead task behind, which costs one entry of a great many until the + * session ends, where not stopping would cost the session itself. + */ +static int prune_dead_map_pids(struct bpf_map *map, bool (*is_alive)(pid_t pid)) +{ + size_t max_entries = bpf_map__max_entries(map); + pid_t cursor, key; + bool have_cursor = false; + + for (size_t step = 0; step < max_entries; step++) { + int err; + + /* Anything other than success means there is no next key. */ + if (bpf_map__get_next_key(map, have_cursor ? &cursor : NULL, + &key, sizeof(key)) != 0) + break; + + if (is_alive(key)) { + cursor = key; + have_cursor = true; + continue; + } + + err = bpf_map__delete_elem(map, &key, sizeof(key), /*flags=*/0); + /* + * ENOENT means the key went away while this was running, which + * is sched_process_exit() doing the same job. + */ + if (err && err != -ENOENT) + return err; + + /* + * The cursor stays where it was, which is either a key that is + * still in the map or unset. Unset asks the next step for the + * first key of the map, and the one just deleted is no longer + * it, so the walk moves on either way. + */ + } + + return 0; +} + +/* + * Remove the tasks that are no longer alive from both pid maps. + * + * has_pids_to_trace and has_pids_filtered are left alone. They say that the + * maps are in use rather than that they have anything in them, and clearing + * the first would turn a targeted session into a system wide one. + */ +int augmented_syscalls__prune_dead_pids(bool (*is_alive)(pid_t pid)) +{ + int err; + + if (skel == NULL) + return 0; + + err = prune_dead_map_pids(skel->maps.pids_to_trace, is_alive); + if (!err) + err = prune_dead_map_pids(skel->maps.pids_filtered, is_alive); + + return err; +} + /* * Populate syscalls in the BPF syscalls_to_trace map: * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index ad992f5fa726..87dbf8c46c1e 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -16,6 +16,7 @@ int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__prune_dead_pids(bool (*is_alive)(pid_t pid)); int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); @@ -55,6 +56,11 @@ static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_un return 0; } +static inline int augmented_syscalls__prune_dead_pids(bool (*is_alive)(pid_t pid) __maybe_unused) +{ + return 0; +} + static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, int *syscall_ids __maybe_unused, bool not_syscalls __maybe_unused) -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 11/18] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (9 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 10/18] perf trace: Drop targets that died before they were filtered Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 12/18] perf test common: Only disable probes in clear_all_probes Ian Rogers ` (8 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz test_task_analyzer.sh writes perf.data and temporary files directly into the current working directory, causing collisions when running tests in parallel. As a temporary measure until `perf script report` supports an input file option, resolve perfdir to an absolute path, change directory into $tmpdir for the test duration, and clean up in the exit trap. Remove the (exclusive) tag so the test runs in parallel. perfdir is derived from $0, which may be relative, so it has to be resolved before the cd into $tmpdir, otherwise both PERF_EXEC_PATH and the cleanup trap point at paths that no longer exist. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/test_task_analyzer.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/perf/tests/shell/test_task_analyzer.sh b/tools/perf/tests/shell/test_task_analyzer.sh index 0314412e63b4..443b88957b17 100755 --- a/tools/perf/tests/shell/test_task_analyzer.sh +++ b/tools/perf/tests/shell/test_task_analyzer.sh @@ -1,8 +1,18 @@ #!/bin/bash -# perf script task-analyzer tests (exclusive) +# perf script task-analyzer tests # SPDX-License-Identifier: GPL-2.0 -tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) +# Resolve the source directory before changing the working directory below, +# $0 may be a relative path and would no longer resolve from $tmpdir. +perfdir=$(cd "$(dirname "$0")/../.." && pwd) + +tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) || exit 1 +# The cleanup trap is only installed further down, once the functions it +# calls have been defined, so tidy up by hand if this cd fails. +cd "$tmpdir" || { + rmdir "$tmpdir" + exit 1 +} # TODO: perf script report only supports input from the CWD perf.data file, make # it support input from any file. perfdata="perf.data" @@ -11,7 +21,6 @@ csvsummary="$tmpdir/csvsummary" err=0 # set PERF_EXEC_PATH to find scripts in the source directory -perfdir=$(dirname "$0")/../.. if [ -e "$perfdir/scripts/python/Perf-Trace-Util" ]; then export PERF_EXEC_PATH=$perfdir fi @@ -20,8 +29,9 @@ fi export ASAN_OPTIONS=detect_leaks=0 cleanup() { - rm -f "${perfdata}" - rm -f "${perfdata}".old + # Step out of $tmpdir before removing it. The removal uses an absolute + # path and works from anywhere, so a failure to cd must not skip it. + cd "$perfdir" || cd / || true rm -rf "$tmpdir" trap - exit term int -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 12/18] perf test common: Only disable probes in clear_all_probes 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (10 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 11/18] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 13/18] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers ` (7 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz clear_all_probes() began by writing 0 to /sys/kernel/debug/tracing/events/enable, which disables every tracepoint on the system rather than just the probes the probe tests created. When tests run in parallel that also silences the events of any concurrent perf record, perf trace or ftrace session, so unrelated tests lose the events they are waiting for and fail. The write cannot simply be dropped. Clearing kprobe_events or uprobe_events is all or nothing: dyn_events_release_all() walks every probe of that type first and returns -EBUSY without removing any of them if it finds one that is still enabled, where enabled means TP_FLAG_TRACE from tracefs or TP_FLAG_PROFILE from a perf session. A probe left enabled through tracefs would therefore block the whole clear and leave stale probes behind to collide with the next run. Disable the probes and only the probes. The set to disable is taken from the kprobe_events and uprobe_events listings rather than from a guess at which groups perf uses, so it matches what dyn_events_release_all() is going to inspect: a probe an unrelated session left enabled in, say, the kprobes group would otherwise still abort the clear. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/common/init.sh | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/common/init.sh b/tools/perf/tests/shell/common/init.sh index cbfc78bec974..7c2ca74298ca 100644 --- a/tools/perf/tests/shell/common/init.sh +++ b/tools/perf/tests/shell/common/init.sh @@ -130,9 +130,40 @@ check_uprobes_available() test -e /sys/kernel/debug/tracing/uprobe_events } +# Disable every kprobe and uprobe event. The listings name each probe as +# "TYPE:GROUP/EVENT ARGS...", for instance "p:probe/vfs_read vfs_read", and +# events/GROUP/EVENT/enable is the switch for it. +disable_all_probes() +{ + PROBE_SPECS=`cat /sys/kernel/debug/tracing/kprobe_events \ + /sys/kernel/debug/tracing/uprobe_events 2> /dev/null | + cut -d ' ' -f 1` + for PROBE_SPEC in $PROBE_SPECS + do + case "$PROBE_SPEC" in + *:*/*) ;; + *) continue ;; + esac + PROBE_ENABLE="/sys/kernel/debug/tracing/events/${PROBE_SPEC#*:}/enable" + test -e "$PROBE_ENABLE" && echo 0 > "$PROBE_ENABLE" + done +} + clear_all_probes() { - echo 0 > /sys/kernel/debug/tracing/events/enable + # Disable the probes before removing them. Writing to kprobe_events or + # uprobe_events is all or nothing: dyn_events_release_all() walks every + # probe of that type first and returns -EBUSY without removing any of + # them if it finds one that is still enabled, which would leave stale + # probes behind to collide with the next run. That covers probes this + # test suite never created, so disable all of them and not just the + # ones in the groups perf uses. + # + # Only probes are disabled. Writing to events/enable would also silence + # the tracepoints of any perf record, perf trace or ftrace session + # sharing the machine, which breaks those tests when they run in + # parallel with this one. + disable_all_probes check_kprobes_available && echo > /sys/kernel/debug/tracing/kprobe_events check_uprobes_available && echo > /sys/kernel/debug/tracing/uprobe_events } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 13/18] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (11 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 12/18] perf test common: Only disable probes in clear_all_probes Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 14/18] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers ` (6 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The probe name `vfs_getname` was hardcoded, causing collisions when tests ran concurrently. Furthermore, `cleanup_probe_vfs_getname()` used `perf probe -d probe:vfs_getname*`, deleting probes registered by other parallel tests. Scope the probe name to the pid, and rename it to `getname_flags_$$` so that it no longer begins with "vfs_getname". perf trace calls evlist__add_vfs_getname(), which opens every event matching a hardcoded "probe:vfs_getname*" wildcard, so a perf trace run by any other test would otherwise pin this probe and make `perf probe -d` fail with -EBUSY. That also unblocks making the perf trace tests non-exclusive later in this series. Enumerate the probes to record and to delete from `perf probe -l`, matching `^probe:${vfs_getname}(_[[:digit:]]+)?$` exactly, rather than globbing on `${vfs_getname}*`. perf probe appends _1, _2, ... when getname_flags is inlined at more than one call site, so the variants do have to be matched, but since the name now ends in a pid a trailing wildcard would also match the probes of a test whose pid merely starts with this one's, e.g. 123 and 1234. Remove the `(exclusive)` tag from probe_vfs_getname.sh and record+script_probe_vfs_getname.sh so they run concurrently in pass 1. trace+probe_vfs_getname.sh has to stay exclusive: it is the one test that wants to be discovered by that wildcard, so it sets vfs_getname to a "vfs_getname_$$" name before sourcing the library, and would then pin its siblings' probes if it ran alongside them. A comment in the test records this. Delete the probes from an exit trap as well as on the way out. A pid scoped name is never seen again, so a run interrupted before cleanup_probe_vfs_getname() would leave its probes behind for good, accumulating a set per run. The fixed name at least meant the next run found, and went on to reuse, whatever the last one left. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/lib/probe_vfs_getname.sh | 51 +++++++++++++++++-- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+script_probe_vfs_getname.sh | 18 +++++-- .../tests/shell/trace+probe_vfs_getname.sh | 9 ++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/tools/perf/tests/shell/lib/probe_vfs_getname.sh b/tools/perf/tests/shell/lib/probe_vfs_getname.sh index 88cd0e26d5f6..4e915b6aefc8 100644 --- a/tools/perf/tests/shell/lib/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/lib/probe_vfs_getname.sh @@ -1,15 +1,58 @@ #!/bin/bash # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 -perf probe -l 2>&1 | grep -q probe:vfs_getname +# The name of the getname_flags probe added and removed below. +# +# It is scoped to the pid so that tests running in parallel do not collide, +# and it deliberately does not start with "vfs_getname": perf trace calls +# evlist__add_vfs_getname(), which opens everything matching the hardcoded +# "probe:vfs_getname*" wildcard, so a perf trace running in another test would +# otherwise pin this probe and make the 'perf probe -d' below fail with -EBUSY. +# +# trace+probe_vfs_getname.sh is the one test that does want to be found that +# way, so it sets vfs_getname itself before sourcing this file, and is +# (exclusive) as a result. +: "${vfs_getname:=getname_flags_$$}" + +# Print the probes add_probe_vfs_getname() created. perf probe appends _1, _2, +# ... when getname_flags is inlined at more than one call site, so there can be +# several. Match them exactly rather than with a "${vfs_getname}*" glob: the +# name ends in a pid, so such a glob would also match the probes of a test +# whose pid merely starts with this one's, e.g. 123 and 1234. +probes_vfs_getname() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe:${vfs_getname}(_[[:digit:]]+)?$" +} + +[ -n "$(probes_vfs_getname)" ] had_vfs_getname=$? cleanup_probe_vfs_getname() { if [ $had_vfs_getname -eq 1 ] ; then - perf probe -q -d probe:vfs_getname* + local probe + for probe in $(probes_vfs_getname); do + perf probe -q -d "$probe" + done fi } +# Delete the probes however the test ends, not just when it runs to +# completion. The name is scoped to the pid, so nothing that runs later +# reuses or tidies up a probe an interrupted test left behind, and they +# would accumulate one set per run. The fixed name used before was at least +# picked up again by the next run. +# +# Tests may still call cleanup_probe_vfs_getname directly. Doing so leaves +# nothing for probes_vfs_getname to find, so the trap below then does +# nothing. A test needing cleanup of its own should call +# cleanup_probe_vfs_getname from its own exit trap, since installing one +# replaces this rather than adding to it. +trap cleanup_probe_vfs_getname exit +# Turn a signal into an ordinary exit so that the exit trap above runs. An +# exit trap that returns leaves the exit status alone, so a test exiting 2 +# to skip still skips. +trap 'exit 1' term int + add_probe_vfs_getname() { add_probe_verbose=$1 if [ $had_vfs_getname -eq 1 ] ; then @@ -33,8 +76,8 @@ add_probe_vfs_getname() { return 2 fi - perf probe -q "vfs_getname=getname_flags:${line} pathname=result->name:string" || \ - perf probe $add_probe_verbose "vfs_getname=getname_flags:${line} pathname=filename:ustring" || return 1 + perf probe -q "${vfs_getname}=getname_flags:${line} pathname=result->name:string" || \ + perf probe $add_probe_verbose "${vfs_getname}=getname_flags:${line} pathname=filename:ustring" || return 1 fi } diff --git a/tools/perf/tests/shell/probe_vfs_getname.sh b/tools/perf/tests/shell/probe_vfs_getname.sh index 5fe5682c28ce..05f1d50732b6 100755 --- a/tools/perf/tests/shell/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/probe_vfs_getname.sh @@ -1,6 +1,5 @@ #!/bin/bash -# Add vfs_getname probe to get syscall args filenames (exclusive) - +# Add vfs_getname probe to get syscall args filenames # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 diff --git a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh index 002f7037f182..1d4fb4a4fbfe 100755 --- a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh +++ b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Use vfs_getname probe to get syscall args filenames (exclusive) +# Use vfs_getname probe to get syscall args filenames # Uses the 'perf test shell' library to add probe:vfs_getname to the system # then use it with 'perf record' using 'touch' to write to a temp file, then @@ -17,22 +17,32 @@ skip_if_no_perf_probe || exit 2 # shellcheck source=lib/probe_vfs_getname.sh . "$(dirname "$0")/lib/probe_vfs_getname.sh" +# shellcheck disable=SC2154 # vfs_getname is assigned in lib/probe_vfs_getname.sh record_open_file() { echo "Recording open file:" # Check presence of libtraceevent support to run perf record - skip_no_probe_record_support "probe:vfs_getname*" + skip_no_probe_record_support if [ $? -eq 2 ]; then echo "WARN: Skipping test record_open_file. No libtraceevent support" return 2 fi - perf record -o ${perfdata} -e probe:vfs_getname\* touch $file + # Record every probe the inlining of getname_flags produced, naming + # them exactly rather than with a "${vfs_getname}*" glob, which would + # also match the probes of a test whose pid starts with this one's. + local events + events=$(probes_vfs_getname | paste -sd, -) + if [ -z "${events}" ] ; then + echo "FAIL: no ${vfs_getname} probe to record" + return 1 + fi + perf record -o ${perfdata} -e "${events}" touch $file } perf_script_filenames() { echo "Looking at perf.data file for vfs_getname records for the file we touched:" perf script -i ${perfdata} | \ - grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:vfs_getname[_0-9]*: +\([[:xdigit:]]+\) +pathname=\"${file}\"" + grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:${vfs_getname}(_[0-9]+)?: +\([[:xdigit:]]+\) +pathname=\"${file}\"" } add_probe_vfs_getname diff --git a/tools/perf/tests/shell/trace+probe_vfs_getname.sh b/tools/perf/tests/shell/trace+probe_vfs_getname.sh index 7a0b1145d0cd..146305f4d549 100755 --- a/tools/perf/tests/shell/trace+probe_vfs_getname.sh +++ b/tools/perf/tests/shell/trace+probe_vfs_getname.sh @@ -10,6 +10,13 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 +# This test must stay exclusive, and is the only one of the probe tests that +# does: it does not name the event it uses. perf trace discovers it with the +# hardcoded "probe:vfs_getname*" wildcard in evlist__add_vfs_getname(), so the +# probe has to carry that prefix, and a parallel run of this test would then +# also match, and pin, the probes of the other tests. The sibling tests avoid +# all of this by using a name that the wildcard cannot reach. + # shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh @@ -17,6 +24,8 @@ skip_if_no_perf_probe || exit 2 skip_if_no_perf_trace || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# shellcheck disable=SC2034 # consumed by lib/probe_vfs_getname.sh +vfs_getname="vfs_getname_$$" . "$(dirname $0)"/lib/probe_vfs_getname.sh trace_open_vfs_getname() { -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 14/18] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (12 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 13/18] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 15/18] perf test trace_summary: Improve error diagnostics Ian Rogers ` (5 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The uprobe name was not scoped to PID, and concurrent writes to `/sys/kernel/debug/tracing/uprobe_events` can occasionally return `-EBUSY` when another process holds the tracefs inode lock. Scope the probe event name with `$$` (`inet_pton_$$=inet_pton`) and add a retry loop with backoff for uprobe addition. Drop the `(exclusive)` tag so the test can run in parallel during pass 1. A PID scoped probe is no longer cleaned up by any other test, so add an EXIT/TERM/INT trap to delete it, otherwise an interrupted run leaks the uprobe into the system. The trap is installed only after the root and IPv6 checks that `exit 2` to skip the test, as trap_cleanup() exits 1 and would otherwise turn those skips into failures. Deletion enumerates the probes from `perf probe -l`, matching `^probe_libc:inet_pton_$$(_[[:digit:]]+)?$` exactly, rather than reading $event_name: a signal arriving after perf probe injected the uprobe but before the assignment completed would leave that variable empty and leak the probe, and an `inet_pton_$$*` glob would reach the probe of a test whose pid merely starts with this one's. While here use mktemp rather than mktemp -u for the temporary files: this test runs as root in a world writable /tmp, and predicting a name without creating it allows another user to win the race and plant a symlink. The perf.data check becomes -s rather than -e as mktemp now pre-creates an empty file. Pre-create the temporary files with mktemp rather than reserving names with mktemp -u, and bail out if mktemp fails. The emptiness check on the recorded data quotes its path for the same reason: unquoted, an empty value would leave [ ! -s ] testing the string "-s", which is true, so the negation would skip the failure path and the test would go on to pass without having recorded anything. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../shell/record+probe_libc_inet_pton.sh | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh index eca629ee83f0..00367f26bfae 100755 --- a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh +++ b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh @@ -1,5 +1,5 @@ #!/bin/bash -# probe libc's inet_pton & backtrace it with ping (exclusive) +# probe libc's inet_pton & backtrace it with ping # Installs a probe on libc's inet_pton function, that will use uprobes, # then use 'perf trace' on a ping to localhost asking for just one packet @@ -21,20 +21,30 @@ nm -Dg $libc 2>/dev/null | grep -F -q inet_pton || exit 254 event_pattern='probe_libc:inet_pton(_[[:digit:]]+)?' add_libc_inet_pton_event() { + local attempts=0 + while [ $attempts -lt 3 ]; do + event_name=$(perf probe -f -x $libc -a "inet_pton_$$=inet_pton" 2>&1 | \ + awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ + ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) + + if [ -n "$event_name" ]; then + return 0 + fi + attempts=$((attempts + 1)) + sleep 0.1 + done - event_name=$(perf probe -f -x $libc -a inet_pton 2>&1 | \ - awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ - ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) - - if [ $? -ne 0 ] || [ -z "$event_name" ] ; then - printf "FAIL: could not add event\n" - return 1 - fi + printf "FAIL: could not add event\n" + return 1 } trace_libc_inet_pton_backtrace() { - expected=`mktemp -u /tmp/expected.XXX` + # Create the files rather than just reserving names with mktemp -u: + # this runs as root and /tmp is world writable, so a predictable name + # that is written to later can be pre-created as a symlink by an + # unprivileged user and used to clobber an arbitrary file. + expected=$(mktemp /tmp/expected.XXX) || return 1 echo "ping[][0-9 \.:]+$event_name: \([[:xdigit:]]+\)" > $expected echo ".*inet_pton\+0x[[:xdigit:]]+[[:space:]]\($libc|inlined\)$" >> $expected @@ -50,8 +60,8 @@ trace_libc_inet_pton_backtrace() { ;; esac - perf_data=`mktemp -u /tmp/perf.data.XXX` - perf_script=`mktemp -u /tmp/perf.script.XXX` + perf_data=$(mktemp /tmp/perf.data.XXX) || return 1 + perf_script=$(mktemp /tmp/perf.script.XXX) || return 1 # Check presence of libtraceevent support to run perf record skip_no_probe_record_support "$event_name/$eventattr/" @@ -61,9 +71,12 @@ trace_libc_inet_pton_backtrace() { fi perf record -e $event_name/$eventattr/ -o $perf_data ping -6 -c 1 ::1 > /dev/null 2>&1 - # check if perf data file got created in above step. - if [ ! -e $perf_data ]; then - printf "FAIL: perf record failed to create \"%s\" \n" "$perf_data" + # Check perf record actually wrote data. mktemp already created the + # file, so test that it is non-empty rather than that it exists. Quote + # the path: were it ever empty, [ ! -s ] would test the string "-s" + # instead and report success. + if [ ! -s "$perf_data" ]; then + printf "FAIL: perf record failed to write \"%s\" \n" "$perf_data" return 1 fi perf script -i $perf_data | tac | grep -m1 ^ping -B9 | tac > $perf_script @@ -97,21 +110,75 @@ trace_libc_inet_pton_backtrace() { # even if the perf script output does not match. } +# Print the pid scoped uprobes this test may have created. perf probe appends +# _1, _2, ... when the name is already taken, so match those too, but anchor +# the match: an "inet_pton_$$*" glob would also match the probe of a test whose +# pid merely starts with this one's, e.g. 123 and 1234. +libc_inet_pton_events() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe_libc:inet_pton_$$(_[[:digit:]]+)?$" +} + delete_libc_inet_pton_event() { + # Ask the kernel what is actually there rather than trusting + # $event_name: a signal arriving after perf probe injected the uprobe + # but before the assignment to event_name completed would otherwise + # leave the variable empty and leak the probe. + # + # Retry as the addition does. Deleting writes to uprobe_events just as + # adding does, so it can lose the same race with a concurrent test and + # fail with -EBUSY. Re-list rather than assume the delete worked, and + # only give up once the probes are really gone: the name is pid + # scoped, so one left behind here is never reused or overwritten by a + # later run and would sit in the kernel until reboot. + local attempts=0 + local probe + + while [ $attempts -lt 3 ]; do + for probe in $(libc_inet_pton_events); do + perf probe -q -d "$probe" + done - if [ -n "$event_name" ] ; then - perf probe -q -d $event_name - fi + if [ -z "$(libc_inet_pton_events)" ]; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 0.1 + done + + printf "WARN: could not delete event(s): %s\n" \ + "$(libc_inet_pton_events | tr '\n' ' ')" + return 1 +} + +cleanup() { + rm -f ${perf_data} ${perf_script} ${expected} + delete_libc_inet_pton_event + + trap - EXIT TERM INT +} + +trap_cleanup() { + cleanup + exit 1 } # Check for IPv6 interface existence ip a sh lo | grep -F -q inet6 || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# Install the trap only now that the skips above are out of the way: it exits +# 1, so arming it any earlier would turn an 'exit 2' skip into a failure. +# +# The event name is pid scoped, so unlike the old fixed name an orphan left +# behind by an interrupted run is never overwritten by a later run: it would +# stay in the kernel forever. Always clean up, including on a signal. +trap trap_cleanup EXIT TERM INT + skip_if_no_perf_probe && \ add_libc_inet_pton_event && \ trace_libc_inet_pton_backtrace err=$? -rm -f ${perf_data} ${perf_script} ${expected} -delete_libc_inet_pton_event +cleanup exit $err -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 15/18] perf test trace_summary: Improve error diagnostics 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (13 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 14/18] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 16/18] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers ` (4 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When pattern matching fails in test_perf_trace(), print the command that failed along with the actual match count, the matching lines found, and the last 20 lines of output to aid debugging. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index b80dea77cec6..4589b4581419 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -28,10 +28,16 @@ test_perf_trace() { count=$(grep -E -c -m 3 "${search}" ${OUTPUT}) if [ "${count}" != "3" ]; then - echo "Error: cannot find enough pattern ${search} in the output" - cat ${OUTPUT} - rm -f ${OUTPUT} - exit 1 + echo "Error: cannot find enough pattern ${search} (count=${count}) in output of:" + echo "Error: perf trace ${args} -- ${workload}" + echo "Error: matched lines:" + grep -E "${search}" ${OUTPUT} || echo "none" + echo "Error: last 20 lines of output:" + # The summary is printed after any trace output, so show the end of + # the file: with -S the head of it is the trace, not what is matched. + tail -n 20 ${OUTPUT} + rm -f ${OUTPUT} + exit 1 fi } -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 16/18] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (14 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 15/18] perf test trace_summary: Improve error diagnostics Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 17/18] perf test trace_summary: Make non-exclusive Ian Rogers ` (3 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_btf_general.sh used `--max-events=1` with `perf trace` on commands such as `mv`, `echo`, and `sleep`. When background activity occurs or tests run in parallel, `perf trace` can capture an event from an unrelated process and exit prematurely before recording the target command's syscalls. Drop `--max-events=1` and let tracing run until the command completes, checking for the expected output with grep (matching trace_btf_enum.sh). Remove the (exclusive) tag so the test runs in parallel. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_btf_general.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_btf_general.sh b/tools/perf/tests/shell/trace_btf_general.sh index 7a94a5743924..4d654b687a4e 100755 --- a/tools/perf/tests/shell/trace_btf_general.sh +++ b/tools/perf/tests/shell/trace_btf_general.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace BTF general tests (exclusive) +# perf trace BTF general tests # SPDX-License-Identifier: GPL-2.0 err=0 @@ -27,7 +27,7 @@ check_vmlinux() { trace_test_string() { echo "Testing perf trace's string augmentation" - output="$(perf trace --sort-events -e renameat* --max-events=1 -- mv ${file1} ${file2} 2>&1)" + output="$(perf trace --sort-events -e renameat* -- mv ${file1} ${file2} 2>&1)" if ! echo "$output" | grep -q -E "^mv/[0-9]+ renameat(2)?\(.*, \"${file1}\", .*, \"${file2}\", .*\) += +[0-9]+$" then printf "String augmentation test failed, output:\n$output\n" @@ -38,7 +38,7 @@ trace_test_string() { trace_test_buffer() { echo "Testing perf trace's buffer augmentation" # echo will insert a newline (\10) at the end of the buffer - output="$(perf trace --sort-events -e write --max-events=1 -- echo "${buffer}" 2>&1)" + output="$(perf trace --sort-events -e write -- echo "${buffer}" 2>&1)" if ! echo "$output" | grep -q -E "^echo/[0-9]+ write\([0-9]+, ${buffer}.*, [0-9]+\) += +[0-9]+$" then printf "Buffer augmentation test failed, output:\n$output\n" @@ -48,7 +48,7 @@ trace_test_buffer() { trace_test_struct_btf() { echo "Testing perf trace's struct augmentation" - output="$(perf trace --sort-events -e clock_nanosleep --force-btf --max-events=1 -- sleep 1 2>&1)" + output="$(perf trace --sort-events -e clock_nanosleep --force-btf -- sleep 1 2>&1)" if ! echo "$output" | grep -q -E "^sleep/[0-9]+ clock_nanosleep\(0, 0, \{1,.*\}, 0x[0-9a-f]+\) += +[0-9]+$" then printf "BTF struct augmentation test failed, output:\n$output\n" -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 17/18] perf test trace_summary: Make non-exclusive 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (15 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 16/18] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-18 21:19 ` [PATCH v4 18/18] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers ` (2 subsequent siblings) 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_summary.sh tests various summary modes of `perf trace`. It already directs output to a unique temporary file without polluting the current working directory. Remove the (exclusive) tag so it can run concurrently in parallel test runs. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index 4589b4581419..d0196351243d 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace summary (exclusive) +# perf trace summary # SPDX-License-Identifier: GPL-2.0 # Check that perf trace works with various summary mode -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v4 18/18] perf test uprobe_from_different_cu: Scope probe name to PID 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (16 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 17/18] perf test trace_summary: Make non-exclusive Ian Rogers @ 2026-09-18 21:19 ` Ian Rogers 2026-09-22 13:11 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Arnaldo Carvalho de Melo 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers 19 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-18 21:19 UTC (permalink / raw) To: irogers, acme, namhyung, Howard Chu Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The test builds a binary in a per-run temporary directory and probes its foo function. The directory name is unique, but perf probe derives the event name from the probed function and the group name from the binary's basename, so every run registers the same probe_testfile:foo event. Running the test concurrently with itself, as 'perf test -r3' does, therefore fails in all but one of the runs with: Error: event "foo" already exists. Hint: Remove existing event by 'perf probe -d' and a losing run's cleanup goes on to delete the winning run's probe out from under it. Name the event after the pid, foo_$$, so that parallel runs no longer collide. This lets the test stay in the parallel pass rather than having to be marked (exclusive). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/test_uprobe_from_different_cu.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh index 7adf9755d6de..47c99d93436b 100755 --- a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh +++ b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh @@ -18,12 +18,19 @@ fi temp_dir=$(mktemp -d /tmp/perf-uprobe-different-cu-sh.XXXXXXXXXX) +# The name of the uprobe added and removed below. The probe is placed on +# ${temp_dir}/testfile, but perf probe derives the event name from the probed +# function and the group name from the binary's basename, so every run would +# otherwise share one probe_testfile:foo event, and a concurrent run would +# fail with 'event "foo" already exists'. Scope the event name to the pid. +probe_name="foo_$$" + cleanup() { trap - EXIT TERM INT if [[ "${temp_dir}" =~ ^/tmp/perf-uprobe-different-cu-sh.*$ ]]; then echo "--- Cleaning up ---" - perf probe -x ${temp_dir}/testfile -d foo || true + perf probe -x ${temp_dir}/testfile -d ${probe_name} || true rm -f "${temp_dir}/"* rmdir "${temp_dir}" fi @@ -84,6 +91,6 @@ gcc -g -Og -c ${temp_dir}/testfile-main.c -o ${temp_dir}/testfile-main.o gcc -g -Og -o ${temp_dir}/testfile ${temp_dir}/testfile-foo.o ${temp_dir}/testfile-main.o perf probe -x ${temp_dir}/testfile --funcs foo | grep "foo" -perf probe -x ${temp_dir}/testfile foo +perf probe -x ${temp_dir}/testfile ${probe_name}=foo cleanup -- 2.55.0.1082.g2b9226bbc0-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* Re: [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (17 preceding siblings ...) 2026-09-18 21:19 ` [PATCH v4 18/18] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers @ 2026-09-22 13:11 ` Arnaldo Carvalho de Melo 2026-09-22 22:43 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers 19 siblings, 1 reply; 91+ messages in thread From: Arnaldo Carvalho de Melo @ 2026-09-22 13:11 UTC (permalink / raw) To: Ian Rogers Cc: namhyung, Howard Chu, adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz On Fri, Sep 18, 2026 at 02:19:14PM -0700, Ian Rogers wrote: > perf trace's BPF augmentation attaches to raw_syscalls:sys_enter and > raw_syscalls:sys_exit system wide, and used the program return value to > decide whether a syscall was interesting. Returning 0 from a > BPF_PROG_TYPE_TRACEPOINT program makes perf_trace_run_bpf_submit() drop > the event for every listener on that tracepoint, not just for the perf > trace that installed the program. Any concurrent perf trace, perf record > or ftrace session watching raw_syscalls therefore lost events, which is > one of the reasons so many of the perf trace and perf probe shell tests > had to be marked (exclusive) and run on their own. > > Patches 1 to 3 are independent fixes to the code the rest of the series > goes on to rework or to rely on. > > Patches 4 to 10 fix perf trace. They stop the return value being used as > a filter and do the filtering in BPF maps instead, fix argument handling > for the __data_loc internal tracepoint fields that syscalls:sys_enter_* > gained in 6.19, stop the sys_exit program array tail calling a sys_enter > augmenter, and replace the userspace PERF_RECORD_FORK/PERF_RECORD_EXIT > bookkeeping with BTF-typed raw tracepoint programs on > sched_process_{fork,exit,exec}. A task is then registered before its > first syscall and evicted in do_exit(), rather than whenever userspace > next drains the ring buffer. > > Patches 11 to 18 deal with the tests. Several collided with each other > through global state rather than through perf trace: fixed probe names, > clear_all_probes() disabling every tracepoint on the system, and perf > trace's hardcoded "probe:vfs_getname*" wildcard pinning probes belonging > to other tests. With those scoped to a pid they can drop (exclusive) and > run in parallel again. > > Tested on x86_64. The trace and probe tests pass under 'perf test -r3', > which runs the repeats concurrently. Every patch builds individually, > and the series also builds with BUILD_BPF_SKEL=0. Lots of changes, from a quick look all look sensible, I'll test it some more, as in one case it segfaulted: root@x2:~# strace -e perf_event_open perf trace -a sleep 1 <SNIP> 262.297 ( 0.002 ms): ptyxis/30654 write(fd: 4<anon_inode:[eventfd]>, buf: \1\0\0\0\0\0\0\0, count: 8) = 8 262.304 ( 0.011 ms): ptyxis/30654 ppoll(ufds: 0x55dfa7a62f10, nfds: 11, tsp: 0x7ffc0701eed0, sigsetsize: 8) = 2 262.318 ( 0.002 ms): ptyxis/30654 read(fd: 4<anon_inode:[eventfd]>, buf: 0x7ffc0701ee10, count: 8) = 8 262.324 ( 0.002 ms): ptyxis/30654 write(fd: 4<anon_inode:[eventfd]>, buf: \1\0\0\0\0\0\0\0, count: 8) = 8 262.328 ( 0.002 ms): ptyxis/30654 read(fd: 31</dev/ptmx>, buf: 0x55dfa8a54f19, count: 5751) = 8 262.332 ( 0.002 ms): ptyxis/30654 read(fd: 31</dev/ptmx>, buf: 0x55dfa8a54f20, count: 5744) = -1 (unknown) (Resource temporarily unavailable) LOST 70 events! --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x117f000} --- perf: Segmentation fault #0 0x73104f in dump_stack debug.c:366 #1 0x7310c5 in sighandler_dump_stack debug.c:378 #2 0x7f749ea66fb0 in __restore_rt libc.so.6[19fb0] #3 0x4c0fa0 in syscall_arg__scnprintf_buf builtin-trace.c:1955 #4 0x4c2f3d in syscall_arg_fmt__scnprintf_val builtin-trace.c:2632 #5 0x4c33ae in syscall__scnprintf_args builtin-trace.c:2722 #6 0x4c43d3 in trace__sys_enter builtin-trace.c:3094 #7 0x4c7865 in trace__handle_event builtin-trace.c:4013 #8 0x4c9ab4 in __trace__deliver_event builtin-trace.c:4962 #9 0x4c9c06 in trace__deliver_event builtin-trace.c:4988 #10 0x4caed8 in trace__run builtin-trace.c:5397 #11 0x4cf530 in cmd_trace builtin-trace.c:6681 #12 0x4da43c in run_builtin perf.c:380 #13 0x4da68e in handle_internal_command perf.c:430 #14 0x4da7ea in run_argv perf.c:477 #15 0x4daabe in main perf.c:577 #16 0x7f749ea50681 in __libc_start_call_main libc.so.6[3681] #17 0x7f749ea50798 in __libc_start_main@@GLIBC_2.34 libc.so.6[3798] #18 0x409f25 in _start perf[409f25] --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_TKILL, si_pid=1097958, si_uid=0} --- +++ killed by SIGSEGV (core dumped) +++ Segmentation fault (core dumped) strace -e perf_event_open perf trace -a sleep 1 root@x2:~# root@x2:~# uname -a Linux x2 7.2.5-200.fc44.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Sep 11 15:11:05 UTC 2026 x86_64 GNU/Linux root@x2:~# - Arnaldo ^ permalink raw reply [flat|nested] 91+ messages in thread
* Re: [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive 2026-09-22 13:11 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Arnaldo Carvalho de Melo @ 2026-09-22 22:43 ` Ian Rogers 0 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-22 22:43 UTC (permalink / raw) To: Arnaldo Carvalho de Melo Cc: namhyung, Howard Chu, adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz On Tue, Sep 22, 2026 at 6:11 AM Arnaldo Carvalho de Melo <acme@kernel.org> wrote: > Lots of changes, from a quick look all look sensible, I'll test it some > more, as in one case it segfaulted: > > root@x2:~# strace -e perf_event_open perf trace -a sleep 1 > <SNIP> > 262.297 ( 0.002 ms): ptyxis/30654 write(fd: 4<anon_inode:[eventfd]>, buf: \1\0\0\0\0\0\0\0, count: 8) = 8 > 262.304 ( 0.011 ms): ptyxis/30654 ppoll(ufds: 0x55dfa7a62f10, nfds: 11, tsp: 0x7ffc0701eed0, sigsetsize: 8) = 2 > 262.318 ( 0.002 ms): ptyxis/30654 read(fd: 4<anon_inode:[eventfd]>, buf: 0x7ffc0701ee10, count: 8) = 8 > 262.324 ( 0.002 ms): ptyxis/30654 write(fd: 4<anon_inode:[eventfd]>, buf: \1\0\0\0\0\0\0\0, count: 8) = 8 > 262.328 ( 0.002 ms): ptyxis/30654 read(fd: 31</dev/ptmx>, buf: 0x55dfa8a54f19, count: 5751) = 8 > 262.332 ( 0.002 ms): ptyxis/30654 read(fd: 31</dev/ptmx>, buf: 0x55dfa8a54f20, count: 5744) = -1 (unknown) (Resource temporarily unavailable) > LOST 70 events! > --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x117f000} --- > perf: Segmentation fault > #0 0x73104f in dump_stack debug.c:366 > #1 0x7310c5 in sighandler_dump_stack debug.c:378 > #2 0x7f749ea66fb0 in __restore_rt libc.so.6[19fb0] > #3 0x4c0fa0 in syscall_arg__scnprintf_buf builtin-trace.c:1955 > #4 0x4c2f3d in syscall_arg_fmt__scnprintf_val builtin-trace.c:2632 > #5 0x4c33ae in syscall__scnprintf_args builtin-trace.c:2722 > #6 0x4c43d3 in trace__sys_enter builtin-trace.c:3094 > #7 0x4c7865 in trace__handle_event builtin-trace.c:4013 > #8 0x4c9ab4 in __trace__deliver_event builtin-trace.c:4962 > #9 0x4c9c06 in trace__deliver_event builtin-trace.c:4988 > #10 0x4caed8 in trace__run builtin-trace.c:5397 > #11 0x4cf530 in cmd_trace builtin-trace.c:6681 > #12 0x4da43c in run_builtin perf.c:380 > #13 0x4da68e in handle_internal_command perf.c:430 > #14 0x4da7ea in run_argv perf.c:477 > #15 0x4daabe in main perf.c:577 > #16 0x7f749ea50681 in __libc_start_call_main libc.so.6[3681] > #17 0x7f749ea50798 in __libc_start_main@@GLIBC_2.34 libc.so.6[3798] > #18 0x409f25 in _start perf[409f25] > --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_TKILL, si_pid=1097958, si_uid=0} --- > +++ killed by SIGSEGV (core dumped) +++ > Segmentation fault (core dumped) strace -e perf_event_open perf trace -a sleep 1 > root@x2:~# > > root@x2:~# uname -a > Linux x2 7.2.5-200.fc44.x86_64 #1 SMP PREEMPT_DYNAMIC Fri Sep 11 15:11:05 UTC 2026 x86_64 GNU/Linux > root@x2:~# Thanks Arnaldo for testing, and sorry for the crash. Working with antigravity I got the analysis below for the crash and I'll fix it in v5. I can account for it, and it is a regression from patch 7 ("perf trace: Filter events in BPF and avoid tracepoint vetoes") landing on a missing bounds check that predates the series. Before the series syscall_unaugmented() was: SEC("tp/raw_syscalls/sys_enter") int syscall_unaugmented(struct syscall_enter_args *args) { return 1; } so an unaugmented sys_enter emitted nothing at all. Patch 7 moved it to tp/syscalls/sys_enter_unaugmented and made it output the 64 byte struct syscall_enter_args to __augmented_syscalls__, so that perf trace could stop listening on the raw tracepoint and stop vetoing it. That 64 byte record does not come back as 64 bytes. perf_sample_save_raw_data() does: size = round_up(sum + sizeof(u32), sizeof(u64)); raw->size = size - sizeof(u32); frag->pad = raw->size - sum; so sum of 64 gives raw->size of 68, and the 4 bytes of padding are written by: __output_skip(handle, NULL, frag->pad); which advances over them rather than zeroing them. They are whatever the ring buffer last held there. Userspace then does: *augmented_args_size = sample->raw_size - args_size; with args_size of 64, gets 4, decides that is an augmented payload, and copies those 4 stale bytes into argbuf as the size and int_arg of a struct augmented_arg. syscall_arg__scnprintf_buf() has no bound on the length it reads there: for (int j = 0; j < augmented_arg->size; ++j) so a size taken from, say, the low half of a pointer walks straight off the end of the 8K argbuf in bss. SCA_BUF is used by exactly one syscall, write()'s buf argument, which is why a terminal emulator's writes were what tripped it. The si_addr in your report is page aligned just past that buffer, which fits. btf_struct_scnprintf() already had the check that would have caught it, which is why the BTF path survived this and SCA_BUF did not: a 4 byte trailing run is smaller than a struct augmented_arg, so it returns early. v5 fixes it in two patches, placed before the patch that introduces the unaugmented output so that no commit in the series is broken: perf trace: Bounds check augmented arguments before reading them Lifts btf_struct_scnprintf()'s check into a shared syscall_arg__augmented_args_valid() in beauty.h and uses it at every site that reads an augmented argument. Besides the crash site this covers the timespec, sockaddr and perf_event_attr beautifiers, which read a fixed sized type out of a payload that may be shorter than it. Two unbounded reads that are not about the header are fixed with them: perf_event_attr__fprintf() always reads a whole struct perf_event_attr, so the payload is now copied into a zero padded local rather than read in place, and the AF_LOCAL path is printed with a length from the payload rather than as a string it need not be. perf trace: Do not read sample padding as an augmented argument A trailing run shorter than a struct augmented_arg cannot be one, so it is recognised as the padding it is rather than being offered to the beautifiers at all. The first alone stops the SIGSEGV. The second stops the syscall being printed with a bogus argument in the cases where the padding happens to hold a length that passes validation. Thanks, Ian ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 00/23] perf trace: Fix BPF filtering and make tracing tests non-exclusive 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers ` (18 preceding siblings ...) 2026-09-22 13:11 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Arnaldo Carvalho de Melo @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 01/23] perf trace: Set the augmented arg header in the augmenters that omit it Ian Rogers ` (22 more replies) 19 siblings, 23 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz perf trace's BPF augmentation attaches to raw_syscalls:sys_enter and raw_syscalls:sys_exit system wide, and used the program return value to decide whether a syscall was interesting. Returning 0 from a BPF_PROG_TYPE_TRACEPOINT program makes perf_trace_run_bpf_submit() drop the event for every listener on that tracepoint, not just for the perf trace that installed the program. Any concurrent perf trace, perf record or ftrace session watching raw_syscalls therefore lost events, which is one of the reasons so many of the perf trace and perf probe shell tests had to be marked (exclusive) and run on their own. Patches 1 to 6 are independent fixes to the code the rest of the series goes on to rework or to rely on. Patches 7 to 10 fix how an augmented argument is read back. They skip the __data_loc internal tracepoint fields that syscalls:sys_enter_* gained in 6.19 for syscalls only, bounds check every read of an augmented payload, and stop the ring buffer padding at the end of a sample being offered to the beautifiers as one. Patch 8 fixes the SIGSEGV Arnaldo hit testing v4. Patches 11 to 15 fix perf trace's filtering. They stop the return value being used as a filter and do the filtering in BPF maps instead, stop the sys_exit program array tail calling a sys_enter augmenter, and replace the userspace PERF_RECORD_FORK/PERF_RECORD_EXIT bookkeeping with BTF-typed raw tracepoint programs on sched_process_{fork,exit,exec}. A task is then registered before its first syscall and evicted in do_exit(), rather than whenever userspace next drains the ring buffer. Patches 16 to 23 deal with the tests. Several collided with each other through global state rather than through perf trace: fixed probe names, clear_all_probes() disabling every tracepoint on the system, and perf trace's hardcoded "probe:vfs_getname*" wildcard pinning probes belonging to other tests. With those scoped to a pid they can drop (exclusive) and run in parallel again. Patches 1 and 2 fix pre-existing bugs that stand on their own and can be applied ahead of the rest. Only patch 9 depends on them. Tested on x86_64. The trace and probe tests pass under 'perf test -r3', which runs the repeats concurrently. Every patch builds individually, and the series also builds with BUILD_BPF_SKEL=0. Changes since v4: - New patch 1 fills in the size in the augmented argument header in the four augmenters that left it unset: sendto, perf_event_open, nanosleep and clock_nanosleep. They all build a payload for a fixed size type, and the beautifiers cast it directly rather than reading the length, so nothing has needed it so far. Patch 9 starts checking it. While there, report a failed bpf_probe_read_user() rather than claiming success for it: the payload length is set to zero and the error recorded in the header, so userspace falls back to printing the raw pointer instead of whatever the per-CPU scratch buffer last held. - New patch 2 includes the augmented argument header in nanosleep's payload length. It used sizeof(augmented_args->args) where its three siblings use sizeof(u64) + sizeof(augmented_args->args), so the record was eight bytes short of the struct timespec it carries and tv_nsec was printed half from the ring buffer. - Patch 7 only skips the internal tracepoint fields when it is setting up a syscall. syscall_arg_fmt__init_array() is also called for an ordinary tracepoint by evsel__init_tp_arg_scnprintf(), and trace__fprintf_tp_fields() walks that array in lockstep with the field list, so packing the entries shifted every formatter along by one from the field it belongs to. 'perf trace -e sched:sched_process_exec' printed its filename with the pid beautifier. The syscall path indexes the array packed and is unaffected, so the two callers now ask for what each of them needs. - New patch 8 bounds checks an augmented argument before reading it. This is the SIGSEGV Arnaldo reported on v4. The check btf_struct_scnprintf() already had is lifted into a shared syscall_arg__augmented_args_valid() in beauty.h and used at every site that reads an augmented payload, in particular syscall_arg__scnprintf_buf(), which looped to a length taken from the payload with nothing bounding it and walked off the end of the 8K argbuf in bss. - New patch 9 bounds the beautifiers that read a fixed size type out of a payload that may be shorter than it. perf_event_attr__fprintf() always reads a whole struct perf_event_attr, so the payload is copied into a zero padded local rather than read in place, and the AF_LOCAL socket address is printed with a length from the payload rather than as a string it need not be. Split out from patch 8 so the crash fix stays minimal. - New patch 10 recognises the padding at the end of a sample for what it is. perf_sample_save_raw_data() rounds the raw size up to eight bytes and __output_skip() advances over the padding rather than zeroing it, so a 64 byte struct syscall_enter_args comes back as 68 bytes and userspace took the four stale bytes for an augmented argument. A trailing run shorter than a struct augmented_arg cannot be one. - Patch 13 notes that a descendant added to pids_filtered by sched_process_fork() can still have a sys_exit printed without a matching enter. The raw_syscalls:sys_exit evsel is still in the evlist and its tracepoint filter is set once, so it never learns of the descendants, and a tracepoint program can only suppress an event by vetoing it, which is what this series removes. It needs --filter-pids naming a process that then forks. Routing sys_exit through __augmented_syscalls__ the way sys_enter now is would close it. - Patch 14 notes that a thread the target creates during the attach window is still missed by 'perf trace -t'. The re-enumeration reads /proc/<tid>/task/<tid>/children, which lists forked processes, and copy_process() makes a CLONE_THREAD child a sibling of its creator rather than a child of it. /proc does not record which task in a group created which, so such a sibling cannot be told apart from one -t was deliberately pointed away from. Changes since v3: - New patch 3 returns -ENOMEM rather than -1 from evsel__set_filter(), evsel__append_filter(), evlist__set_tp_filter() and evlist__append_tp_filter(). An allocation is the only thing that can fail in any of them, and patch 7 goes on to print what they return, where -1 negated is EPERM and a failure to allocate would have been reported as "Operation not permitted". - Patch 5 sets sc->args only once the arg_fmt array that is indexed alongside it has been allocated. Publishing the field first left a syscall with args set and arg_fmt NULL, and since sc->name is already set by then a later syscall__read_info() returns success without retrying the allocation, leaving syscall_arg_fmt__mask_val() to dereference NULL. - Patch 7 adds the raw_syscalls tracepoints in trace__run() only if they are not in the evlist already, and stops ignoring the result of trace__add_syscall_newtp(). cmd_trace() adds them before it creates the bpf-output event, and on the paths where that creation fails they were added a second time, giving the session two enter and two exit evsels for the same pair of tracepoints. - Patch 7 no longer abandons the session when a target does not fit in the pid map. bpf_map__update_elem() answers -E2BIG once max_entries keys are present, so a target with more threads than the map has room for took perf trace down with it, on exactly the large workloads where there is least else to reach for. The tasks that fit are added and a warning says how many did not. - Patch 9 includes <sys/types.h> for the pid_t it uses rather than relying on the include chain to drag it in. - Patch 9 no longer ends the session when the target has already exited. thread_map__new_by_pid() fails with ENOENT once /proc/<pid>/task is gone, which is the ordinary outcome of 'perf trace -p' on a short lived process, so only ENOMEM is now propagated and everything else is logged and stepped over. - Patch 9 reads /proc once per thread group instead of once per thread. 'perf trace -p' names every thread of the target in the thread map and /proc/<tid>/task lists the whole group whichever thread it is asked through, so the enumeration was quadratic in the number of threads. On a 64 thread target it went from 65 directory reads and 4097 entries to 2 and 65, for the same set of pids. - New patch 10 removes from the BPF maps the tasks that died before userspace got round to writing them there. sched_process_exit() can only delete a pid that is already in the map, so a target that exits during startup leaves an entry behind for the rest of the session, and since pids are reused an unrelated task then gets traced, or silently filtered out, in its place. The map is walked with the cursor left on a task found alive, which is one the walk has decided to keep, so its own deletions can never leave the cursor on a key that htab_map_get_next_key() will answer by starting the walk over. Changes since v2: - Rebased onto the current perf-tools-next. - Patch 1 also includes <assert.h>, for the assert() in augmented_syscalls__create_bpf_output(). - New patch 2 makes evsel__put_and_free_priv() free the whole evsel_trace. It only zfree()d the struct, leaking the syscall_arg_fmt array hanging off it. No caller can reach that today, but patch 6 adds one that discards a fully set up evsel. - Patch 6 identifies the evsel to drop from the evlist by comparing against trace.syscalls.events.sys_enter instead of a strstr() for "syscalls:sys_enter". That substring also matches the per syscall syscalls:sys_enter_SYSCALL tracepoints, so a user asking for one of those by name would have had it removed from the evlist, and __augmented_syscalls__ described with its tracefs format rather than the raw tracepoint's. - Patch 6 reports a failure to program the pid filters with the error that caused it. trace__run() sent everything trace__set_filter_pids() returned to out_error_mem, which prints "Not enough memory to run!". That was already a guess, and a wrong one now the function writes BPF maps too. - Patch 7 moves the pid across in sched_process_exec() by deleting the old key before inserting the new one. The move is only a rename, but holding both keys at once needs a spare slot, and on a full map the insert failed with -E2BIG while the delete still succeeded, losing the task instead of moving it. - Patch 7 no longer claims that a task forked during the attach window is still traced unaugmented. That was wrong: cmd_trace() removes the sys_enter evsel once the bpf-output event exists, so __augmented_syscalls__ is the only source of enter events and such a task is not reported at all. Describe what is really lost, why the tgid fallback is not kept as a safety net for it, note that only 'perf trace -p' is exposed, and that closing it needs the target's descendants re-enumerated from /proc after the attach. - Patch 7 drops the parent tgid test from sched_process_fork(), so a child inherits from the pid of the thread that called clone() and from nothing else. The lookups only ever match a task's own pid, and every thread of a -p target is enumerated from /proc/<pid>/task and inserted in its own right, so the tgid added no reach. What it did add was a child inheriting from a thread that is not traced itself, such as a sibling of the thread 'perf trace -t' selected. - Patch 7 ends the session with the error that caused it when the BPF programs cannot be attached, rather than printing a bare errno left over from unwinding the attach. There is nothing to fall back to at that point, cmd_trace() has already built the evlist around __augmented_syscalls__ and dropped the sys_enter evsel, and the code being replaced did not fall back either: it ignored the result of augmented_raw_syscalls_bpf__attach() altogether. - New patch 8 reads the target out of /proc again once the BPF programs are attached, so a task the target created while perf trace was starting up is traced rather than missed for the whole session. This is the gap patch 7 describes and left for later. It covers the target's new threads and, through task->children, anything it or they forked, to any depth. What is left is a task that made syscalls between sys_enter going live and being added to the map, which is momentary rather than lasting for the session. - Patch 9 bails out if mktemp fails rather than carrying on with an empty $tmpdir. cd rejects the null directory, and the rmdir that was meant to undo the mktemp then failed to remove '' instead. Its cleanup() also no longer exits when it cannot cd out of the temporary directory, which would have skipped removing it. The removal takes an absolute path and does not need the cd to have succeeded. - Patch 10 now narrows the disable in clear_all_probes() to the probes themselves rather than dropping it. Clearing kprobe_events or uprobe_events is all or nothing: dyn_events_release_all() returns -EBUSY without removing anything if it finds a probe that is still enabled, so simply removing the write could leave stale probes behind to collide with the next run. The set to disable is read from the kprobe_events and uprobe_events listings rather than assumed to be the groups perf uses, since a probe left enabled in another group, such as the default kprobes group used when kprobe_events is written directly, would abort the clear just the same. - Patch 11 deletes the probes from an exit trap as well as on the way out. A pid scoped name is never seen again, so a run interrupted before cleanup_probe_vfs_getname() left its probes behind for good, a set per run, where the fixed name was at least found and reused by the next run. - Patch 12 retries deleting the uprobe. Deletion writes uprobe_events just as addition does and can lose the same race with a concurrent test, and because the event name is now pid scoped a probe left behind is never overwritten by a later run. It also bails out if mktemp fails and quotes the path in the emptiness check, which unquoted would have tested the string "-s" and reported success. - Patch 13 prints the tail of the output rather than the head. The pattern it matches only appears in the summary, which is printed after any trace output, so the head of the file is not the part that failed to match. Changes since v1: - New patch 1 includes <sys/types.h> and <string.h> for the pid_t and strcmp() uses that were relying on the include chain happening to drag them in, which does not hold on libcs such as musl. - Patch 6 no longer returns success when the event qualifier filter string fails to allocate. err now defaults to 0 because either tracepoint may legitimately be absent, so the ENOMEM path has to set the error itself rather than rely on that default. It also includes <stdbool.h> for the bool parameters it adds to trace_augment.h. - Patch 9 removes the temporary directory if the cd into it fails. That happens before the cleanup trap is installed, so the directory would otherwise be left behind in /tmp. Ian Rogers (23): perf trace: Set the augmented arg header in the augmenters that omit it perf trace: Include the augmented arg header in nanosleep's payload length perf trace: Include the headers declaring pid_t, strcmp and assert perf trace: Free the whole evsel_trace in evsel__put_and_free_priv perf evsel: Report an allocation failure as ENOMEM when setting filters perf trace: Start BPF summary before starting workload perf trace: Skip internal tracepoint fields in formatting and beauty map perf trace: Bounds check augmented arguments before reading them perf trace: Bound the fixed size augmented argument beautifiers perf trace: Do not read sample padding as an augmented argument perf trace: Do not set unaugmented BPF program on sys_exit map perf trace: Filter events in BPF and avoid tracepoint vetoes perf trace: Handle fork and exit directly in BPF filter maps perf trace: Enumerate the target again once BPF is attached perf trace: Drop targets that died before they were filtered perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive perf test common: Only disable probes in clear_all_probes perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive perf test trace_summary: Improve error diagnostics perf test trace_btf_general: Drop --max-events=1 and make non-exclusive perf test trace_summary: Make non-exclusive perf test uprobe_from_different_cu: Scope probe name to PID tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 863 +++++++++++++++--- tools/perf/tests/shell/common/init.sh | 33 +- .../perf/tests/shell/lib/probe_vfs_getname.sh | 51 +- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+probe_libc_inet_pton.sh | 107 ++- .../shell/record+script_probe_vfs_getname.sh | 18 +- tools/perf/tests/shell/test_task_analyzer.sh | 20 +- .../shell/test_uprobe_from_different_cu.sh | 11 +- .../tests/shell/trace+probe_vfs_getname.sh | 9 + tools/perf/tests/shell/trace_btf_general.sh | 8 +- tools/perf/tests/shell/trace_summary.sh | 16 +- tools/perf/trace/beauty/beauty.h | 43 + tools/perf/trace/beauty/perf_event_open.c | 38 +- tools/perf/trace/beauty/sockaddr.c | 55 +- tools/perf/trace/beauty/timespec.c | 2 +- .../bpf_skel/augmented_raw_syscalls.bpf.c | 407 ++++++++- tools/perf/util/bpf_trace_augment.c | 288 +++++- tools/perf/util/evlist.c | 12 +- tools/perf/util/evsel.c | 4 +- tools/perf/util/trace_augment.h | 33 +- 21 files changed, 1801 insertions(+), 225 deletions(-) base-commit: 9db4e9d6cc7c7d318fb6d6bb73abe3fed27385a8 -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 01/23] perf trace: Set the augmented arg header in the augmenters that omit it 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 02/23] perf trace: Include the augmented arg header in nanosleep's payload length Ian Rogers ` (21 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz An augmented argument is a struct augmented_arg header, holding the length of the payload and an error code, followed by the payload. The augmenters build it in augmented_args_tmp, a single entry per-CPU array reused by every syscall on that CPU, so a field left unassigned holds whatever the previous syscall on that CPU put there rather than anything about this one. The string augmenters get both fields from augmented_arg__read_str(), and sys_enter_perf_event_open() bails out when its read fails, but sys_enter_connect(), sys_enter_sendto(), sys_enter_clock_nanosleep() and sys_enter_nanosleep() copy the payload and output the record without ever describing it, and augment_arg() sets the length but not the error. This has gone unnoticed because the beautifiers for those payloads take a fixed sized type and so read the value without consulting the header: syscall_arg__scnprintf_augmented_sockaddr(), syscall_arg__scnprintf_augmented_timespec() and syscall_arg__scnprintf_augmented_perf_event_attr() all cast augmented.args->value directly. Nothing has yet read a length that was never written. Describe the payload everywhere one is produced, so that a reader can bound it by its length. These reads can fail, and none of the four checked whether they had. Rather than claim a payload that was not read, report a length of zero and the error, so the record describes what it holds and leaves the scratch out of it. A reader that bounds the payload by the header then shows the pointer, as it does for a syscall with no augmentation at all, rather than another task's data. The error is set for the same reason the length is, that the header is in a buffer the next syscall on this CPU will reuse and so carries the previous one's value if it is not written, rather than because anything reads it yet: beauty.h still calls the field int_arg and no beautifier looks at it. augment_arg() reported a string it could not read as an empty one before this and still does. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../bpf_skel/augmented_raw_syscalls.bpf.c | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 3bc9e28a9b8a..dd3aa5bd910b 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -210,6 +210,7 @@ int sys_enter_connect(struct syscall_enter_args *args) const void *sockaddr_arg = (const void *)args->args[1]; unsigned int socklen = args->args[2]; unsigned int len = sizeof(u64) + sizeof(augmented_args->args); // the size + err in all 'augmented_arg' structs + int err; if (augmented_args == NULL) return 1; /* Failure: don't filter */ @@ -217,9 +218,16 @@ int sys_enter_connect(struct syscall_enter_args *args) _Static_assert(is_power_of_2(sizeof(augmented_args->arg.saddr)), "sizeof(augmented_args->arg.saddr) needs to be a power of two"); socklen &= sizeof(augmented_args->arg.saddr) - 1; - bpf_probe_read_user(&augmented_args->arg.saddr, socklen, sockaddr_arg); + err = bpf_probe_read_user(&augmented_args->arg.saddr, socklen, sockaddr_arg); + /* + * A failed read leaves the scratch holding whatever the previous + * syscall on this CPU put there, so say there is no payload and why, + * rather than describing another task's data as this task's sockaddr. + */ + if (err < 0) + socklen = 0; augmented_args->arg.size = socklen; - augmented_args->arg.err = 0; + augmented_args->arg.err = err < 0 ? err : 0; return augmented__output(args, augmented_args, len + socklen); } @@ -231,13 +239,19 @@ int sys_enter_sendto(struct syscall_enter_args *args) const void *sockaddr_arg = (const void *)args->args[4]; unsigned int socklen = args->args[5]; unsigned int len = sizeof(u64) + sizeof(augmented_args->args); // the size + err in all 'augmented_arg' structs + int err; if (augmented_args == NULL) return 1; /* Failure: don't filter */ socklen &= sizeof(augmented_args->arg.saddr) - 1; - bpf_probe_read_user(&augmented_args->arg.saddr, socklen, sockaddr_arg); + err = bpf_probe_read_user(&augmented_args->arg.saddr, socklen, sockaddr_arg); + /* As in sys_enter_connect(), do not describe scratch as a sockaddr. */ + if (err < 0) + socklen = 0; + augmented_args->arg.size = socklen; + augmented_args->arg.err = err < 0 ? err : 0; return augmented__output(args, augmented_args, len + socklen); } @@ -372,6 +386,9 @@ int sys_enter_perf_event_open(struct syscall_enter_args *args) if (bpf_probe_read_user(&augmented_args->arg.value, size, attr) < 0) goto failure; + augmented_args->arg.size = size; + augmented_args->arg.err = 0; + return augmented__output(args, augmented_args, len + size); failure: return 1; /* Failure: don't filter */ @@ -384,6 +401,7 @@ int sys_enter_clock_nanosleep(struct syscall_enter_args *args) const void *rqtp_arg = (const void *)args->args[2]; unsigned int len = sizeof(u64) + sizeof(augmented_args->args); // the size + err in all 'augmented_arg' structs __u32 size = sizeof(struct timespec64); + int err; if (augmented_args == NULL) goto failure; @@ -391,7 +409,12 @@ int sys_enter_clock_nanosleep(struct syscall_enter_args *args) if (size > sizeof(augmented_args->arg.value)) goto failure; - bpf_probe_read_user(&augmented_args->arg.value, size, rqtp_arg); + err = bpf_probe_read_user(&augmented_args->arg.value, size, rqtp_arg); + /* As in sys_enter_connect(), do not describe scratch as a timespec. */ + if (err < 0) + size = 0; + augmented_args->arg.size = size; + augmented_args->arg.err = err < 0 ? err : 0; return augmented__output(args, augmented_args, len + size); failure: @@ -405,6 +428,7 @@ int sys_enter_nanosleep(struct syscall_enter_args *args) const void *req_arg = (const void *)args->args[0]; unsigned int len = sizeof(augmented_args->args); __u32 size = sizeof(struct timespec64); + int err; if (augmented_args == NULL) goto failure; @@ -412,7 +436,12 @@ int sys_enter_nanosleep(struct syscall_enter_args *args) if (size > sizeof(augmented_args->arg.value)) goto failure; - bpf_probe_read_user(&augmented_args->arg.value, size, req_arg); + err = bpf_probe_read_user(&augmented_args->arg.value, size, req_arg); + /* As in sys_enter_connect(), do not describe scratch as a timespec. */ + if (err < 0) + size = 0; + augmented_args->arg.size = size; + augmented_args->arg.err = err < 0 ? err : 0; return augmented__output(args, augmented_args, len + size); failure: @@ -445,6 +474,7 @@ static inline int augment_arg(struct syscall_enter_args *args, int i, struct beauty_payload_enter *payload, u64 offset) { int index, value_size = sizeof(struct augmented_arg) - offsetof(struct augmented_arg, value); + int read_err = 0; struct augmented_arg *payload_offset; s64 aug_size, size; bool augmented; @@ -467,8 +497,18 @@ static inline int augment_arg(struct syscall_enter_args *args, int i, if (size == 1) { /* string */ aug_size = bpf_probe_read_user_str(payload_offset->value, value_size, arg); /* minimum of 0 to pass the verifier */ - if (aug_size < 0) + if (aug_size < 0) { + /* + * Record why nothing was read. The header sits in + * scratch that the next syscall on this CPU reuses, + * so an error left unwritten is the previous one's. + * No beautifier reads it yet, beauty.h still calls + * the field int_arg, so the string is still shown + * as an empty one. + */ + read_err = aug_size; aug_size = 0; + } augmented = true; } else if (size > 0 && size <= value_size) { /* struct */ @@ -498,6 +538,7 @@ static inline int augment_arg(struct syscall_enter_args *args, int i, return -1; payload_offset->size = aug_size; + payload_offset->err = read_err; return written; } -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 02/23] perf trace: Include the augmented arg header in nanosleep's payload length 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers 2026-09-23 7:13 ` [PATCH v5 01/23] perf trace: Set the augmented arg header in the augmenters that omit it Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 03/23] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers ` (20 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz sys_enter_nanosleep() sizes the record it outputs as unsigned int len = sizeof(augmented_args->args); where every other augmenter that appends a payload uses unsigned int len = sizeof(u64) + sizeof(augmented_args->args); the extra u64 being the size and err fields of the struct augmented_arg that the payload follows. The payload is copied to augmented_args->arg.value, which starts 8 bytes after the tracepoint arguments, but only len + size bytes are output. So the record stops 8 bytes short of the end of the timespec, and userspace reads the tail of tv_nsec from whatever follows in the ring buffer. Count the header, as the other augmenters do. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index dd3aa5bd910b..18904bebf672 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -426,7 +426,8 @@ int sys_enter_nanosleep(struct syscall_enter_args *args) { struct augmented_args_payload *augmented_args = augmented_args_payload(); const void *req_arg = (const void *)args->args[0]; - unsigned int len = sizeof(augmented_args->args); + // the size + err in all 'augmented_arg' structs + unsigned int len = sizeof(u64) + sizeof(augmented_args->args); __u32 size = sizeof(struct timespec64); int err; -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 03/23] perf trace: Include the headers declaring pid_t, strcmp and assert 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers 2026-09-23 7:13 ` [PATCH v5 01/23] perf trace: Set the augmented arg header in the augmenters that omit it Ian Rogers 2026-09-23 7:13 ` [PATCH v5 02/23] perf trace: Include the augmented arg header in nanosleep's payload length Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 04/23] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers ` (19 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz, Arnaldo Carvalho de Melo trace_augment.h uses pid_t in the augmented_syscalls__set_filter_pids() prototype and in the !HAVE_BPF_SKEL stub. bpf_trace_augment.c calls strcmp() in augmented_syscalls__find_by_title() and assert() in augmented_syscalls__create_bpf_output(), but neither pulls in the header that declares what it uses. Both happen to build today only because something else in the include chain drags <sys/types.h>, <string.h> and <assert.h> in first, which is not guaranteed and does not hold on libcs such as musl that keep the POSIX namespaces strictly separated. Include <sys/types.h>, <string.h> and <assert.h> explicitly. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com> --- tools/perf/util/bpf_trace_augment.c | 2 ++ tools/perf/util/trace_augment.h | 1 + 2 files changed, 3 insertions(+) diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index a9cf2a77ded1..ebb26225fb04 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,7 @@ +#include <assert.h> #include <bpf/libbpf.h> #include <internal/xyarray.h> +#include <string.h> #include "bpf_skel/augmented_raw_syscalls.skel.h" #include "debug.h" diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 4f729bc67753..a1cd9a5e0213 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <sys/types.h> struct bpf_program; struct evlist; -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 04/23] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (2 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 03/23] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 05/23] perf evsel: Report an allocation failure as ENOMEM when setting filters Ian Rogers ` (18 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz, Arnaldo Carvalho de Melo Every evsel->priv in builtin-trace.c is a struct evsel_trace, allocated by evsel_trace__new(). It holds a syscall_arg_fmt array in its fmt member, which evsel__syscall_arg_fmt() allocates on demand for the syscalls:sys_{enter,exit}_SYSCALL tracepoints and for every other tracepoint that gets its arguments pretty printed. evsel__put_and_free_priv() only did zfree(&evsel->priv), releasing the evsel_trace itself and leaking that array. Use evsel_trace__delete(), which frees fmt first, exactly as the out_delete path of evsel__syscall_arg_fmt() already does. The current callers are all error paths that run before fmt can have been allocated, so nothing leaks in practice today, but the helper is the obvious thing to reach for whenever an evsel is discarded and it should be safe for that. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com> --- tools/perf/builtin-trace.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 20fffc24507b..f67557e7a254 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -464,7 +464,13 @@ static int evsel__init_tp_ptr_field(struct evsel *evsel, struct tp_field *field, static void evsel__put_and_free_priv(struct evsel *evsel) { - zfree(&evsel->priv); + /* + * evsel->priv is always a struct evsel_trace here, so it has to go + * through evsel_trace__delete(): zfree() on its own would release the + * struct while leaking the syscall_arg_fmt array hanging off it. + */ + evsel_trace__delete(evsel->priv); + evsel->priv = NULL; evsel__put(evsel); } -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 05/23] perf evsel: Report an allocation failure as ENOMEM when setting filters 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (3 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 04/23] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 06/23] perf trace: Start BPF summary before starting workload Ian Rogers ` (17 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz, Arnaldo Carvalho de Melo evsel__set_filter() and evsel__append_filter() return -1 when the strdup() or asprintf() that builds the new filter string fails, and evlist__set_tp_filter() and evlist__append_tp_filter() do the same when handed the NULL that asprintf__tp_filter_pids() returns for the same reason. In every case the only thing that can have gone wrong is an allocation. Callers all test the result with "< 0" or for being non-zero, so -1 has been as good as any other error so far, but it is not an errno and so cannot be printed as one. A caller that does, such as str_error_r(-err, errbuf, sizeof(errbuf)) turns it into EPERM and reports a failure to allocate as "Operation not permitted", which is no help to anyone trying to work out what happened. Return -ENOMEM instead, which is what these failures are. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com> --- tools/perf/util/evlist.c | 12 ++++++++++-- tools/perf/util/evsel.c | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tools/perf/util/evlist.c b/tools/perf/util/evlist.c index 22eb01116992..9392d912d254 100644 --- a/tools/perf/util/evlist.c +++ b/tools/perf/util/evlist.c @@ -1432,8 +1432,12 @@ int evlist__set_tp_filter(struct evlist *evlist, const char *filter) struct evsel *evsel; int err = 0; + /* + * The only caller that passes NULL is evlist__set_tp_filter_pids(), + * where it means asprintf__tp_filter_pids() failed to allocate. + */ if (filter == NULL) - return -1; + return -ENOMEM; evlist__for_each_entry(evlist, evsel) { if (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) @@ -1452,8 +1456,12 @@ int evlist__append_tp_filter(struct evlist *evlist, const char *filter) struct evsel *evsel; int err = 0; + /* + * As above, a NULL filter is asprintf__tp_filter_pids() having failed + * to allocate in evlist__append_tp_filter_pids(). + */ if (filter == NULL) - return -1; + return -ENOMEM; evlist__for_each_entry(evlist, evsel) { if (evsel->core.attr.type != PERF_TYPE_TRACEPOINT) diff --git a/tools/perf/util/evsel.c b/tools/perf/util/evsel.c index 0d0b2804d238..dd468716f424 100644 --- a/tools/perf/util/evsel.c +++ b/tools/perf/util/evsel.c @@ -1872,7 +1872,7 @@ int evsel__set_filter(struct evsel *evsel, const char *filter) return 0; } - return -1; + return -ENOMEM; } static int evsel__append_filter(struct evsel *evsel, const char *fmt, const char *filter) @@ -1888,7 +1888,7 @@ static int evsel__append_filter(struct evsel *evsel, const char *fmt, const char return 0; } - return -1; + return -ENOMEM; } int evsel__append_tp_filter(struct evsel *evsel, const char *filter) -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 06/23] perf trace: Start BPF summary before starting workload 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (4 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 05/23] perf evsel: Report an allocation failure as ENOMEM when setting filters Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 07/23] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers ` (16 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When using --bpf-summary, trace_start_bpf_summary() sets skel->bss->enabled = 1. In trace__run(), trace_start_bpf_summary() was previously invoked after evlist__start_workload(). Because evlist__start_workload() immediately unblocks the child process by writing to its go_pipe, short-lived workloads (such as `cat /dev/null`) can execute and complete their initial system calls before trace_start_bpf_summary() is reached by the parent process. Furthermore, under high system load, the child process may finish before the BPF summary tracking is enabled in the kernel at all, causing syscall summary tests to fail. Additionally, if initial_delay was configured, the workload was started before sleeping. Move trace_start_bpf_summary() to be invoked before evlist__start_workload(), matching evlist__enable(), and ensure it respects target.initial_delay. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f67557e7a254..f90c6bb4d8b4 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4844,17 +4844,19 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (!target__none(&trace->opts.target) && !trace->opts.target.initial_delay) evlist__enable(evlist); + if (trace->summary_bpf && !trace->opts.target.initial_delay) + trace_start_bpf_summary(); + if (forks) evlist__start_workload(evlist); if (trace->opts.target.initial_delay) { usleep(trace->opts.target.initial_delay * 1000); evlist__enable(evlist); + if (trace->summary_bpf) + trace_start_bpf_summary(); } - if (trace->summary_bpf) - trace_start_bpf_summary(); - trace->multiple_threads = perf_thread_map__pid(evlist__core(evlist)->threads, 0) == -1 || perf_thread_map__nr(evlist__core(evlist)->threads) > 1 || evlist__first(evlist)->core.attr.inherit; -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 07/23] perf trace: Skip internal tracepoint fields in formatting and beauty map 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (5 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 06/23] perf trace: Start BPF summary before starting workload Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 08/23] perf trace: Bounds check augmented arguments before reading them Ian Rogers ` (15 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Linux 6.19+ added __data_loc char[] internal fields for string arguments in syscalls:sys_enter_<name> tracepoints (e.g., __data_loc_oldname in sys_enter_renameat2). While is_internal_field() was added to detect them, several places did not properly account for them: 1. In syscall_arg_fmt__init_array(), when an internal field was skipped, the arg pointer was still incremented, causing the subsequent argument formatters to be mismatched. Leaving the slot out is only right for syscall tracepoints, where syscall__scnprintf_args() indexes the array the same packed way and sc->nr_args is counted to match. Every other tracepoint reaches the array through trace__fprintf_tp_fields(), which advances the field list and the array in lockstep, and for those a __data_loc char[] is an ordinary dynamic string to be shown rather than something the kernel appended to a syscall behind perf's back. Add a skip_internal argument so that each caller gets the layout it indexes. 2. In syscall__scnprintf_args(), internal fields were not skipped, causing spurious trailing arguments like ", 0, 16" to be formatted and printed. 3. In trace__bpf_sys_enter_beauty_map(), internal fields were not skipped, offsetting beauty array argument indices and breaking string and buffer augmentation. 4. In trace__find_usable_bpf_prog_entry(), candidate pointer checks matched on internal pointer fields, breaking signature compatibility matching between syscalls for augmenter sharing. Introduce next_user_arg() and advance both cursors with it, so that the two argument lists are always compared at a real argument and the walk ends when one syscall runs out of arguments rather than when one happens to have trailing internal fields. 5. syscall__augmented_args() computed the augmented payload as sample->raw_size - sc->args_size for any sys_enter style sample. sc->args_size deliberately stops at the last non-internal field, so on 6.19+ a native syscalls:sys_enter_<name> record leaves the __data_loc words and their string payloads in the remainder. Those bytes are not a struct augmented_arg, so syscall_arg__scnprintf_augmented_string() read a bogus length and walked arg->augmented.args out of bounds. This is reachable from trace__event_handler(), which calls trace__fprintf_sys_enter() for any evsel whose tracepoint name starts with "sys_enter_". 6. In syscall__read_info(), syscall__alloc_arg_fmts() was called before checking and dropping the leading __syscall_nr (or nr) field, using nr_fields - 1 unconditionally. If a tracepoint format lacks that leading field, the allocated arg_fmt array is one entry too small and syscall_arg_fmt__init_array() writes one entry past the end of the heap buffer. Drop __syscall_nr/nr first and size the allocation from the remaining fields. Update these functions to check and skip is_internal_field() so that arguments are correctly formatted and beauty map entries match the expected syscall signatures, restrict syscall__augmented_args() to the __augmented_syscalls__ bpf-output evsel, and size arg_fmt after dropping the syscall number field. Publish sc->args only once that allocation has succeeded. sc->name is set earlier in syscall__read_info(), and a later call takes a syscall with a name to have been read already and returns it as it stands, so a syscall left with arguments and no arg_fmt to describe them would be printed by walking the arguments and indexing an array that was never allocated. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 194 ++++++++++++++++++++++++++++--------- 1 file changed, 148 insertions(+), 46 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f90c6bb4d8b4..6ffd4a0718ca 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2276,22 +2276,40 @@ static bool field_is_ptr_sized(const struct tep_format_field *field) return field->size == ptr_size || field->size == sizeof(u64); } +/* + * @skip_internal: whether an internal field is left out of the array rather + * than given a slot of its own. + * + * Syscall tracepoints want it left out, as syscall__scnprintf_args() walks the + * array with an index that skips internal fields too and sc->nr_args is counted + * the same way. + * + * Other tracepoints do not: trace__fprintf_tp_fields() advances the field list + * and this array in lockstep, so every field needs a slot or the fields after + * an internal one are formatted with the wrong entry. For them a + * __data_loc char[] is also an ordinary dynamic string to be shown, rather than + * something the kernel appended to a syscall tracepoint behind perf's back. + */ static struct tep_format_field * syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field *field, - bool *use_btf) + bool *use_btf, bool skip_internal) { struct tep_format_field *last_field = NULL; int len; - for (; field; field = field->next, ++arg) { - /* assume it's the last argument */ - if (is_internal_field(field)) + for (; field; field = field->next) { + if (is_internal_field(field)) { + if (!skip_internal) + ++arg; continue; + } last_field = field; - if (arg->scnprintf) + if (arg->scnprintf) { + ++arg; continue; + } len = strlen(field->name); @@ -2348,6 +2366,7 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field } } } + ++arg; } return last_field; @@ -2356,7 +2375,8 @@ syscall_arg_fmt__init_array(struct syscall_arg_fmt *arg, struct tep_format_field static int syscall__set_arg_fmts(struct syscall *sc) { struct tep_format_field *last_field = syscall_arg_fmt__init_array(sc->arg_fmt, sc->args, - &sc->use_btf); + &sc->use_btf, + /*skip_internal=*/true); if (last_field) sc->args_size = last_field->offset + last_field->size; @@ -2368,7 +2388,8 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) { char tp_name[128]; const char *name; - struct tep_format_field *field; + struct tep_format_field *args, *field; + int nr_args; int err; if (sc->nonexistent) @@ -2407,24 +2428,35 @@ static int syscall__read_info(struct syscall *sc, struct trace *trace) return err; } - /* - * The tracepoint format contains __syscall_nr field, so it's one more - * than the actual number of syscall arguments. - */ - if (syscall__alloc_arg_fmts(sc, sc->tp_format->format.nr_fields - 1)) - return -ENOMEM; - - sc->args = sc->tp_format->format.fields; + args = sc->tp_format->format.fields; + nr_args = sc->tp_format->format.nr_fields; /* * We need to check and discard the first variable '__syscall_nr' * or 'nr' that mean the syscall number. It is needless here. * So drop '__syscall_nr' or 'nr' field but does not exist on older kernels. + * + * Do this before allocating, and size the array from what is left, so + * that a format without the field does not leave + * syscall_arg_fmt__init_array() walking one entry past the end. */ - if (sc->args && (!strcmp(sc->args->name, "__syscall_nr") || !strcmp(sc->args->name, "nr"))) { - sc->args = sc->args->next; - --sc->nr_args; + if (args && (!strcmp(args->name, "__syscall_nr") || !strcmp(args->name, "nr"))) { + args = args->next; + --nr_args; } + if (syscall__alloc_arg_fmts(sc, nr_args)) + return -ENOMEM; + + /* + * Only now that there is an arg_fmt for each of them are the arguments + * published. sc->name was set above, so a later syscall__read_info() + * takes this syscall to be read already and returns it as it stands; + * were sc->args set with sc->arg_fmt still NULL, the printing of that + * syscall would walk the arguments and index an array that does not + * exist. + */ + sc->args = args; + field = sc->args; while (field) { if (is_internal_field(field)) @@ -2452,7 +2484,8 @@ static int evsel__init_tp_arg_scnprintf(struct evsel *evsel, bool *use_btf) const struct tep_event *tp_format = evsel__tp_format(evsel); if (tp_format) { - syscall_arg_fmt__init_array(fmt, tp_format->format.fields, use_btf); + syscall_arg_fmt__init_array(fmt, tp_format->format.fields, use_btf, + /*skip_internal=*/false); return 0; } } @@ -2642,10 +2675,16 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, if (sc->args != NULL) { struct tep_format_field *field; - for (field = sc->args; field; - field = field->next, ++arg.idx, bit <<= 1) { - if (arg.mask & bit) + for (field = sc->args; field; field = field->next) { + /* Skip internal fields so they are not printed as spurious arguments */ + if (is_internal_field(field)) + continue; + + if (arg.mask & bit) { + ++arg.idx; + bit <<= 1; continue; + } arg.fmt = &sc->arg_fmt[arg.idx]; val = syscall_arg__val(&arg, arg.idx); @@ -2664,8 +2703,11 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, */ if (val == 0 && !trace->show_zeros && !(sc->arg_fmt && sc->arg_fmt[arg.idx].show_zero) && - !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) + !(sc->arg_fmt && sc->arg_fmt[arg.idx].strtoul == STUL_BTF_TYPE)) { + ++arg.idx; + bit <<= 1; continue; + } printed += scnprintf(bf + printed, size - printed, "%s", printed ? ", " : ""); @@ -2680,12 +2722,16 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size, size - printed, val, field->type); if (btf_printed) { printed += btf_printed; + ++arg.idx; + bit <<= 1; continue; } } printed += syscall_arg_fmt__scnprintf_val(&sc->arg_fmt[arg.idx], bf + printed, size - printed, &arg, val); + ++arg.idx; + bit <<= 1; } } else if (IS_ERR(sc->tp_format)) { /* @@ -2946,7 +2992,9 @@ static int trace__fprintf_sample(struct trace *trace, struct perf_sample *sample return printed; } -static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sample, int *augmented_args_size, int raw_augmented_args_size) +static void *syscall__augmented_args(struct trace *trace, struct syscall *sc, + struct perf_sample *sample, + int *augmented_args_size, int raw_augmented_args_size) { /* * For now with BPF raw_augmented we hook into raw_syscalls:sys_enter @@ -2964,6 +3012,24 @@ static void *syscall__augmented_args(struct syscall *sc, struct perf_sample *sam */ int args_size = raw_augmented_args_size ?: sc->args_size; + /* + * Augmented arguments are a perf trace specific payload, they are only + * ever appended to samples emitted by the BPF __augmented_syscalls__ + * bpf-output event. + * + * Native syscalls:sys_enter_NAME tracepoints may also carry trailing + * data of their own: since Linux 6.19 they append __data_loc char[] + * fields plus the string payloads they point at. Those bytes are not a + * struct augmented_arg, so treating them as one would make + * syscall_arg__scnprintf_augmented_string() read a bogus length and + * walk arg->augmented.args far out of bounds. + * + * So only look for augmented arguments on the event that can actually + * produce them. + */ + if (sample->evsel != trace->syscalls.events.bpf_output) + return NULL; + *augmented_args_size = sample->raw_size - args_size; if (*augmented_args_size > 0) { static uintptr_t argbuf[1024]; /* assuming single-threaded */ @@ -3022,17 +3088,13 @@ static int trace__sys_enter(struct trace *trace, if (!(trace->duration_filter || trace->summary_only || trace->min_stack)) trace__printf_interrupted_entry(trace); /* - * If this is raw_syscalls.sys_enter, then it always comes with the 6 possible - * arguments, even if the syscall being handled, say "openat", uses only 4 arguments - * this breaks syscall__augmented_args() check for augmented args, as we calculate - * syscall->args_size using each syscalls:sys_enter_NAME tracefs format file, - * so when handling, say the openat syscall, we end up getting 6 args for the - * raw_syscalls:sys_enter event, when we expected just 4, we end up mistakenly - * thinking that the extra 2 u64 args are the augmented filename, so just check - * here and avoid using augmented syscalls when the evsel is the raw_syscalls one. + * syscall__augmented_args() only returns a payload for the BPF + * __augmented_syscalls__ event, so raw_syscalls:sys_enter (which always + * carries all 6 possible arguments rather than sc->args_size worth) and + * the native syscalls:sys_enter_NAME tracepoints are both handled there. */ - if (evsel != trace->syscalls.events.sys_enter) - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); ttrace->entry_time = sample->time; ttrace->entry_cpu = sample->cpu; msg = ttrace->entry_str; @@ -3077,7 +3139,7 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam struct syscall *sc; char msg[1024]; void *args, *augmented_args = NULL; - int augmented_args_size, e_machine; + int augmented_args_size = 0, e_machine; size_t printed = 0; @@ -3095,7 +3157,8 @@ static int trace__fprintf_sys_enter(struct trace *trace, struct perf_sample *sam goto out_put; args = perf_evsel__sc_tp_ptr(args, sample); - augmented_args = syscall__augmented_args(sc, sample, &augmented_args_size, trace->raw_augmented_syscalls_args_size); + augmented_args = syscall__augmented_args(trace, sc, sample, &augmented_args_size, + trace->raw_augmented_syscalls_args_size); printed += syscall__scnprintf_args(sc, msg, sizeof(msg), args, augmented_args, augmented_args_size, trace, thread); fprintf(trace->output, "%.*s", (int)printed, msg); err = 0; @@ -4127,10 +4190,16 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i if (trace->btf == NULL) return -1; - for (i = 0, field = sc->args; field; ++i, field = field->next) { + for (i = 0, field = sc->args; field; field = field->next) { + /* Skip internal fields to keep beauty array index aligned with syscall arguments */ + if (is_internal_field(field)) + continue; + // XXX We're only collecting pointer payloads _from_ user space - if (!sc->arg_fmt[i].from_user) + if (!sc->arg_fmt[i].from_user) { + ++i; continue; + } struct_offset = strstr(field->type, "struct "); if (struct_offset == NULL) @@ -4149,8 +4218,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i name[cnt] = '\0'; /* cache struct's btf_type and type_id */ - if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) + if (syscall_arg_fmt__cache_btf_struct(&sc->arg_fmt[i], trace->btf, name)) { + ++i; continue; + } bt = sc->arg_fmt[i].type; beauty_array[i] = bt->size; @@ -4176,7 +4247,9 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i struct tep_format_field *field_tmp; /* find the size of the buffer that appears in pairs with buf */ - for (j = 0, field_tmp = sc->args; field_tmp; ++j, field_tmp = field_tmp->next) { + for (j = 0, field_tmp = sc->args; field_tmp; field_tmp = field_tmp->next) { + if (is_internal_field(field_tmp)) + continue; if (!(field_tmp->flags & TEP_FIELD_IS_POINTER) && /* only integers */ (strstr(field_tmp->name, "count") || strstr(field_tmp->name, "siz") || /* size, bufsiz */ @@ -4186,8 +4259,10 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i can_augment = true; break; } + ++j; } } + ++i; } if (can_augment) @@ -4196,6 +4271,19 @@ static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, i return -1; } +/* + * Advance to the first field that is a real syscall argument, so that callers + * walking two argument lists in step never have to reason about internal + * fields appearing in one list but not the other. + */ +static struct tep_format_field *next_user_arg(struct tep_format_field *field) +{ + while (field && is_internal_field(field)) + field = field->next; + + return field; +} + static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace, struct syscall *sc) { @@ -4203,7 +4291,7 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace /* * We're only interested in syscalls that have a pointer: */ - for (field = sc->args; field; field = field->next) { + for (field = next_user_arg(sc->args); field; field = next_user_arg(field->next)) { if (field->flags & TEP_FIELD_IS_POINTER) goto try_to_find_pair; } @@ -4221,21 +4309,31 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace pair->bpf_prog.sys_enter == unaugmented_prog) continue; - for (field = sc->args, candidate_field = pair->args; - field && candidate_field; field = field->next, candidate_field = candidate_field->next) { + /* + * Both cursors only ever point at real arguments, so the loop + * ends when one of the two syscalls runs out of them, rather + * than when one happens to have trailing internal fields. + */ + field = next_user_arg(sc->args); + candidate_field = next_user_arg(pair->args); + while (field && candidate_field) { bool is_pointer = field->flags & TEP_FIELD_IS_POINTER, candidate_is_pointer = candidate_field->flags & TEP_FIELD_IS_POINTER; if (is_pointer) { - if (!candidate_is_pointer) { + if (!candidate_is_pointer) { // The candidate just doesn't copies our pointer arg, might copy other pointers we want. + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; - } + } } else { if (candidate_is_pointer) { // The candidate might copy a pointer we don't have, skip it. goto next_candidate; } + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); continue; } @@ -4256,6 +4354,8 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace goto next_candidate; is_candidate = true; + field = next_user_arg(field->next); + candidate_field = next_user_arg(candidate_field->next); } if (!is_candidate) @@ -4267,7 +4367,9 @@ static struct bpf_program *trace__find_usable_bpf_prog_entry(struct trace *trace * more than what is common to the two syscalls. */ if (candidate_field) { - for (candidate_field = candidate_field->next; candidate_field; candidate_field = candidate_field->next) + candidate_field = next_user_arg(candidate_field->next); + for (; candidate_field; + candidate_field = next_user_arg(candidate_field->next)) if (candidate_field->flags & TEP_FIELD_IS_POINTER) goto next_candidate; } -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 08/23] perf trace: Bounds check augmented arguments before reading them 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (6 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 07/23] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 09/23] perf trace: Bound the fixed size augmented argument beautifiers Ian Rogers ` (14 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Augmented arguments are length prefixed payloads that the BPF augmenter appends to a sample: a struct augmented_arg header holding the payload length, followed by the payload. arg->augmented.args points at the next unconsumed one and arg->augmented.size says how many bytes are left. Both the length and the number of bytes remaining come from the sample, but syscall_arg__scnprintf_buf() takes the length on trust and loops over it with no bound at all, so a bogus value walks straight off the end of the static argbuf that syscall__augmented_args() copies into: #0 0x73104f in dump_stack debug.c:366 #1 0x7310c5 in sighandler_dump_stack debug.c:378 #2 0x7f749ea66fb0 in __restore_rt libc.so.6[19fb0] #3 0x4c0fa0 in syscall_arg__scnprintf_buf builtin-trace.c:1955 #4 0x4c2f3d in syscall_arg_fmt__scnprintf_val builtin-trace.c:2632 #5 0x4c33ae in syscall__scnprintf_args builtin-trace.c:2722 #6 0x4c43d3 in trace__sys_enter builtin-trace.c:3094 #7 0x4c7865 in trace__handle_event builtin-trace.c:4013 syscall_arg__scnprintf_augmented_string() likewise prints the payload with "%.*s" and a length it has not checked, and both dereference the header before testing it for NULL. btf_struct_scnprintf() already gets this right. Lift its check into a shared syscall_arg__augmented_args_valid() in beauty.h, which verifies that a whole header is present and that the payload it advertises fits in what is left, taking a @need argument for callers that go on to read a fixed sized type, and use it at each of these sites. An unusable payload now makes syscall_arg__scnprintf_filename() fall back to the vfs_getname probe or the raw pointer, as it does when there is no augmented data at all, rather than printing nothing. Reported-by: Arnaldo Carvalho de Melo <acme@kernel.org> Closes: https://lore.kernel.org/linux-perf-users/arJ-gpzqOHk-gF8T@x2/ Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 29 ++++++++++++++------- tools/perf/trace/beauty/beauty.h | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 6ffd4a0718ca..eeaaab44016c 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -1139,14 +1139,12 @@ static size_t btf_struct_scnprintf(const struct btf_type *type, struct btf *btf, LIBBPF_OPTS(btf_dump_opts, dump_opts); LIBBPF_OPTS(btf_dump_type_data_opts, dump_data_opts); - if (arg == NULL || arg->augmented.args == NULL || arg->augmented.size < (int)sizeof(*augmented_arg) || + /* btf_dump__dump_type_data() below reads type->size bytes of the payload. */ + if (!syscall_arg__augmented_args_valid(arg, type->size) || arg->fmt == NULL || !arg->fmt->from_user) return 0; augmented_arg = arg->augmented.args; - if (augmented_arg->size <= 0 || augmented_arg->size > arg->augmented.size - (int)sizeof(*augmented_arg) || - (size_t)augmented_arg->size < type->size) - return 0; dump_data_opts.compact = true; dump_data_opts.skip_names = !arg->trace->show_arg_names; @@ -1904,12 +1902,18 @@ static void thread__set_filename_pos(struct thread *thread, const char *bf, static size_t syscall_arg__scnprintf_augmented_string(struct syscall_arg *arg, char *bf, size_t size) { struct augmented_arg *augmented_arg = arg->augmented.args; - size_t printed = scnprintf(bf, size, "\"%.*s\"", augmented_arg->size, augmented_arg->value); + size_t printed; + int consumed; + + if (!syscall_arg__augmented_args_valid(arg, 0)) + return 0; + + printed = scnprintf(bf, size, "\"%.*s\"", augmented_arg->size, augmented_arg->value); /* * So that the next arg with a payload can consume its augmented arg, i.e. for rename* syscalls * we would have two strings, each prefixed by its size. */ - int consumed = sizeof(*augmented_arg) + augmented_arg->size; + consumed = sizeof(*augmented_arg) + augmented_arg->size; arg->augmented.args = ((void *)arg->augmented.args) + consumed; arg->augmented.size -= consumed; @@ -1922,7 +1926,12 @@ static size_t syscall_arg__scnprintf_filename(char *bf, size_t size, { unsigned long ptr = arg->val; - if (arg->augmented.args) + /* + * A truncated or otherwise unusable augmented payload is no better than + * not having one at all, so test it here and let the pointer or the + * vfs_getname probe name the file instead of printing nothing. + */ + if (syscall_arg__augmented_args_valid(arg, 0)) return syscall_arg__scnprintf_augmented_string(arg, bf, size); if (!arg->trace->vfs_getname) @@ -1938,13 +1947,15 @@ static size_t syscall_arg__scnprintf_filename(char *bf, size_t size, static size_t syscall_arg__scnprintf_buf(char *bf, size_t size, struct syscall_arg *arg) { struct augmented_arg *augmented_arg = arg->augmented.args; - unsigned char *orig = (unsigned char *)augmented_arg->value; size_t printed = 0; + unsigned char *orig; int consumed; - if (augmented_arg == NULL) + if (!syscall_arg__augmented_args_valid(arg, 0)) return 0; + orig = (unsigned char *)augmented_arg->value; + for (int j = 0; j < augmented_arg->size; ++j) { bool control_char = orig[j] <= MAX_CONTROL_CHAR || orig[j] >= MAX_ASCII; /* print control characters (0~31 and 127), and non-ascii characters in \(digits) */ diff --git a/tools/perf/trace/beauty/beauty.h b/tools/perf/trace/beauty/beauty.h index 0f4801c61a5b..71f93372c34e 100644 --- a/tools/perf/trace/beauty/beauty.h +++ b/tools/perf/trace/beauty/beauty.h @@ -124,6 +124,49 @@ struct syscall_arg { unsigned long syscall_arg__val(struct syscall_arg *arg, u8 idx); +/** + * syscall_arg__augmented_args_valid: can the next augmented arg be read? + * + * Augmented arguments are a series of length prefixed payloads that the BPF + * augmenter appends to a sample, with arg->augmented.args pointing at the next + * unconsumed one and arg->augmented.size saying how many bytes are left. + * + * Both the remaining byte count and the length prefix come from the sample, so + * neither can be trusted: a sample that carries no augmented arguments at all + * can still have a few bytes of ring buffer padding after the tracepoint + * payload, and those bytes then look like a struct augmented_arg holding an + * arbitrary length. + * + * Check that a whole header is present and that the payload it advertises fits + * in what is left, so that a beautifier reading augmented_arg->value stays + * inside the sample. + * + * @need: how many bytes of payload the caller goes on to read, zero when it is + * driven by the length prefix itself rather than by a fixed sized type. + */ +static inline bool syscall_arg__augmented_args_valid(struct syscall_arg *arg, size_t need) +{ + const struct augmented_arg *augmented_arg; + + if (arg == NULL || arg->augmented.args == NULL) + return false; + + /* The header has to be there before its size field can be read. */ + if (arg->augmented.size < (int)sizeof(*augmented_arg)) + return false; + + augmented_arg = arg->augmented.args; + + /* A negative length would make the bounds below meaningless. */ + if (augmented_arg->size < 0) + return false; + + if (augmented_arg->size > arg->augmented.size - (int)sizeof(*augmented_arg)) + return false; + + return (size_t)augmented_arg->size >= need; +} + size_t syscall_arg__scnprintf_strarray_flags(char *bf, size_t size, struct syscall_arg *arg); #define SCA_STRARRAY_FLAGS syscall_arg__scnprintf_strarray_flags -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 09/23] perf trace: Bound the fixed size augmented argument beautifiers 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (7 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 08/23] perf trace: Bounds check augmented arguments before reading them Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 10/23] perf trace: Do not read sample padding as an augmented argument Ian Rogers ` (13 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The timespec, sockaddr and perf_event_attr beautifiers read a fixed sized type out of the augmented payload without checking that one is there. The payload is only as long as the augmenter made it, which for a sockaddr is the addrlen the tracee passed and so can be as little as two bytes, and which for a perf_event_attr is the attr size the tracee chose. Gate each of them on syscall_arg__augmented_args_valid() with the size it needs before its first read, so that a payload too short for the type leaves the raw pointer printed instead, as happens when there is no augmented data at all. Two reads that are unbounded for reasons other than the header go with them: - perf_event_attr__fprintf() always reads a whole struct perf_event_attr, so an attr from an older version is read past its end. Copy the payload into a zero padded local instead of reading it in place, which also replaces the local copy that was only made to fix up a zero size field. - af_local__scnprintf() prints sun_path as a plain string, but the payload holds only addrlen bytes and the path need not be NUL terminated. Pass the payload length to the address family printers so the path is printed with a bound, and so that the AF_INET and AF_INET6 printers do not read a sockaddr_in or sockaddr_in6 out of a payload too short to hold one. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/trace/beauty/perf_event_open.c | 38 +++++++++------- tools/perf/trace/beauty/sockaddr.c | 55 ++++++++++++++++++----- tools/perf/trace/beauty/timespec.c | 2 +- 3 files changed, 67 insertions(+), 28 deletions(-) diff --git a/tools/perf/trace/beauty/perf_event_open.c b/tools/perf/trace/beauty/perf_event_open.c index 6315b46bcdf0..fba8993f3b93 100644 --- a/tools/perf/trace/beauty/perf_event_open.c +++ b/tools/perf/trace/beauty/perf_event_open.c @@ -81,33 +81,39 @@ static size_t perf_event_attr___scnprintf(struct perf_event_attr *attr, char *bf static size_t syscall_arg__scnprintf_augmented_perf_event_attr(struct syscall_arg *arg, char *bf, size_t size) { - struct perf_event_attr *attr = (void *)arg->augmented.args->value; + const struct augmented_arg *augmented_arg = arg->augmented.args; struct perf_event_attr local_attr; + size_t copied = (size_t)augmented_arg->size; /* - * augmented_raw_syscalls.bpf.c (shipped with perf) copies - * PERF_ATTR_SIZE_VER0 bytes when the tracee passes size=0, - * but leaves the size field as 0. The payload size is - * guaranteed by perf's own BPF program, not externally - * controllable. Copy to a local so we can fix up size - * without writing to the potentially read-only augmented - * args buffer. + * The tracee picks attr.size, so the payload that + * augmented_raw_syscalls.bpf.c copied can be anything from + * PERF_ATTR_SIZE_VER0 up to the size this build of perf knows about, + * while perf_event_attr__fprintf() below always reads a whole struct + * perf_event_attr. Work on a zero padded local copy so that a payload + * from an older attr is not read past its end, which also lets the size + * be fixed up without writing to the augmented args buffer. */ - if (!attr->size) { - memcpy(&local_attr, attr, PERF_ATTR_SIZE_VER0); - memset((void *)&local_attr + PERF_ATTR_SIZE_VER0, 0, - sizeof(local_attr) - PERF_ATTR_SIZE_VER0); + if (copied > sizeof(local_attr)) + copied = sizeof(local_attr); + + memcpy(&local_attr, augmented_arg->value, copied); + memset((void *)&local_attr + copied, 0, sizeof(local_attr) - copied); + + /* + * The BPF program copies PERF_ATTR_SIZE_VER0 bytes when the tracee + * passes size=0, but leaves the size field as it found it. + */ + if (!local_attr.size) local_attr.size = PERF_ATTR_SIZE_VER0; - attr = &local_attr; - } - return perf_event_attr___scnprintf(attr, bf, size, + return perf_event_attr___scnprintf(&local_attr, bf, size, trace__show_zeros(arg->trace)); } size_t syscall_arg__scnprintf_perf_event_attr(char *bf, size_t size, struct syscall_arg *arg) { - if (arg->augmented.args) + if (syscall_arg__augmented_args_valid(arg, PERF_ATTR_SIZE_VER0)) return syscall_arg__scnprintf_augmented_perf_event_attr(arg, bf, size); return scnprintf(bf, size, "%#lx", arg->val); diff --git a/tools/perf/trace/beauty/sockaddr.c b/tools/perf/trace/beauty/sockaddr.c index a17a27ac2a6f..173402400018 100644 --- a/tools/perf/trace/beauty/sockaddr.c +++ b/tools/perf/trace/beauty/sockaddr.c @@ -10,21 +10,36 @@ #include "trace/beauty/generated/sockaddr.c" DEFINE_STRARRAY(socket_families, "PF_"); -static size_t af_inet__scnprintf(struct sockaddr *sa, char *bf, size_t size) +/* + * The augmented payload is only as long as the addrlen the tracee passed, so + * each of these gets that length and prints nothing rather than reading a + * struct that isn't fully there. + */ +static size_t af_inet__scnprintf(struct sockaddr *sa, size_t sa_size, char *bf, size_t size) { struct sockaddr_in *sin = (struct sockaddr_in *)sa; char tmp[16]; + + if (sa_size < sizeof(*sin)) + return 0; + return scnprintf(bf, size, ", port: %d, addr: %s", ntohs(sin->sin_port), inet_ntop(sin->sin_family, &sin->sin_addr, tmp, sizeof(tmp))); } -static size_t af_inet6__scnprintf(struct sockaddr *sa, char *bf, size_t size) +static size_t af_inet6__scnprintf(struct sockaddr *sa, size_t sa_size, char *bf, size_t size) { struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)sa; - u32 flowinfo = ntohl(sin6->sin6_flowinfo); + u32 flowinfo; char tmp[512]; - size_t printed = scnprintf(bf, size, ", port: %d, addr: %s", ntohs(sin6->sin6_port), - inet_ntop(sin6->sin6_family, &sin6->sin6_addr, tmp, sizeof(tmp))); + size_t printed; + + if (sa_size < sizeof(*sin6)) + return 0; + + flowinfo = ntohl(sin6->sin6_flowinfo); + printed = scnprintf(bf, size, ", port: %d, addr: %s", ntohs(sin6->sin6_port), + inet_ntop(sin6->sin6_family, &sin6->sin6_addr, tmp, sizeof(tmp))); if (flowinfo != 0) printed += scnprintf(bf + printed, size - printed, ", flowinfo: %lu", flowinfo); if (sin6->sin6_scope_id != 0) @@ -33,13 +48,28 @@ static size_t af_inet6__scnprintf(struct sockaddr *sa, char *bf, size_t size) return printed; } -static size_t af_local__scnprintf(struct sockaddr *sa, char *bf, size_t size) +static size_t af_local__scnprintf(struct sockaddr *sa, size_t sa_size, char *bf, size_t size) { struct sockaddr_un *sun = (struct sockaddr_un *)sa; - return scnprintf(bf, size, ", path: %s", sun->sun_path); + size_t path_size; + + if (sa_size <= offsetof(struct sockaddr_un, sun_path)) + return 0; + + /* + * Unlike the other families the path is variable length, and it need + * not be NUL terminated, so print at most what the payload holds. An + * abstract socket's name starts with a NUL, which %.*s stops at, as + * printing it as a plain string used to. + */ + path_size = sa_size - offsetof(struct sockaddr_un, sun_path); + if (path_size > sizeof(sun->sun_path)) + path_size = sizeof(sun->sun_path); + + return scnprintf(bf, size, ", path: %.*s", (int)path_size, sun->sun_path); } -static size_t (*af_scnprintfs[])(struct sockaddr *sa, char *bf, size_t size) = { +static size_t (*af_scnprintfs[])(struct sockaddr *sa, size_t sa_size, char *bf, size_t size) = { [AF_LOCAL] = af_local__scnprintf, [AF_INET] = af_inet__scnprintf, [AF_INET6] = af_inet6__scnprintf, @@ -47,7 +77,9 @@ static size_t (*af_scnprintfs[])(struct sockaddr *sa, char *bf, size_t size) = { static size_t syscall_arg__scnprintf_augmented_sockaddr(struct syscall_arg *arg, char *bf, size_t size) { - struct sockaddr *sa = (struct sockaddr *)&arg->augmented.args->value; + const struct augmented_arg *augmented_arg = arg->augmented.args; + struct sockaddr *sa = (struct sockaddr *)&augmented_arg->value; + size_t sa_size = (size_t)augmented_arg->size; char family[32]; size_t printed; @@ -55,14 +87,15 @@ static size_t syscall_arg__scnprintf_augmented_sockaddr(struct syscall_arg *arg, printed = scnprintf(bf, size, "{ .family: %s", family); if (sa->sa_family < ARRAY_SIZE(af_scnprintfs) && af_scnprintfs[sa->sa_family]) - printed += af_scnprintfs[sa->sa_family](sa, bf + printed, size - printed); + printed += af_scnprintfs[sa->sa_family](sa, sa_size, bf + printed, size - printed); return printed + scnprintf(bf + printed, size - printed, " }"); } size_t syscall_arg__scnprintf_sockaddr(char *bf, size_t size, struct syscall_arg *arg) { - if (arg->augmented.args) + /* Only the family is read unconditionally, the rest is up to the family printer. */ + if (syscall_arg__augmented_args_valid(arg, sizeof(sa_family_t))) return syscall_arg__scnprintf_augmented_sockaddr(arg, bf, size); return scnprintf(bf, size, "%#lx", arg->val); diff --git a/tools/perf/trace/beauty/timespec.c b/tools/perf/trace/beauty/timespec.c index b14ab72a2738..8da0b28be0af 100644 --- a/tools/perf/trace/beauty/timespec.c +++ b/tools/perf/trace/beauty/timespec.c @@ -14,7 +14,7 @@ static size_t syscall_arg__scnprintf_augmented_timespec(struct syscall_arg *arg, size_t syscall_arg__scnprintf_timespec(char *bf, size_t size, struct syscall_arg *arg) { - if (arg->augmented.args) + if (syscall_arg__augmented_args_valid(arg, sizeof(struct timespec))) return syscall_arg__scnprintf_augmented_timespec(arg, bf, size); return scnprintf(bf, size, "%#lx", arg->val); -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 10/23] perf trace: Do not read sample padding as an augmented argument 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (8 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 09/23] perf trace: Bound the fixed size augmented argument beautifiers Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 11/23] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers ` (12 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz syscall__augmented_args() treats everything after the fixed tracepoint payload as augmented arguments, taking any non-zero trailing length as a struct augmented_arg. A record with no augmented arguments at all still has a trailing run, because the raw payload is padded. perf_sample_save_raw_data() sizes the raw data as: size = round_up(sum + sizeof(u32), sizeof(u64)); raw->size = size - sizeof(u32); frag->pad = raw->size - sum; and the kernel writes that padding with __output_skip(), which advances over it rather than zeroing it, so the bytes are whatever the ring buffer last held there. A 64 byte struct syscall_enter_args therefore arrives with raw_size of 68, and the 4 bytes past the end are stale memory that syscall__augmented_args() copies out and hands to a beautifier as the size and int_arg of an augmented argument. A trailing run shorter than a struct augmented_arg cannot be one, so recognise it as the padding it is. The length prefix and the payload it describes are checked separately by syscall_arg__augmented_args_valid(); this stops the padding being offered as augmented data in the first place, so that the syscall appears with no augmented arguments as it should rather than with one whose contents happen to pass validation. Reported-by: Arnaldo Carvalho de Melo <acme@kernel.org> Closes: https://lore.kernel.org/linux-perf-users/arJ-gpzqOHk-gF8T@x2/ Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 47 ++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index eeaaab44016c..aa2d64eb56bd 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -3022,6 +3022,7 @@ static void *syscall__augmented_args(struct trace *trace, struct syscall *sc, * traffic to just what is needed for each syscall. */ int args_size = raw_augmented_args_size ?: sc->args_size; + static uintptr_t argbuf[1024]; /* assuming single-threaded */ /* * Augmented arguments are a perf trace specific payload, they are only @@ -3042,24 +3043,40 @@ static void *syscall__augmented_args(struct trace *trace, struct syscall *sc, return NULL; *augmented_args_size = sample->raw_size - args_size; - if (*augmented_args_size > 0) { - static uintptr_t argbuf[1024]; /* assuming single-threaded */ - if ((size_t)(*augmented_args_size) > sizeof(argbuf)) - return NULL; - - /* - * The perf ring-buffer is 8-byte aligned but sample->raw_data - * is not because it's preceded by u32 size. Later, beautifier - * will use the augmented args with stricter alignments like in - * some struct. To make sure it's aligned, let's copy the args - * into a static buffer as it's single-threaded for now. - */ - memcpy(argbuf, sample->raw_data + args_size, *augmented_args_size); + /* + * perf_sample_save_raw_data() rounds the raw payload up to a multiple + * of 8 bytes less the u32 that holds its size, and the kernel skips + * over that padding rather than zeroing it, so those bytes are stale + * ring buffer contents. + * + * A record that carries no augmented arguments at all therefore still + * arrives with up to 7 trailing bytes, e.g. the 64 byte struct + * syscall_enter_args that an unaugmented syscall emits comes back with + * raw_size of 68. Anything shorter than a struct augmented_arg cannot + * be one, so drop it instead of letting a beautifier read a length out + * of uninitialised memory. + */ + if (*augmented_args_size < (int)sizeof(struct augmented_arg)) { + *augmented_args_size = 0; + return NULL; + } - return argbuf; + if ((size_t)(*augmented_args_size) > sizeof(argbuf)) { + *augmented_args_size = 0; + return NULL; } - return NULL; + + /* + * The perf ring-buffer is 8-byte aligned but sample->raw_data + * is not because it's preceded by u32 size. Later, beautifier + * will use the augmented args with stricter alignments like in + * some struct. To make sure it's aligned, let's copy the args + * into a static buffer as it's single-threaded for now. + */ + memcpy(argbuf, sample->raw_data + args_size, *augmented_args_size); + + return argbuf; } static int trace__sys_enter(struct trace *trace, -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 11/23] perf trace: Do not set unaugmented BPF program on sys_exit map 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (9 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 10/23] perf trace: Do not read sample padding as an augmented argument Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 12/23] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers ` (11 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz In trace__init_syscalls_bpf_prog_array_maps(), the BPF program array map for sys_exit (syscalls_sys_exit) was populated with the result of trace__bpf_prog_sys_exit_fd(). When a syscall had no specific exit augmenter, trace__find_syscall_bpf_prog() fell back to unaugmented_prog (syscall_unaugmented). However, syscall_unaugmented is a sys_enter program that outputs enter arguments to __augmented_syscalls__. As a consequence, when an unaugmented syscall exited, sys_exit tail-called syscall_unaugmented, which interpreted the exit arguments as enter arguments and emitted a duplicate, corrupt sys_enter event into __augmented_syscalls__ right as the syscall completed. Fix this by: 1. Returning NULL from trace__find_syscall_bpf_prog() when looking up exit augmenters and none is found. 2. Returning -1 from trace__bpf_prog_sys_exit_fd() when no exit program is present. 3. Only updating map_exit_fd when prog_fd >= 0. 4. Clearing err = 0 when trace__bpf_sys_enter_beauty_map() returns non-zero (indicating the syscall has no augmentable pointer arguments) before continuing the loop, so a trailing run of such syscalls (e.g. 'perf trace -e close') does not leave err non-zero on return and abort the session. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index aa2d64eb56bd..0ae14ecd9f00 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4176,7 +4176,12 @@ static struct bpf_program *trace__find_syscall_bpf_prog(struct trace *trace __ma pr_debug("Couldn't find BPF prog \"%s\" to associate with syscalls:sys_%s_%s, not augmenting it\n", prog_name, type, sc->name); out_unaugmented: - return unaugmented_prog; + /* + * Do not set unaugmented_prog for exit: syscall_unaugmented is a + * sys_enter program that outputs enter arguments. Exit without a + * specialized return augmenter returns 1 directly from sys_exit. + */ + return !strcmp(type, "exit") ? NULL : unaugmented_prog; } static void trace__init_syscall_bpf_progs(struct trace *trace, int e_machine, int id) @@ -4199,7 +4204,7 @@ static int trace__bpf_prog_sys_enter_fd(struct trace *trace, int e_machine, int static int trace__bpf_prog_sys_exit_fd(struct trace *trace, int e_machine, int id) { struct syscall *sc = trace__syscall_info(trace, NULL, e_machine, id); - return sc ? bpf_program__fd(sc->bpf_prog.sys_exit) : bpf_program__fd(unaugmented_prog); + return sc && sc->bpf_prog.sys_exit ? bpf_program__fd(sc->bpf_prog.sys_exit) : -1; } static int trace__bpf_sys_enter_beauty_map(struct trace *trace, int e_machine, int key, unsigned int *beauty_array) @@ -4454,16 +4459,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace, int e_m err = bpf_map_update_elem(map_enter_fd, &key, &prog_fd, BPF_ANY); if (err) break; + /* Only update the exit prog array map if an exit augmenter exists */ prog_fd = trace__bpf_prog_sys_exit_fd(trace, e_machine, key); - err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); - if (err) - break; + if (prog_fd >= 0) { + err = bpf_map_update_elem(map_exit_fd, &key, &prog_fd, BPF_ANY); + if (err) + break; + } /* use beauty_map to tell BPF how many bytes to collect, set beauty_map's value here */ memset(beauty_array, 0, sizeof(beauty_array)); err = trace__bpf_sys_enter_beauty_map(trace, e_machine, key, (unsigned int *)beauty_array); - if (err) + if (err) { + /* + * Not a failure: the syscall just has no augmentable + * arguments. Clear err, or a trailing run of such + * syscalls, e.g. all of them for 'perf trace -e close', + * would leave it set on return and abort the session. + */ + err = 0; continue; + } err = bpf_map_update_elem(beauty_map_fd, &key, beauty_array, BPF_ANY); if (err) break; -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 12/23] perf trace: Filter events in BPF and avoid tracepoint vetoes 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (10 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 11/23] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 13/23] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers ` (10 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The BPF augmented_raw_syscalls sys_enter and sys_exit programs returned 0 for syscalls that were not of interest. Returning 0 from a tracepoint BPF program vetoes the event for the whole system, so an unrelated concurrent perf trace, perf record or ftrace session listening to raw_syscalls would silently lose events. This is a cross-session side effect and shows up as flaky failures when perf tests run in parallel. Furthermore, syscall_unaugmented previously returned 1 without writing anything to the __augmented_syscalls__ ring buffer. This forced userspace perf trace to listen to both raw_syscalls:sys_enter and __augmented_syscalls__ in its evlist, requiring userspace event deduplication. Address these issues: 1. In augmented_raw_syscalls.bpf.c, never return 0 from tracepoint handlers: return 1 so non-traced syscalls pass through without vetoing other concurrent listeners. 2. Introduce pids_to_trace and syscalls_to_trace BPF hash maps to perform targeted filtering directly in BPF. Unselected syscalls or PIDs return 1 immediately without writing to the buffer. 3. In syscall_unaugmented, output the unaugmented enter payload into __augmented_syscalls__ and return 1. Change its section from SEC("tp/raw_syscalls/sys_enter") to SEC("tp/syscalls/sys_enter_unaugmented") so libbpf does not attempt to auto-attach it to raw_syscalls:sys_enter. 4. In bpf_trace_augment.c, add helpers to configure target PIDs and syscalls in the BPF maps, setting the activation flags (has_pids_to_trace, has_syscalls_to_trace) only after the maps are fully populated so already-attached BPF programs do not filter against a half-filled map. Explicitly attach only sys_enter and sys_exit via an attach_prog() helper that saves -errno before calling pr_debug() or bpf_link__destroy(). Destroy the skeleton on every failure path. Leaving a loaded but unusable skeleton behind is not inert: the setters called later from trace__run() would program its maps, and a partial attach would leave a BPF program live on raw_syscalls for a session that never starts. Since augmented_syscalls__{prepare,create_bpf_output}() failures fall back to unaugmented tracing rather than aborting, those setters have to become no-ops, which they only do once skel is NULL again. errno is used directly here, so include <errno.h> rather than relying on it arriving via another header, which it does not under musl. 5. In builtin-trace.c, hook trace__set_ev_qualifier_filter() and PID filtering into the BPF maps. When __augmented_syscalls__ is active, remove raw_syscalls:sys_enter from trace.evlist since all traced enter events (both augmented and unaugmented) are now emitted by BPF into __augmented_syscalls__. Identify that evsel by comparing against trace.syscalls.events.sys_enter rather than by a strstr() of its name. The substring "syscalls:sys_enter" also matches the per syscall syscalls:sys_enter_SYSCALL tracepoints, which a user can ask for by name, and now that the match decides what is taken out of the evlist, claiming one of those would drop an event that was asked for and would describe __augmented_syscalls__ with its format rather than the raw one's. Restore tracking on the remaining evsel via evlist__set_tracking_event() so PERF_RECORD_COMM and tracking events continue to be recorded. Errors from augmented_syscalls__set_target_syscalls() are reported and propagated, the tracepoint filter string is freed on every exit path, and an allocation failure in trace__set_filter_pids() now returns -ENOMEM instead of being silently ignored. Note that in trace__set_filter_pids() the target pids and the filtered pids are two independent axes and both have to be programmed. Naming pids to leave out with --filter-pids does not widen -p/-t or a workload to the whole system, and a BPF tracepoint program is attached system wide rather than to the target's file descriptors, so pids_to_trace is the only thing keeping other tasks out. 6. Add --syscall-augment option (defaulting to true) to allow users to explicitly use --no-syscall-augment to run perf trace in the classic unaugmented tracepoint mode without BPF. When BPF is unavailable or disabled, ensure the non-augmented tracepoint path cleanly configures sys_enter and sys_exit without duplicate entries. In trace__run(), add those tracepoints only when neither of them is in the evlist already. trace__add_syscall_newtp() adds the pair whatever is there, and cmd_trace() may have left either or both behind: both when it prepared augmentation and then gave up on it, the exit one alone when __augmented_syscalls__ took over the enters. Neither is covered by trace.raw_augmented_syscalls, which is only set when the exit evsel is named raw_syscalls:sys_exit, and not on a kernel old enough for perf_evsel__raw_syscall_newtp() to have fallen back to syscalls:sys_exit. Adding the pair again reports the events that were there twice, which the fallback after a bpf-output failure already did before this. Act on trace__add_syscall_newtp()'s result in cmd_trace() rather than discarding it. Those tracepoints are what the augmented events are described and paired with, so without them there is nothing to augment: the skeleton is dropped and the session carries on with plain tracepoints, which is what the bpf-output failure beside it already does, and what lets the evlist test above rely on the exit tracepoint being there whenever the bpf-output event is. A target that does not fit in the map is not a reason to give up on the session. bpf_map__update_elem() answers -E2BIG once max_entries keys are present, so a target with more threads than pids_to_trace has room for would take the whole of perf trace down with it, on exactly the large workloads where there is least else to reach for. The tasks that fit are added, a warning says how many did not, and the ones left out are in the position they would have been in had they been created once the map was already full, which sched_process_fork() has to allow for regardless. Report a failure to program the pid filters with the error that caused it. trace__run() sent everything trace__set_filter_pids() returned to out_error_mem, which prints "Not enough memory to run!". That was already a guess, and becomes a wrong one now that the function also writes BPF maps, which fail for reasons of their own that have nothing to do with memory and that the message gives the user no way to act on. Report a failure to program the syscall filters the same way. The pr_err() in trace__set_ev_qualifier_filter() ran before trace__run() reached out_errno and printed "%m", and anything called in between could have changed errno by then, so the two did not have to agree. The inner report becomes a pr_debug() and the caller prints the error it was given, which leaves out_errno without a user and it is removed. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/Documentation/perf-trace.txt | 5 + tools/perf/builtin-trace.c | 222 +++++++++++++++--- .../bpf_skel/augmented_raw_syscalls.bpf.c | 133 +++++++++-- tools/perf/util/bpf_trace_augment.c | 169 ++++++++++++- tools/perf/util/trace_augment.h | 34 +++ 5 files changed, 511 insertions(+), 52 deletions(-) diff --git a/tools/perf/Documentation/perf-trace.txt b/tools/perf/Documentation/perf-trace.txt index d20b43ea3d37..4680c69160d7 100644 --- a/tools/perf/Documentation/perf-trace.txt +++ b/tools/perf/Documentation/perf-trace.txt @@ -260,6 +260,11 @@ the thread executes on the designated CPUs. Default is to monitor all CPUs. Maximum number of lines in the summary mode. Note that this applies to each entry (thread or cgroup). +--syscall-augment:: + Augment syscalls with BPF. Enabled by default when BPF support is available. + Use --no-syscall-augment to disable BPF augmentation and fall back to the + unaugmented tracepoint approach. + PAGEFAULTS ---------- diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 0ae14ecd9f00..f7226331c311 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -200,6 +200,7 @@ struct trace { int max_summary; int raw_augmented_syscalls_args_size; bool raw_augmented_syscalls; + bool syscall_augment; bool fd_path_disabled; bool sort_events; bool not_ev_qualifier; @@ -2071,6 +2072,23 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; + case PERF_RECORD_FORK: + if (trace->raw_augmented_syscalls && + (augmented_syscalls__has_target_pid(event->fork.ppid) || + augmented_syscalls__has_target_pid(event->fork.ptid))) { + augmented_syscalls__add_target_pid(event->fork.pid); + } + ret = machine__process_fork_event(machine, event, sample); + break; + case PERF_RECORD_EXIT: + if (trace->raw_augmented_syscalls) { + if (event->fork.pid == event->fork.tid) + augmented_syscalls__del_target_pid(event->fork.pid); + else + augmented_syscalls__del_target_pid(event->fork.tid); + } + ret = machine__process_exit_event(machine, event, sample); + break; default: ret = machine__process_event(machine, event, sample); break; @@ -4102,7 +4120,7 @@ static int trace__add_syscall_newtp(struct trace *trace) static int trace__set_ev_qualifier_tp_filter(struct trace *trace) { - int err = -1; + int err = 0; struct evsel *sys_exit; char *filter = asprintf_expr_inout_ints("id", !trace->not_ev_qualifier, trace->ev_qualifier_ids.nr, @@ -4111,15 +4129,27 @@ static int trace__set_ev_qualifier_tp_filter(struct trace *trace) if (filter == NULL) goto out_enomem; - if (!evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter)) { - sys_exit = trace->syscalls.events.sys_exit; + /* + * With BPF augmentation sys_enter is filtered in BPF and removed from + * the evlist, so only apply the tracepoint filter to the events that + * are actually present. + */ + if (trace->syscalls.events.sys_enter) + err = evsel__append_tp_filter(trace->syscalls.events.sys_enter, filter); + + sys_exit = trace->syscalls.events.sys_exit; + if (!err && sys_exit) err = evsel__append_tp_filter(sys_exit, filter); - } free(filter); out: return err; out_enomem: + /* + * err defaults to 0 because either tracepoint may legitimately be + * absent, so the error has to be set explicitly here. + */ + err = -ENOMEM; errno = ENOMEM; goto out; } @@ -4561,7 +4591,27 @@ static int trace__init_syscalls_bpf_prog_array_maps(struct trace *trace __maybe_ static int trace__set_ev_qualifier_filter(struct trace *trace) { - if (trace->syscalls.events.sys_enter) + /* + * Synchronize syscall filter with BPF augmenter map: + * Pass trace->not_ev_qualifier to indicate blacklist mode ('!' prefix, + * e.g., -e !open,close) vs whitelist mode (-e open,close). + * + * A failure here would leave the BPF program filtering on a partially + * populated map, silently dropping or emitting the wrong syscalls, so + * propagate the error rather than continuing. + */ + if (trace->ev_qualifier_ids.nr > 0) { + int err = augmented_syscalls__set_target_syscalls(trace->ev_qualifier_ids.nr, + trace->ev_qualifier_ids.entries, + trace->not_ev_qualifier); + + if (err) { + pr_debug("Failed to set the syscalls to trace in the BPF map: %d\n", err); + return err; + } + } + + if (trace->syscalls.events.sys_enter || trace->syscalls.events.sys_exit) return trace__set_ev_qualifier_tp_filter(trace); return 0; } @@ -4602,13 +4652,21 @@ static int trace__set_filter_loop_pids(struct trace *trace) static int trace__set_filter_pids(struct trace *trace) { - int err = 0; + struct perf_thread_map *threads = evlist__core(trace->evlist)->threads; /* * Better not use !target__has_task() here because we need to cover the * case where no threads were specified in the command line, but a * workload was, and in that case we will fill in the thread_map when * we fork the workload in evlist__prepare_workload. */ + bool has_target = perf_thread_map__pid(threads, 0) != -1; + int err = 0; + + /* + * The exclusion list: --filter-pids names tasks to never report, and + * with no target at all we instead exclude perf itself so that tracing + * does not feed back into itself. + */ if (trace->filter_pids.nr > 0) { err = evlist__append_tp_filter_pids(trace->evlist, trace->filter_pids.nr, trace->filter_pids.entries); @@ -4616,10 +4674,37 @@ static int trace__set_filter_pids(struct trace *trace) err = augmented_syscalls__set_filter_pids(trace->filter_pids.nr, trace->filter_pids.entries); } - } else if (perf_thread_map__pid(evlist__core(trace->evlist)->threads, 0) == -1) { + } else if (!has_target) { err = trace__set_filter_loop_pids(trace); } + if (err) + return err; + + /* + * The inclusion list, which is a separate axis from the exclusion list + * above and so must be programmed even when --filter-pids was given: + * naming tasks to leave out does not widen -p/-t or a workload to the + * whole system. + * + * This matters more than it does on the tracepoint only path. A BPF + * tracepoint program is attached system wide rather than to the + * target's file descriptors, so pids_to_trace is the only thing + * keeping other tasks out. + */ + if (has_target) { + int nr = perf_thread_map__nr(threads); + pid_t *pids = malloc(nr * sizeof(pid_t)); + + if (pids == NULL) + return -ENOMEM; + + for (int i = 0; i < nr; i++) + pids[i] = perf_thread_map__pid(threads, i); + err = augmented_syscalls__set_target_pids(nr, pids); + free(pids); + } + return err; } @@ -4846,7 +4931,20 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } if (!trace->raw_augmented_syscalls) { - if (trace->trace_syscalls && trace__add_syscall_newtp(trace)) + /* + * The syscall tracepoints may be in the evlist already: + * cmd_trace() adds the pair whenever it prepares BPF + * augmentation, and it then either keeps them, gives up on + * augmentation and leaves them behind, or has + * __augmented_syscalls__ take over the enters and removes only + * the enter one. trace__add_syscall_newtp() adds the pair + * whatever is there, so it is called only when neither is, and + * every event it adds is one the evlist does not already have. + */ + bool have_syscall_tp = trace->syscalls.events.sys_enter != NULL || + trace->syscalls.events.sys_exit != NULL; + + if (trace->trace_syscalls && !have_syscall_tp && trace__add_syscall_newtp(trace)) goto out_error_raw_syscalls; if (trace->trace_syscalls) @@ -4941,7 +5039,7 @@ static int trace__run(struct trace *trace, int argc, const char **argv) err = trace__set_filter_pids(trace); if (err < 0) - goto out_error_mem; + goto out_error_filter_pids; /* * TODO: Initialize for all host binary machine types, not just @@ -4952,7 +5050,7 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (trace->ev_qualifier_ids.nr > 0) { err = trace__set_ev_qualifier_filter(trace); if (err < 0) - goto out_errno; + goto out_error_ev_qualifier; if (trace->syscalls.events.sys_exit) { pr_debug("event qualifier tracepoint filter: %s\n", @@ -5132,14 +5230,32 @@ static int trace__run(struct trace *trace, int argc, const char **argv) "Failed to set filter \"%s\" on event %s: %m\n", evsel->filter, evsel__name(evsel)); goto out_put_evlist; + +out_error_filter_pids: + /* + * Report what actually went wrong. Programming the pid filters + * allocates, but it also writes BPF maps, which fails for reasons of + * its own: -E2BIG when the target has more threads than pids_to_trace + * has room for, say. + */ + fprintf(trace->output, "Failed to set the pid filters: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; + +out_error_ev_qualifier: + /* + * Use the returned error, not errno. Reporting the failure on the way + * out of trace__set_ev_qualifier_filter() goes through the formatted + * output functions, which are free to leave errno describing + * something else by the time it is read here. + */ + fprintf(trace->output, "Failed to set the syscall filters: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; } out_error_mem: fprintf(trace->output, "Not enough memory to run!\n"); goto out_put_evlist; - -out_errno: - fprintf(trace->output, "%m\n"); - goto out_put_evlist; } static int trace__replay(struct trace *trace) @@ -5854,6 +5970,7 @@ int cmd_trace(int argc, const char **argv) .show_arg_names = true, .args_alignment = 70, .trace_syscalls = false, + .syscall_augment = true, .kernel_syscallchains = false, .max_stack = UINT_MAX, .max_events = ULONG_MAX, @@ -5909,6 +6026,8 @@ int cmd_trace(int argc, const char **argv) OPT_CALLBACK_DEFAULT('F', "pf", &trace.trace_pgfaults, "all|maj|min", "Trace pagefaults", parse_pagefaults, "maj"), OPT_BOOLEAN(0, "syscalls", &trace.trace_syscalls, "Trace syscalls"), + OPT_BOOLEAN(0, "syscall-augment", &trace.syscall_augment, + "Augment syscalls with BPF"), OPT_BOOLEAN('f', "force", &trace.force, "don't complain, do it"), OPT_CALLBACK(0, "call-graph", &trace.opts, "record_mode[,record_size]", record_callchain_help, @@ -6031,7 +6150,7 @@ int cmd_trace(int argc, const char **argv) "cgroup monitoring only available in system-wide mode"); } - if (!trace.trace_syscalls) + if (!trace.trace_syscalls || !trace.syscall_augment) goto skip_augmentation; if ((argc >= 1) && (strcmp(argv[0], "record") == 0)) { @@ -6053,11 +6172,34 @@ int cmd_trace(int argc, const char **argv) if (err < 0) goto skip_augmentation; - trace__add_syscall_newtp(&trace); + /* + * The syscall tracepoints are what the augmented events are described + * and paired with: the enter one supplies the format + * __augmented_syscalls__ is read with, and the exit one reports the + * returns, since BPF only takes over the enters. Without them there is + * nothing to augment, so drop the skeleton and carry on with plain + * tracepoints, as the bpf-output failure below does. + */ + if (trace__add_syscall_newtp(&trace)) { + pr_debug("Failed to set up the syscall tracepoints, disabling augmentation\n"); + augmented_syscalls__cleanup(); + goto skip_augmentation; + } err = augmented_syscalls__create_bpf_output(trace.evlist); - if (err == 0) + if (err == 0) { trace.syscalls.events.bpf_output = evlist__last(trace.evlist); + } else { + /* + * augmented_syscalls__prepare() already attached sys_enter and + * sys_exit, which are system wide. Falling through to + * skip_augmentation without undoing that would run a BPF + * program for every syscall on the machine, for the whole + * session, with nothing consuming the output. + */ + pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); + augmented_syscalls__cleanup(); + } skip_augmentation: err = -1; @@ -6113,7 +6255,9 @@ int cmd_trace(int argc, const char **argv) * syscall. */ if (trace.syscalls.events.bpf_output) { - evlist__for_each_entry(trace.evlist, evsel) { + struct evsel *n; + + evlist__for_each_entry_safe(trace.evlist, n, evsel) { bool raw_syscalls_sys_exit = evsel__name_is(evsel, "raw_syscalls:sys_exit"); if (raw_syscalls_sys_exit) { @@ -6121,28 +6265,44 @@ int cmd_trace(int argc, const char **argv) goto init_augmented_syscall_tp; } - if (trace.syscalls.events.bpf_output->priv == NULL && - strstr(evsel__name(evsel), "syscalls:sys_enter")) { + /* + * Match the evsel trace__add_syscall_newtp() made by + * identity rather than by name. It is called + * raw_syscalls:sys_enter, or syscalls:sys_enter on + * kernels too old to have the raw variant, and a + * substring test for the latter also matches the + * per syscall syscalls:sys_enter_SYSCALL tracepoints + * a user can ask for by name. Claiming one of those + * here would take the event the user asked for out of + * the evlist below and describe __augmented_syscalls__ + * with the wrong tracefs format. + */ + if (evsel == trace.syscalls.events.sys_enter) { struct evsel *augmented = trace.syscalls.events.bpf_output; if (evsel__init_augmented_syscall_tp(augmented, evsel) || evsel__init_augmented_syscall_tp_args(augmented)) goto out; /* - * Augmented is __augmented_syscalls__ BPF_OUTPUT event + * Augmented is __augmented_syscalls__ BPF_OUTPUT event. * Above we made sure we can get from the payload the tp fields * that we get from syscalls:sys_enter tracefs format file. + * Since BPF outputs all enter events (both augmented and + * unaugmented) into __augmented_syscalls__, we remove the raw + * sys_enter evsel from evlist so that perf trace only listens + * to __augmented_syscalls__, avoiding duplicate events and + * avoiding kernel tracepoint vetoes. + * + * Because evlist__remove() removes the first evsel (which had + * tracking=true by default), re-designate the tracking event + * so PERF_RECORD_COMM and fork tracking continue to be enabled. */ augmented->handler = trace__sys_enter; - /* - * Now we do the same for the *syscalls:sys_enter event so that - * if we handle it directly, i.e. if the BPF prog returns 0 so - * as not to filter it, then we'll handle it just like we would - * for the BPF_OUTPUT one: - */ - if (evsel__init_augmented_syscall_tp(evsel, evsel) || - evsel__init_augmented_syscall_tp_args(evsel)) - goto out; - evsel->handler = trace__sys_enter; + evlist__remove(trace.evlist, evsel); + evsel__put_and_free_priv(evsel); + trace.syscalls.events.sys_enter = NULL; + evlist__set_tracking_event(trace.evlist, + trace.syscalls.events.sys_exit ?: augmented); + continue; } if (strstarts(evsel__name(evsel), "syscalls:sys_exit_")) { diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index 18904bebf672..f16299c1d311 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -114,6 +114,41 @@ struct pids_filtered { __uint(max_entries, 64); } pids_filtered SEC(".maps"); +/* + * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when + * attached to a process with -p or tracing a specific command workload). + * + * has_pids_to_trace: Set to true if target PID filtering is active. + * When false, all processes are eligible for tracing. + */ +struct pids_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, pid_t); + __type(value, bool); + __uint(max_entries, 1024); +} pids_to_trace SEC(".maps"); + +bool has_pids_to_trace; + +/* + * Hash map storing syscall IDs for filtering (via 'perf trace -e ...'). + * + * has_syscalls_to_trace: Set to true if any syscall filter is active. + * not_syscalls_to_trace: Inverts matching when '!' prefix is used in -e + * (e.g., -e !open,close means trace everything EXCEPT + * open and close; an exclusion blacklist rather than + * an inclusion whitelist). + */ +struct syscalls_to_trace { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, int); + __type(value, bool); + __uint(max_entries, 1024); +} syscalls_to_trace SEC(".maps"); + +bool has_syscalls_to_trace; +bool not_syscalls_to_trace; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -154,8 +189,8 @@ static inline struct augmented_args_payload *augmented_args_payload(void) static inline int augmented__output(void *ctx, struct augmented_args_payload *args, int len) { - /* If perf_event_output fails, return non-zero so that it gets recorded unaugmented */ - return bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + bpf_perf_event_output(ctx, &__augmented_syscalls__, BPF_F_CURRENT_CPU, args, len); + return 1; } static inline int augmented__beauty_output(void *ctx, void *data, int len) @@ -191,10 +226,21 @@ unsigned int augmented_arg__read_str(struct augmented_arg *augmented_arg, const return augmented_len; } -SEC("tp/raw_syscalls/sys_enter") +/* + * Default sys_enter program for syscalls without pointer argument augmentation. + * Writes the raw struct syscall_enter_args payload into __augmented_syscalls__ + * and returns 1 so the tracepoint is never vetoed in the kernel. + */ +SEC("tp/syscalls/sys_enter_unaugmented") int syscall_unaugmented(struct syscall_enter_args *args) { - return 1; + struct augmented_args_payload *augmented_args = augmented_args_payload(); + + if (augmented_args == NULL) + return 1; + + bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + return augmented__output(args, augmented_args, sizeof(augmented_args->args)); } /* @@ -454,11 +500,41 @@ static pid_t getpid(void) return bpf_get_current_pid_tgid(); } +/* + * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { return bpf_map_lookup_elem(pids, &pid) != NULL; } +/* + * Checks if the current task (thread PID or process TGID) is targeted for tracing. + * Checks both PID (thread ID) and TGID (process ID) so that all threads of a + * target process match. + */ +static inline bool pid_to_trace__has(pid_t pid) +{ + pid_t tgid = bpf_get_current_pid_tgid() >> 32; + + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || + bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; +} + +/* + * Determines if a syscall should be traced based on the filter map: + * - When not_syscalls_to_trace is true: blacklist mode (trace if NOT in map). + * - When not_syscalls_to_trace is false: whitelist mode (trace ONLY if IN map). + */ +static inline bool syscall_to_trace__enabled(int id) +{ + bool in_map = bpf_map_lookup_elem(&syscalls_to_trace, &id) != NULL; + + if (not_syscalls_to_trace) + return !in_map; + return in_map; +} + u64 ZERO = 0; /* @@ -604,6 +680,11 @@ static int augment_sys_enter(void *ctx, struct syscall_enter_args *args) return augmented__beauty_output(ctx, payload, sizeof(struct syscall_enter_args) + output); } +/* + * Main raw_syscalls:sys_enter tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel for + * other concurrent listeners. Filtered events simply do not output to the ring buffer. + */ SEC("tp/raw_syscalls/sys_enter") int sys_enter(struct syscall_enter_args *args) { @@ -618,8 +699,11 @@ int sys_enter(struct syscall_enter_args *args) * initial, non-augmented raw_syscalls:sys_enter payload. */ + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; augmented_args = augmented_args_payload(); if (augmented_args == NULL) @@ -627,25 +711,41 @@ int sys_enter(struct syscall_enter_args *args) bpf_probe_read_kernel(&augmented_args->args, sizeof(augmented_args->args), args); + if (has_syscalls_to_trace && !syscall_to_trace__enabled(augmented_args->args.syscall_nr)) + return 1; + /* - * Jump to syscall specific augmenter, even if the default one, - * "!raw_syscalls:unaugmented" that will just return 1 to return the - * unaugmented tracepoint payload. + * Jump to syscall specific augmenter. If augmented, augment_sys_enter() + * outputs the payload to __augmented_syscalls__ and returns 0. + * Return 1 so we never veto the kernel tracepoint for other listeners. */ - if (augment_sys_enter(args, &augmented_args->args)) - bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + if (augment_sys_enter(args, &augmented_args->args) == 0) + return 1; - // If not found on the PROG_ARRAY syscalls map, then we're filtering it: - return 0; + bpf_tail_call(args, &syscalls_sys_enter, augmented_args->args.syscall_nr); + + /* + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. + */ + return 1; } +/* + * Main raw_syscalls:sys_exit tracepoint handler. + * Always returns 1 so the tracepoint is never vetoed in the kernel. + */ SEC("tp/raw_syscalls/sys_exit") int sys_exit(struct syscall_exit_args *args) { struct syscall_exit_args exit_args; + if (has_pids_to_trace && !pid_to_trace__has(getpid())) + return 1; + if (pid_filter__has(&pids_filtered, getpid())) - return 0; + return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); /* @@ -655,9 +755,12 @@ int sys_exit(struct syscall_exit_args *args) */ bpf_tail_call(args, &syscalls_sys_exit, exit_args.syscall_nr); /* - * If not found on the PROG_ARRAY syscalls map, then we're filtering it: + * If not found on the PROG_ARRAY syscalls map, return 1 so we + * don't veto the tracepoint event system-wide for other concurrent + * listeners. perf trace's own evsel filter will discard non-matching + * syscalls. */ - return 0; + return 1; } char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index ebb26225fb04..5f15b27264e9 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -1,5 +1,6 @@ #include <assert.h> #include <bpf/libbpf.h> +#include <errno.h> #include <internal/xyarray.h> #include <string.h> @@ -12,6 +13,23 @@ static struct augmented_raw_syscalls_bpf *skel; static struct evsel *bpf_output; +/* Set by attach_prog() so the first failure is what gets reported. */ +static int attach_err; + +static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const char *name) +{ + *link = bpf_program__attach(prog); + if (*link) + return 0; + /* + * Save errno before pr_debug(), which formats and writes output and so + * can overwrite it. + */ + attach_err = -errno; + pr_debug("Failed to attach %s BPF program\n", name); + return attach_err; +} + int augmented_syscalls__prepare(void) { struct bpf_program *prog; @@ -37,11 +55,35 @@ int augmented_syscalls__prepare(void) if (err < 0) { libbpf_strerror(err, buf, sizeof(buf)); pr_debug("Failed to load augmented syscalls BPF skeleton: %s\n", buf); + /* + * Tear the skeleton down rather than leaving a half initialized + * one behind. The caller falls back to unaugmented tracing and + * still calls the setters below, which must then do nothing + * instead of failing against a skeleton with no maps. + */ + augmented_syscalls__cleanup(); return err; } - augmented_raw_syscalls_bpf__attach(skel); + /* + * Only sys_enter and sys_exit are attached, the remaining programs are + * reached by tail calls. Attach them explicitly and, on failure, undo + * any partial attachment: leaving sys_enter live on + * raw_syscalls:sys_enter would keep running a BPF program for every + * syscall on the system for a perf trace session that never starts. + */ + if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) + goto out_cleanup; + if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) + goto out_cleanup; + return 0; + +out_cleanup: + err = attach_err; + /* Destroys every link attached above along with the skeleton. */ + augmented_syscalls__cleanup(); + return err; } int augmented_syscalls__create_bpf_output(struct evlist *evlist) @@ -82,22 +124,136 @@ void augmented_syscalls__setup_bpf_output(void) } } +/* + * Add every pid to a pid keyed filter map. + * + * A map with no room left is not treated as a failure. bpf_map__update_elem() + * answers -E2BIG once max_entries keys are present, and a task that did not + * fit is then one the filter does not know about, which is exactly the + * position it would be in had it been created after the map filled up. + * Refusing to trace at all instead would make perf trace unusable against the + * very large targets that are the only way to reach the limit, so say how many + * missed out and carry on with those that did fit. + */ +static int add_pids_to_map(struct bpf_map *map, const char *missing_out_on, + unsigned int nr, pid_t *pids) +{ + unsigned int nr_no_room = 0; + bool value = true; + + for (unsigned int i = 0; i < nr; ++i) { + int err = bpf_map__update_elem(map, &pids[i], sizeof(*pids), + &value, sizeof(value), BPF_ANY); + + if (err == -E2BIG) { + nr_no_room++; + continue; + } + if (err) + return err; + } + + if (nr_no_room) { + pr_warning("Only %u of %u tasks fit in the %s BPF map, %u will not be %s.\n", + nr - nr_no_room, nr, bpf_map__name(map), nr_no_room, + missing_out_on); + } + + return 0; +} + int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) +{ + if (skel == NULL) + return 0; + + return add_pids_to_map(skel->maps.pids_filtered, "filtered out", nr, pids); +} + +/* + * Populate target PIDs in the BPF pids_to_trace map (e.g., for -p <PID> or + * when tracing a specified command workload). + */ +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) +{ + int err; + + if (skel == NULL || nr == 0) + return 0; + + err = add_pids_to_map(skel->maps.pids_to_trace, "traced", nr, pids); + if (err) + return err; + + /* + * Set the flag only once every target is in the map. The BPF programs + * are attached by this point, so flipping it first would have them + * filter against a partially populated map and drop syscalls made by + * the targets that had not been added yet. + */ + skel->bss->has_pids_to_trace = true; + return 0; +} + +int augmented_syscalls__add_target_pid(pid_t pid) +{ + bool value = true; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), BPF_ANY); +} + +int augmented_syscalls__del_target_pid(pid_t pid) +{ + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return 0; + + return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); +} + +bool augmented_syscalls__has_target_pid(pid_t pid) +{ + bool value; + + if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) + return false; + + return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), + &value, sizeof(value), 0) == 0; +} + +/* + * Populate syscalls in the BPF syscalls_to_trace map: + * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace + * all syscalls EXCEPT these). + * false if whitelist mode (trace ONLY these syscalls). + */ +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls) { bool value = true; int err = 0; - if (skel == NULL) + if (skel == NULL || nr == 0) return 0; + skel->bss->not_syscalls_to_trace = not_syscalls; for (size_t i = 0; i < nr; ++i) { - err = bpf_map__update_elem(skel->maps.pids_filtered, &pids[i], - sizeof(*pids), &value, sizeof(value), + err = bpf_map__update_elem(skel->maps.syscalls_to_trace, &syscall_ids[i], + sizeof(int), &value, sizeof(value), BPF_ANY); if (err) - break; + return err; } - return err; + /* + * As for the pid maps, publish the filter only once it is complete: + * in whitelist mode a half filled map would drop syscalls that were + * asked for but not added yet. + */ + skel->bss->has_syscalls_to_trace = true; + return 0; } int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd) @@ -142,4 +298,5 @@ struct bpf_program *augmented_syscalls__find_by_title(const char *name) void augmented_syscalls__cleanup(void) { augmented_raw_syscalls_bpf__destroy(skel); + skel = NULL; } diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index a1cd9a5e0213..5702eda3b469 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -2,6 +2,7 @@ #define TRACE_AUGMENT_H #include <linux/compiler.h> +#include <stdbool.h> #include <sys/types.h> struct bpf_program; @@ -13,6 +14,11 @@ int augmented_syscalls__prepare(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__add_target_pid(pid_t pid); +int augmented_syscalls__del_target_pid(pid_t pid); +bool augmented_syscalls__has_target_pid(pid_t pid); +int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); struct bpf_program *augmented_syscalls__unaugmented(void); @@ -40,6 +46,34 @@ static inline int augmented_syscalls__set_filter_pids(unsigned int nr __maybe_un return 0; } +static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_unused, + pid_t *pids __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) +{ + return 0; +} + +static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) +{ + return false; +} + +static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, + int *syscall_ids __maybe_unused, + bool not_syscalls __maybe_unused) +{ + return 0; +} + static inline int augmented_syscalls__get_map_fds(int *enter_fd __maybe_unused, int *exit_fd __maybe_unused, int *beauty_fd __maybe_unused) -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 13/23] perf trace: Handle fork and exit directly in BPF filter maps 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (11 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 12/23] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 14/23] perf trace: Enumerate the target again once BPF is attached Ian Rogers ` (9 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz Updating target or filtered PIDs in userspace upon processing PERF_RECORD_FORK and PERF_RECORD_EXIT events introduces latency between event occurrence and userspace BPF map updates. If a newly forked child executes system calls before userspace processes PERF_RECORD_FORK, those syscalls may be dropped by BPF PID filtering. Conversely, if userspace evicts PIDs asynchronously on PERF_RECORD_EXIT, the kernel may recycle a PID before userspace processes the exit event, causing the late eviction to silently drop a newly created task that received the recycled PID. Address this by attaching BTF-typed raw tracepoint BPF programs directly to the scheduler task lifetime tracepoints: 1. Attach SEC("tp_btf/sched_process_fork") (sched_process_fork), which runs in copy_process() in the parent's context before wake_up_new_task() wakes the child. Using tp_btf rather than SEC("tp/sched/sched_process_fork") receives the stable TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) rather than the tracepoint ring-buffer record (TP_STRUCT__entry), whose layout changed in Linux 6.16 when parent_comm and child_comm were converted from 16-byte arrays to 4-byte __data_loc strings. When inherit is enabled and the pid of the thread that called clone() is in pids_to_trace or pids_filtered, insert child->pid into the corresponding map immediately. Because child->pid is task_struct.pid (the global initial-namespace PID), this works accurately across PID namespaces without aliasing host PIDs, and covers both new processes and CLONE_THREAD threads without needing real_parent CO-RE walks or syscall-return heuristics. 2. Attach SEC("tp_btf/sched_process_exit") (sched_process_exit), which runs in do_exit() for every task in its own context, including tasks killed by signals (SIGKILL, SIGSEGV, etc.) and secondary threads torn down implicitly by exit_group. Delete the dying task's PID from pids_to_trace and pids_filtered immediately in kernel space, eliminating both map leaks and any asynchronous userspace eviction window where PID recycling could occur. 3. Attach SEC("tp_btf/sched_process_exec") (sched_process_exec) to follow the one case where a live task's pid changes underneath the maps. When a thread that is not the group leader execs, de_thread() kills the leader and hands the leader's pid, which is the tgid, to the exec'ing thread. The leader dies first, so sched_process_exit() has already dropped exactly the pid the survivor now holds, and the survivor's old entry would be stranded in the map for good. Move the entry from old_pid to p->pid. old_pid is sampled in bprm_execve() before de_thread() runs, so the ordinary group leader exec is a no-op here. Drop the old key before inserting the new one: the move is only a rename, but holding both keys at once needs a spare slot, and on a full map the insert would fail with -E2BIG while the delete still succeeded, losing the task instead of moving it. old_pid can also have been handed to somebody else before the move runs: de_thread() frees it as it releases the leader, which is inside the binfmt loader, while the tracepoint only fires once the whole of exec_binprm() is done. A task created in that gap that is given the pid, and that is itself traced or filtered, is inserted under the same key and has its entry carried across instead. Nothing here can separate the two, an entry saying only that some task with that pid was of interest, and the reuse needs the cyclic pid allocator to come the whole way round inside an exec, with clone3()'s set_tid gated on CAP_CHECKPOINT_RESTORE rather than free for the taking. Both tasks are ones the session was already following, so the cost is which of them is reported, not tracing reaching past what was asked for. 4. With every live task registered before its first syscall and evicted in do_exit(), simplify pid_to_trace__has() and pid_filter__has() to single BPF hash map lookups, and move bpf_probe_read_kernel() in sys_exit back after the PID filter checks. 5. Pass the inherit flag from userspace to BPF .rodata via augmented_syscalls__prepare(!trace.opts.no_inherit), and split attaching out of it into augmented_syscalls__attach(), called from trace__run() once the pid, syscall and program array maps have all been programmed. These are system wide programs, so from the instant they attach they alone decide what is traced: attaching at load time, as before, left a window in which a target could fork without sched_process_fork() knowing the parent was a target, and with the userspace fork handling gone there was nothing left to recover it. The scheduler programs are attached ahead of sys_enter and sys_exit for the same reason. Set has_pids_filtered only after populating pids_filtered. A failure to attach ends the session, reporting the error that caused it. There is no falling back to unaugmented tracing by that point: cmd_trace() built the evlist around __augmented_syscalls__ and dropped the sys_enter evsel, so a session that carried on would report nothing at all. Neither did the code this replaces, which ignored the result of augmented_raw_syscalls_bpf__attach() altogether and ran on with programs that had never been attached. 6. Remove the userspace BPF map updates from PERF_RECORD_FORK and PERF_RECORD_EXIT in trace__process_event(), and delete the now-unused augmented_syscalls__{add,del,has}_target_pid() helpers. No coverage is lost with them: those records only come into being once the ring buffers are mapped by evlist__do_mmap() and the events are switched on by evlist__enable(), both of which run after augmented_syscalls__attach() in trace__run(), and they are then acted on later still, whenever the poll loop gets round to them. The scheduler programs therefore go live strictly earlier than the userspace path could ever have reacted. 7. Gate pid_filter__has() on a has_pids_filtered flag in .bss so the common case without --filter-pids performs no map lookups, and size pids_to_trace and pids_filtered at 16384 entries. pids_filtered is grown from 64 because it is no longer just the handful of pids userspace names: sched_process_fork() adds every descendant of those, so a --filter-pids target that forks or is heavily threaded needs the same headroom as a traced one. A fork or exit is still not seen if it happens before the programs are attached, that is between evlist__create_maps() scanning /proc for a -p target and augmented_syscalls__attach(). Such a window is inherent in programming a system wide filter before switching it on, and as above the userspace handling did not cover it either. The cost is not small though. Once the bpf-output event exists cmd_trace() removes the sys_enter evsel from the evlist, so __augmented_syscalls__ is the only source of enter events and a pid that is missing from pids_to_trace is not reported at all. sys_enter returning 1 keeps the kernel tracepoint alive for other subscribers, it does not give perf trace a second path to the event. A target that forks during perf trace's own startup can therefore have that child, and in turn everything the child forks, go untraced for the whole run. The tgid fallback that pid_to_trace__has() used to have would have masked part of this, since a thread missed in the window still shares the tgid of a target userspace did insert. It is not kept because it would also re-admit tasks that sched_process_fork() deliberately skipped: under --no-inherit a new thread of the target is not added to the map, yet it shares the target's tgid and a tgid test cannot tell it apart from one that was. sched_process_fork() does not consult the parent's tgid either, for the same reason. Every thread of a -p target is enumerated from /proc/<pid>/task and inserted under its own pid, and -t names a single thread, so a tgid test adds no reach. What it would add is a child inheriting from a thread that is not traced itself: a sibling of the thread -t selected, or one missed in the attach window whose own syscalls go unreported. Inheritance keys off the pid of the thread that called clone(), exactly as the lookups do. Two of the three ways of selecting what to trace are unaffected: 'perf trace -- cmd' cannot hit this because evlist__prepare_workload() leaves the child blocked on a pipe until evlist__start_workload(), well after the attach, and 'perf trace -a' never sets has_pids_to_trace so it does not filter at all. It is 'perf trace -p' against an already running target that is exposed. Closing that too needs the descendants of the target re-enumerated from /proc after the attach, which the next change does. Descendants that sched_process_fork() adds to pids_filtered can still have a sys_exit reported without the matching enter. perf trace keeps the raw_syscalls:sys_exit evsel in the evlist, and the pids named by --filter-pids are kept out of it by that evsel's own tracepoint filter, which is set once and never learns of the descendants. sys_exit returning 1 rather than vetoing is what makes this visible, but vetoing is precisely what this series is removing, and a tracepoint program has no other way to suppress an event. It takes --filter-pids naming a process that then forks; the self-exclusion path, where perf trace filters its own pid, only runs without a target and spawns nothing. Routing sys_exit through __augmented_syscalls__ the way sys_enter now is would close it. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 55 ++-- .../bpf_skel/augmented_raw_syscalls.bpf.c | 238 +++++++++++++++++- tools/perf/util/bpf_trace_augment.c | 120 +++++---- tools/perf/util/trace_augment.h | 28 +-- 4 files changed, 343 insertions(+), 98 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index f7226331c311..ddbe1c8ec1dd 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -2072,23 +2072,6 @@ static int trace__process_event(struct trace *trace, struct machine *machine, "LOST %" PRIu64 " events!\n", (u64)event->lost.lost); ret = machine__process_lost_event(machine, event, sample); break; - case PERF_RECORD_FORK: - if (trace->raw_augmented_syscalls && - (augmented_syscalls__has_target_pid(event->fork.ppid) || - augmented_syscalls__has_target_pid(event->fork.ptid))) { - augmented_syscalls__add_target_pid(event->fork.pid); - } - ret = machine__process_fork_event(machine, event, sample); - break; - case PERF_RECORD_EXIT: - if (trace->raw_augmented_syscalls) { - if (event->fork.pid == event->fork.tid) - augmented_syscalls__del_target_pid(event->fork.pid); - else - augmented_syscalls__del_target_pid(event->fork.tid); - } - ret = machine__process_exit_event(machine, event, sample); - break; default: ret = machine__process_event(machine, event, sample); break; @@ -5058,6 +5041,22 @@ static int trace__run(struct trace *trace, int argc, const char **argv) } } + /* + * Everything the BPF programs filter on is now in their maps, so it is + * safe to let them run. They are attached system wide, so anything + * before this point would have been filtered against a map that was + * still being built up. + * + * Falling back to unaugmented tracing is no longer possible here: the + * evlist was built around __augmented_syscalls__ back in cmd_trace(), + * which is where that decision is taken and where the sys_enter evsel + * was dropped. Fail the session rather than run one that can report + * nothing. + */ + err = augmented_syscalls__attach(); + if (err < 0) + goto out_error_attach; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the @@ -5252,6 +5251,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) fprintf(trace->output, "Failed to set the syscall filters: %s\n", str_error_r(-err, errbuf, sizeof(errbuf))); goto out_put_evlist; + +out_error_attach: + /* + * Use the returned error rather than errno: the failing attach is + * unwound before returning, and the libbpf calls that does can leave + * errno describing something else entirely. + */ + fprintf(trace->output, "Failed to attach the augmented syscalls BPF programs: %s\n", + str_error_r(-err, errbuf, sizeof(errbuf))); + goto out_put_evlist; } out_error_mem: fprintf(trace->output, "Not enough memory to run!\n"); @@ -6168,7 +6177,7 @@ int cmd_trace(int argc, const char **argv) goto skip_augmentation; } - err = augmented_syscalls__prepare(); + err = augmented_syscalls__prepare(!trace.opts.no_inherit); if (err < 0) goto skip_augmentation; @@ -6191,11 +6200,11 @@ int cmd_trace(int argc, const char **argv) trace.syscalls.events.bpf_output = evlist__last(trace.evlist); } else { /* - * augmented_syscalls__prepare() already attached sys_enter and - * sys_exit, which are system wide. Falling through to - * skip_augmentation without undoing that would run a BPF - * program for every syscall on the machine, for the whole - * session, with nothing consuming the output. + * Drop the loaded skeleton before falling back to unaugmented + * tracing. Otherwise the setters called from trace__run() would + * still program its maps, and augmented_syscalls__attach() would + * then put system wide BPF programs on raw_syscalls for a + * session with nothing consuming their output. */ pr_debug("Failed to create the augmented syscalls bpf-output event, disabling augmentation\n"); augmented_syscalls__cleanup(); diff --git a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c index f16299c1d311..d400d3d423bc 100644 --- a/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c +++ b/tools/perf/util/bpf_skel/augmented_raw_syscalls.bpf.c @@ -9,6 +9,7 @@ #include "vmlinux.h" #include <bpf/bpf_helpers.h> +#include <bpf/bpf_tracing.h> #include <linux/limits.h> #define PERF_ALIGN(x, a) __PERF_ALIGN_MASK(x, (typeof(x))(a)-1) @@ -107,25 +108,48 @@ struct augmented_arg { }; }; +/* + * Hash map of PIDs/TGIDs whose events must be discarded, e.g. perf trace's own + * pid, so that tracing doesn't feed back on itself. + * + * has_pids_filtered: set to true only when the map is populated. Checking a + * boolean is much cheaper than a map lookup, and sys_enter + * runs for every syscall on the system, so the common + * "no pids filtered" case must stay on a fast path. + * + * max_entries matches pids_to_trace: userspace only ever names a handful of + * pids here, but sched_process_fork() below adds every descendant of those, + * so a --filter-pids target that forks or is heavily threaded needs the same + * headroom as a traced one. + */ struct pids_filtered { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 64); + __uint(max_entries, 16384); } pids_filtered SEC(".maps"); +bool has_pids_filtered; + /* * Optional hash map containing specific PIDs/TGIDs to trace (e.g., when * attached to a process with -p or tracing a specific command workload). * * has_pids_to_trace: Set to true if target PID filtering is active. * When false, all processes are eligible for tracing. + * + * max_entries bounds how many tasks can be tracked at once. sched_process_exit + * below evicts a task as it dies, whatever it died of, so the map holds live + * tasks rather than growing without bound. It is sized well + * above the thread count of realistic traced workloads; should a workload + * still exceed it, bpf_map_update_elem() fails with -E2BIG and the extra + * tasks are simply not traced rather than anything being corrupted. */ struct pids_to_trace { __uint(type, BPF_MAP_TYPE_HASH); __type(key, pid_t); __type(value, bool); - __uint(max_entries, 1024); + __uint(max_entries, 16384); } pids_to_trace SEC(".maps"); bool has_pids_to_trace; @@ -149,6 +173,9 @@ struct syscalls_to_trace { bool has_syscalls_to_trace; bool not_syscalls_to_trace; +/* Inherit tracing for child tasks (set to false if --no-inherit is specified) */ +const volatile bool inherit = true; + struct augmented_args_payload { struct syscall_enter_args args; struct augmented_arg arg, arg2; // We have to reserve space for two arguments (rename, etc) @@ -501,24 +528,35 @@ static pid_t getpid(void) } /* - * Returns true if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * Checks if a PID is explicitly excluded/filtered out (e.g., via --filter-pids). + * + * Children of a filtered task are added to the map by sched_process_fork() + * below, so a plain lookup is all that is needed here. */ static bool pid_filter__has(struct pids_filtered *pids, pid_t pid) { + /* + * Fast path: this runs for every syscall on the system, so when no pid + * is filtered do no work at all rather than failing a lookup. + */ + if (!has_pids_filtered) + return false; + return bpf_map_lookup_elem(pids, &pid) != NULL; } /* - * Checks if the current task (thread PID or process TGID) is targeted for tracing. - * Checks both PID (thread ID) and TGID (process ID) so that all threads of a - * target process match. + * Checks if the current task is targeted for tracing. + * + * Every thread that existed when tracing started was named by the target and + * inserted from userspace, and every task created since was inserted by + * sched_process_fork() below, before it was able to run. So there is nothing + * to derive here, and in particular no need to consult the tgid or walk to the + * parent: a task is traced if and only if it is in the map. */ static inline bool pid_to_trace__has(pid_t pid) { - pid_t tgid = bpf_get_current_pid_tgid() >> 32; - - return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL || - bpf_map_lookup_elem(&pids_to_trace, &tgid) != NULL; + return bpf_map_lookup_elem(&pids_to_trace, &pid) != NULL; } /* @@ -744,10 +782,23 @@ int sys_exit(struct syscall_exit_args *args) if (has_pids_to_trace && !pid_to_trace__has(getpid())) return 1; + /* + * Returning 1 leaves the record in place rather than vetoing the + * tracepoint for every other listener. The pids named by + * --filter-pids are also excluded by the raw_syscalls:sys_exit + * evsel's own tracepoint filter, so they are dropped there. + * Descendants added to pids_filtered by sched_process_fork() below + * are not in that static filter, so their sys_exit is still + * delivered and printed without a matching enter. A tracepoint + * program can only suppress an event by vetoing it, which is what + * this series removes; routing sys_exit through + * __augmented_syscalls__ the way sys_enter now is would close this. + */ if (pid_filter__has(&pids_filtered, getpid())) return 1; bpf_probe_read_kernel(&exit_args, sizeof(exit_args), args); + /* * Jump to syscall specific return augmenter, even if the default one, * "!raw_syscalls:unaugmented" that will just return 1 to return the @@ -763,4 +814,171 @@ int sys_exit(struct syscall_exit_args *args) return 1; } +/* + * Propagate tracing to a newly created task. + * + * tp_btf/sched_process_fork is raised by copy_process(), in the parent's + * context and before the child is woken, so the child is in the maps before it + * can issue its first syscall. That removes the need to inspect real_parent + * when a syscall is seen from an unknown task, which could neither tell a + * genuine descendant from a task merely reparented to a traced init, nor keep + * following a descendant whose parent had already exited. + * + * Using tp_btf rather than tp/sched/sched_process_fork avoids depending on the + * tracepoint ring-buffer record layout (TP_STRUCT__entry), which changed in + * Linux 6.16 when parent_comm and child_comm were converted from fixed 16-byte + * arrays to 4-byte __data_loc strings (shrinking the tracepoint context from + * 48 to 24 bytes and causing BPF_PROG_TYPE_TRACEPOINT attachment to fail with + * -EACCES when accessing higher offsets). Instead, tp_btf receives the stable + * TP_PROTO arguments (struct task_struct *parent, struct task_struct *child) + * directly. + * + * child->pid is task_struct.pid, i.e. the pid in the initial namespace, which + * is what the maps are keyed by. A clone() return value, in contrast, is the + * pid in the caller's namespace and would alias an unrelated host task when a + * containerised workload is traced. + * + * CLONE_THREAD needs no special handling: a new thread arrives here like any + * other task and is inserted under its own pid. + */ +SEC("tp_btf/sched_process_fork") +int BPF_PROG(sched_process_fork, struct task_struct *parent, struct task_struct *child) +{ + pid_t parent_pid, child_pid; + bool val = true; + + if (!inherit) + return 0; + + /* + * Inherit from the thread that called clone() and from nothing else. + * The maps name individual tasks: pid_to_trace__has() and + * pid_filter__has() look up a task's own pid and nothing more, and + * every thread of a -p target is enumerated from /proc/<pid>/task and + * inserted in its own right, so a traced thread is always here under + * its own key. Consulting the parent's tgid as well would let a child + * inherit from a thread that is not itself traced, which is precisely + * what 'perf trace -t <tid>' asked to leave out, and would re-admit + * descendants of a thread that sched_process_fork() skipped or that + * was missed while the programs were being attached, while still not + * tracing that thread itself. + */ + parent_pid = parent->pid; + child_pid = child->pid; + + if (has_pids_to_trace && + bpf_map_lookup_elem(&pids_to_trace, &parent_pid) != NULL) + bpf_map_update_elem(&pids_to_trace, &child_pid, &val, BPF_ANY); + + if (has_pids_filtered && + bpf_map_lookup_elem(&pids_filtered, &parent_pid) != NULL) + bpf_map_update_elem(&pids_filtered, &child_pid, &val, BPF_ANY); + + return 0; +} + +/* + * Drop a dying task from the maps. + * + * tp_btf/sched_process_exit is raised by do_exit() for every task, in its own + * context, so unlike hooking the exit and exit_group syscalls this also covers + * tasks killed by a signal and threads torn down implicitly by exit_group. + * + * Doing it here rather than from the userspace PERF_RECORD_EXIT handler also + * means there is no window between the task dying and the map being updated, + * during which the kernel could recycle the pid and the late eviction silently + * stop tracing whichever new task received it. + * + * Each thread is reported separately, including the group leader, whose pid is + * the thread group's tgid, so one delete per map covers both uses of the key. + */ +SEC("tp_btf/sched_process_exit") +int BPF_PROG(sched_process_exit, struct task_struct *p) +{ + pid_t pid = p->pid; + + bpf_map_delete_elem(&pids_to_trace, &pid); + bpf_map_delete_elem(&pids_filtered, &pid); + + return 0; +} + +/* + * Follow a task whose pid changed under it. + * + * When a thread that is not the thread group leader execs, de_thread() kills + * the rest of the group and then hands the leader's pid, which is the tgid, to + * the exec'ing thread. The leader dies first, so sched_process_exit() above + * has already dropped that pid from the maps, and the survivor is now keyed by + * a pid nothing knows about while its original entry is left behind for good. + * + * Move the entry across so the task stays tracked and nothing is leaked. + * old_pid is sampled in bprm_execve() before de_thread() runs, so for the + * common case of the group leader exec'ing it simply equals p->pid and there + * is nothing to do. + */ +SEC("tp_btf/sched_process_exec") +int BPF_PROG(sched_process_exec, struct task_struct *p, pid_t old_pid) +{ + pid_t pid = p->pid; + bool val = true; + + if (pid == old_pid) + return 0; + + /* + * Drop the old key before adding the new one. The maps are bounded and + * the move is only ever a rename, but inserting first needs a spare + * slot for as long as both keys are present: on a full map that insert + * fails with -E2BIG while the delete still succeeds, which would lose + * the task rather than move it. Deleting first frees the slot the + * insert goes on to use. The task is mid exec and issues no syscalls in + * between, so the gap is not observable. + * + * There is no atomic rename for a hash map, so on a map that is exactly + * full this narrows the window rather than closing it: a fork on + * another CPU can still take the freed slot before the insert below + * runs, and the task is then dropped just as any other task is once the + * map is full. + * + * Nothing else takes old_pid out of the maps. sched_process_exit() ran + * for the group leader under the leader's own pid, before de_thread() + * swapped the two, so skipping the delete would leave old_pid behind + * as an entry for a task that no longer holds it. + * + * old_pid may already belong to somebody else by the time this runs. A + * non leader thread getting here has been through de_thread(), which + * hands it the leader's pid with exchange_tids() and then + * release_task()s the leader, freeing the pid the thread started with. + * That is inside the binfmt loader, whereas the tracepoint only fires + * once the whole of exec_binprm() is done, so the pid is up for reuse + * for the length of an exec. Should a task created in that window be + * given it, and be one that is traced or filtered itself, it is + * inserted under this same key and the move below carries its entry + * across rather than this task's: it stops being reported and this task + * takes its place. + * + * That is left as it is. An entry records only that some task with that + * pid was of interest, and the tracepoint offers no other handle on the + * task old_pid was taken from, so the two cannot be told apart here. + * Reuse needs the cyclic pid allocator to come the whole way round to + * old_pid inside that window, and a task cannot simply ask for the pid + * it wants: clone3()'s set_tid is gated on CAP_CHECKPOINT_RESTORE. + * Whichever task wins, both are ones the session was already following, + * so what is at stake is which of them gets reported rather than + * whether tracing reaches beyond what was asked for. + */ + if (bpf_map_lookup_elem(&pids_to_trace, &old_pid) != NULL) { + bpf_map_delete_elem(&pids_to_trace, &old_pid); + bpf_map_update_elem(&pids_to_trace, &pid, &val, BPF_ANY); + } + + if (bpf_map_lookup_elem(&pids_filtered, &old_pid) != NULL) { + bpf_map_delete_elem(&pids_filtered, &old_pid); + bpf_map_update_elem(&pids_filtered, &pid, &val, BPF_ANY); + } + + return 0; +} + char _license[] SEC("license") = "GPL"; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index 5f15b27264e9..44f30dba5469 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -30,7 +30,7 @@ static int attach_prog(struct bpf_link **link, struct bpf_program *prog, const c return attach_err; } -int augmented_syscalls__prepare(void) +int augmented_syscalls__prepare(bool inherit) { struct bpf_program *prog; char buf[128]; @@ -42,12 +42,18 @@ int augmented_syscalls__prepare(void) return -errno; } + skel->rodata->inherit = inherit; + /* - * Disable attaching the BPF programs except for sys_enter and - * sys_exit that tail call into this as necessary. + * Disable attaching the BPF programs other than those attached + * explicitly by augmented_syscalls__attach(), the rest are reached by + * tail calls. */ bpf_object__for_each_program(prog, skel->obj) { - if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit) + if (prog != skel->progs.sys_enter && prog != skel->progs.sys_exit && + prog != skel->progs.sched_process_fork && + prog != skel->progs.sched_process_exit && + prog != skel->progs.sched_process_exec) bpf_program__set_autoattach(prog, /*autoattach=*/false); } @@ -65,13 +71,42 @@ int augmented_syscalls__prepare(void) return err; } + return 0; +} + +int augmented_syscalls__attach(void) +{ + int err; + + if (skel == NULL) + return 0; + /* - * Only sys_enter and sys_exit are attached, the remaining programs are - * reached by tail calls. Attach them explicitly and, on failure, undo - * any partial attachment: leaving sys_enter live on - * raw_syscalls:sys_enter would keep running a BPF program for every - * syscall on the system for a perf trace session that never starts. + * Attaching is deliberately separate from, and a lot later than, + * loading: these are system wide tracepoint programs, so from the + * moment they are attached they are the only thing deciding which + * tasks and syscalls are traced. Going live before the pid and + * syscall maps are populated would mean a target that forked in the + * meantime was never picked up by sched_process_fork() below. + * + * Attach explicitly, so that a failure part way through can undo what + * came before it: leaving sys_enter live on raw_syscalls:sys_enter + * would keep running a BPF program for every syscall on the system for + * a perf trace session that never starts. + * + * The scheduler programs maintain the pid maps, and are attached first + * so that no fork, exit or exec can be missed between sys_enter going + * live and the maps being maintained. */ + if (attach_prog(&skel->links.sched_process_fork, skel->progs.sched_process_fork, + "sched_process_fork")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exit, skel->progs.sched_process_exit, + "sched_process_exit")) + goto out_cleanup; + if (attach_prog(&skel->links.sched_process_exec, skel->progs.sched_process_exec, + "sched_process_exec")) + goto out_cleanup; if (attach_prog(&skel->links.sys_enter, skel->progs.sys_enter, "sys_enter")) goto out_cleanup; if (attach_prog(&skel->links.sys_exit, skel->progs.sys_exit, "sys_exit")) @@ -83,6 +118,13 @@ int augmented_syscalls__prepare(void) err = attach_err; /* Destroys every link attached above along with the skeleton. */ augmented_syscalls__cleanup(); + /* + * Tearing the skeleton down closes file descriptors and frees memory, + * either of which may overwrite errno. Restore it so that a caller + * reporting this with "%m" describes the attach failure rather than + * whatever the teardown happened to do last. + */ + errno = -err; return err; } @@ -164,10 +206,28 @@ static int add_pids_to_map(struct bpf_map *map, const char *missing_out_on, int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids) { - if (skel == NULL) + int err; + + if (skel == NULL || nr == 0) return 0; - return add_pids_to_map(skel->maps.pids_filtered, "filtered out", nr, pids); + err = add_pids_to_map(skel->maps.pids_filtered, "filtered out", nr, pids); + if (err) + return err; + + /* + * Publish the filter only now that the map is populated. + * augmented_syscalls__attach() has not run yet, so nothing is reading + * either of them, but keeping the flag and the map consistent means + * the ordering stays correct however the callers are rearranged. + * + * The flag also tells the BPF program that the pids_filtered map is in + * use. Without it the program would have to look up every task in an + * empty map, on every syscall on the system, to find out that nothing + * is filtered. + */ + skel->bss->has_pids_filtered = true; + return 0; } /* @@ -186,45 +246,15 @@ int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) return err; /* - * Set the flag only once every target is in the map. The BPF programs - * are attached by this point, so flipping it first would have them - * filter against a partially populated map and drop syscalls made by - * the targets that had not been added yet. + * Set the flag only once every target is in the map, so that the two + * are never inconsistent. Publishing it first would, once the + * programs are attached, have them filter against a partially + * populated map and drop syscalls made by targets not yet added. */ skel->bss->has_pids_to_trace = true; return 0; } -int augmented_syscalls__add_target_pid(pid_t pid) -{ - bool value = true; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__update_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), BPF_ANY); -} - -int augmented_syscalls__del_target_pid(pid_t pid) -{ - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return 0; - - return bpf_map__delete_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), 0); -} - -bool augmented_syscalls__has_target_pid(pid_t pid) -{ - bool value; - - if (skel == NULL || !skel->bss->has_pids_to_trace || skel->maps.pids_to_trace == NULL) - return false; - - return bpf_map__lookup_elem(skel->maps.pids_to_trace, &pid, sizeof(pid), - &value, sizeof(value), 0) == 0; -} - /* * Populate syscalls in the BPF syscalls_to_trace map: * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index 5702eda3b469..ad992f5fa726 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -10,14 +10,12 @@ struct evlist; #ifdef HAVE_BPF_SKEL -int augmented_syscalls__prepare(void); +int augmented_syscalls__prepare(bool inherit); +int augmented_syscalls__attach(void); int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); -int augmented_syscalls__add_target_pid(pid_t pid); -int augmented_syscalls__del_target_pid(pid_t pid); -bool augmented_syscalls__has_target_pid(pid_t pid); int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); @@ -26,11 +24,16 @@ void augmented_syscalls__cleanup(void); #else /* !HAVE_BPF_SKEL */ -static inline int augmented_syscalls__prepare(void) +static inline int augmented_syscalls__prepare(bool inherit __maybe_unused) { return -1; } +static inline int augmented_syscalls__attach(void) +{ + return 0; +} + static inline int augmented_syscalls__create_bpf_output(struct evlist *evlist __maybe_unused) { return -1; @@ -52,21 +55,6 @@ static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_un return 0; } -static inline int augmented_syscalls__add_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline int augmented_syscalls__del_target_pid(pid_t pid __maybe_unused) -{ - return 0; -} - -static inline bool augmented_syscalls__has_target_pid(pid_t pid __maybe_unused) -{ - return false; -} - static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, int *syscall_ids __maybe_unused, bool not_syscalls __maybe_unused) -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 14/23] perf trace: Enumerate the target again once BPF is attached 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (12 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 13/23] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 15/23] perf trace: Drop targets that died before they were filtered Ian Rogers ` (8 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz evlist__create_maps() reads the target out of /proc, and the BPF sched_process_fork() program only sees what is cloned once it is attached. A task the target creates between the two is in neither, and since cmd_trace() drops the sys_enter evsel in favour of __augmented_syscalls__ there is no other source of enter events. Such a task, and in turn everything it forks, goes unreported for the rest of the session. Read the target out of /proc once more, after the attach. Whatever existed before the programs went live is there to be found, and whatever is created after it is sched_process_fork()'s to add, so between them nothing is left out. Doing this before the attach instead would only move the window rather than close it. The enumeration follows the same rule as the BPF programs, which is that the maps name individual tasks: - -p names a process, so its thread group is read from /proc/<pid>/task. - -t names a thread, which is taken on its own. Expanding it to its thread group would trace the siblings it asked to leave out. - Descendants come from task->children, read through /proc/<pid>/task/<tid>/children. A forked task leads a thread group of its own, so each one found is walked in turn and a tree of any depth is covered. New threads are not listed there, copy_process() gives a CLONE_THREAD child the real_parent of its creator rather than the creator itself, but the thread group walk above has them. The other two ways of choosing what to trace need nothing, for the same reasons the window never affected them: 'perf trace -a' does not filter on pid at all, and evlist__prepare_workload() keeps a workload blocked on a pipe until evlist__start_workload(), well after the attach. A target that exits during startup is not an error. Reading /proc for a task that has gone fails with ENOENT, and a task directory that is read but has nothing in it sets nothing at all, so errno is cleared before the enumeration and only an allocation failure is passed back. Anything else leaves the tasks that evlist__create_maps() already found in the map, which sched_process_exit() takes out again as they die, and the session runs on rather than being ended over a target that was going to stop producing events anyway. What is left is smaller and no longer lasts. A task found here may have made syscalls between sys_enter going live and it being added to the map, and those are not reported, but it is traced from that point on. On a kernel built without CONFIG_PROC_CHILDREN the children files are absent and descendants cannot be named, leaving the threads of the target, which are still picked up. pid_t, PATH_MAX, FILE and the directory reading are all used directly by the new code, so <sys/types.h>, <limits.h>, <stdio.h> and <dirent.h> are included rather than relied upon to arrive through another header. Nothing is read out of /proc twice. A pid is in the collection only because the task directory holding it was read, and that directory holds the whole thread group, so both the directory and the children files of everything in it have been read already. 'perf trace -p' needs this: the thread map names every thread of the target, each of them is queued as something to expand, and they all stand for the same directory, so a target of N threads was read N times over and a children file was opened N squared times. On a 64 thread target that is 4097 of them, against 65 once the ones already read are left alone. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 256 +++++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index ddbe1c8ec1dd..6b52f0d408dd 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -15,6 +15,7 @@ */ #include "util/record.h" +#include <api/fs/fs.h> #include <api/fs/tracing_path.h> #ifdef HAVE_LIBBPF_SUPPORT #include <bpf/bpf.h> @@ -65,11 +66,15 @@ #include "trace_augment.h" #include "dwarf-regs.h" +#include <dirent.h> #include <errno.h> #include <sys/stat.h> +#include <sys/types.h> #include <inttypes.h> +#include <limits.h> #include <poll.h> #include <signal.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> #include <linux/err.h> @@ -4691,6 +4696,247 @@ static int trace__set_filter_pids(struct trace *trace) return err; } +/* A list of pids that grows as it is added to, holding each pid once. */ +struct pid_list { + pid_t *entries; + size_t nr; + size_t allocated; +}; + +static bool pid_list__has(const struct pid_list *list, pid_t pid) +{ + for (size_t i = 0; i < list->nr; i++) { + if (list->entries[i] == pid) + return true; + } + return false; +} + +/* Append pid, unless it is already there. */ +static int pid_list__add(struct pid_list *list, pid_t pid) +{ + if (pid_list__has(list, pid)) + return 0; + + if (list->nr == list->allocated) { + size_t allocated = list->allocated ? list->allocated * 2 : 32; + pid_t *entries = realloc(list->entries, allocated * sizeof(*entries)); + + if (entries == NULL) + return -ENOMEM; + + list->entries = entries; + list->allocated = allocated; + } + + list->entries[list->nr++] = pid; + return 0; +} + +static void pid_list__exit(struct pid_list *list) +{ + zfree(&list->entries); + list->nr = 0; + list->allocated = 0; +} + +/* + * Append the tasks tid has forked to tgids. + * + * task->children holds what a task forked, and a forked task leads a thread + * group of its own, so each is something to expand in turn. New threads are + * not listed: copy_process() gives a CLONE_THREAD child the real_parent of + * its creator rather than the creator itself, so a thread is a sibling of the + * task that created it. Those are enumerated from the task directory instead. + */ +static int pid_list__add_children(struct pid_list *tgids, pid_t tid) +{ + char path[PATH_MAX]; + pid_t child; + FILE *fp; + int err = 0; + + scnprintf(path, sizeof(path), "%s/%d/task/%d/children", + procfs__mountpoint(), tid, tid); + fp = fopen(path, "r"); + /* + * Absent if the task exited, and on a kernel built without + * CONFIG_PROC_CHILDREN. Neither is worth failing for: what is missed + * is a task that has gone away, or descendants the kernel will not + * name. + */ + if (fp == NULL) + return 0; + + while (fscanf(fp, "%d", &child) == 1) { + err = pid_list__add(tgids, child); + if (err) + break; + } + + fclose(fp); + return err; +} + +/* + * Collect the tasks to trace: the target, its threads, and everything they + * have forked. + * + * tgids is the queue of thread groups still to expand. It is walked as it + * grows, so a child found here has its own children picked up in a later + * pass and the depth of the tree does not matter. The walk terminates + * because a task cannot be its own ancestor and pid_list__add() ignores a + * pid that is already listed. + */ +static int trace__collect_target_pids(struct trace *trace, struct pid_list *pids) +{ + struct target *target = &trace->opts.target; + /* + * -p names processes, so the whole thread group is a target. -t names + * threads, and expanding one to its group would trace the siblings + * that were deliberately left out. + * + * A thread the target started during the window is left out with them. + * copy_process() makes a CLONE_THREAD child a sibling of its creator + * rather than a child, so it is not in children, and /proc does not + * record which task in a group created which, leaving it + * indistinguishable from a sibling that was never a target. + * sched_process_fork() does pick it up once BPF is attached, as it is + * told the creator rather than having to infer it. + */ + bool whole_group = target->pid != NULL; + struct perf_thread_map *threads; + struct pid_list tgids = {}; + int err = 0; + + /* Enumerate the target as evlist__create_maps() did, but now. */ + errno = 0; + threads = thread_map__new_str(target->pid, target->tid, target->per_thread); + if (threads == NULL) { + char bf[128]; + + /* + * A target that exited while perf trace was starting up shows + * up here as a failure to read /proc/<pid>/task, with scandir() + * setting ENOENT, or as a task directory that is read but has + * nothing in it, which sets nothing at all and is why errno is + * cleared above. Neither is worth ending the session for: the + * tasks the target had are already in the map from + * evlist__create_maps() and sched_process_exit() takes them out + * again as they die. Carry on with what is known and let only + * an allocation failure through, matching how the pid_list + * additions below are treated. + */ + if (errno == ENOMEM) + return -ENOMEM; + + pr_debug("Couldn't enumerate the target again (%s), tracing the tasks already known\n", + errno == 0 ? "it exited" : str_error_r(errno, bf, sizeof(bf))); + return 0; + } + + for (int i = 0; i < perf_thread_map__nr(threads); i++) { + pid_t pid = perf_thread_map__pid(threads, i); + + err = pid_list__add(whole_group ? &tgids : pids, pid); + /* A thread named by -t is not expanded, but its children are. */ + if (!err && !whole_group) + err = pid_list__add_children(&tgids, pid); + if (err) + goto out; + } + + for (size_t i = 0; i < tgids.nr; i++) { + pid_t tgid = tgids.entries[i]; + char path[PATH_MAX]; + struct dirent *dent; + DIR *tasks; + + /* + * A pid is in pids only because the task directory holding it + * was read, and that directory holds the whole thread group, + * so this one has been read already. 'perf trace -p' relies on + * this: the thread map names every thread of the target, each + * of them is queued here, and they all stand for the same + * directory, which would otherwise be read once per thread. + */ + if (pid_list__has(pids, tgid)) + continue; + + scnprintf(path, sizeof(path), "%s/%d/task", procfs__mountpoint(), tgid); + tasks = opendir(path); + if (tasks == NULL) + continue; /* Exited between being named and being read. */ + + while ((dent = readdir(tasks)) != NULL) { + char *end; + pid_t tid = strtol(dent->d_name, &end, 10); + + /* Skip "." and "..". */ + if (*end != '\0') + continue; + + /* Seen before, so its children have been read too. */ + if (pid_list__has(pids, tid)) + continue; + + err = pid_list__add(pids, tid); + if (!err) + err = pid_list__add_children(&tgids, tid); + if (err) + break; + } + + closedir(tasks); + if (err) + goto out; + } +out: + perf_thread_map__put(threads); + pid_list__exit(&tgids); + return err; +} + +/* + * Trace the tasks that appeared while perf trace was starting up. + * + * evlist__create_maps() enumerated the target from /proc, and + * sched_process_fork() only sees what is cloned once it is attached. A task + * created in between is in neither, and since cmd_trace() drops the sys_enter + * evsel in favour of __augmented_syscalls__ it would go unreported for the + * whole session. + * + * Read the target out of /proc again now that the programs are attached. + * Whatever came before the attach is in /proc to be found, and whatever comes + * after it is sched_process_fork()'s to add, so between them nothing is left + * out. + * + * Doing this after the attach rather than before it is what makes that true; + * before it would only move the window. The cost is that a task found here + * may have made syscalls between sys_enter going live and it being added + * below, and those are not reported. It is traced from that point on. + */ +static int trace__set_startup_pids(struct trace *trace) +{ + struct pid_list pids = {}; + int err; + + /* + * Nothing to do without a target: 'perf trace -a' does not filter on + * pid at all, and evlist__prepare_workload() keeps a workload blocked + * on a pipe until evlist__start_workload(), well after the attach. + */ + if (!target__has_task(&trace->opts.target)) + return 0; + + err = trace__collect_target_pids(trace, &pids); + if (!err) + err = augmented_syscalls__set_target_pids(pids.nr, pids.entries); + + pid_list__exit(&pids); + return err; +} + static int __trace__deliver_event(struct trace *trace, union perf_event *event) { struct evlist *evlist = trace->evlist; @@ -5057,6 +5303,16 @@ static int trace__run(struct trace *trace, int argc, const char **argv) if (err < 0) goto out_error_attach; + /* + * With the programs live, anything the target created while they were + * being set up is now sched_process_fork()'s to keep track of, but it + * was not there to see it appear. Enumerate the target once more to + * pick those up. + */ + err = trace__set_startup_pids(trace); + if (err < 0) + goto out_error_filter_pids; + /* * If the "close" syscall is not traced, then we will not have the * opportunity to, in syscall_arg__scnprintf_close_fd() invalidate the -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 15/23] perf trace: Drop targets that died before they were filtered 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (13 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 14/23] perf trace: Enumerate the target again once BPF is attached Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 16/23] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers ` (7 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz sched_process_exit() takes a task out of pids_to_trace and pids_filtered as it dies, which keeps the maps holding live tasks rather than growing for the length of the session. It only finds a task there if userspace put it there first, and userspace writes a pid some time after reading it: - trace__set_filter_pids() writes the thread map and the --filter-pids list before the programs are attached, so for those there is nothing watching them die at all. - trace__set_startup_pids() writes what it enumerated after the attach, which is a much smaller gap but still a gap. A task that dies inside one of those windows is never evicted, because the delete that would have done it ran while the map had nothing to delete. Its pid then sits in the map for the rest of the session: it holds one of a fixed number of entries, and pids are reused, so the unrelated task that eventually receives it is treated as the target, or as a task the user asked to leave out and is then silently not reported. Walk both maps once the last write to them is done and remove the tasks that are not alive. Sweeping the maps rather than the lists that were written to them covers every way a pid can get in, including the pid of perf itself and of the terminal it was started from, which trace__set_filter_loop_pids() adds without recording anywhere. A task found alive here and dying later is one sched_process_exit() can see and evict, so a single pass is enough, and the tasks the BPF programs add for themselves need nothing: sched_process_fork() inserts a task before it has run, so it cannot have died beforehand. Deleting the key the walk is standing on leaves bpf_map__get_next_key() to resume from a key that is no longer there, which htab_map_get_next_key() answers with the first key of the whole map, starting the walk over. The walk therefore only ever stands on a key it has found alive, which is one it has just decided to keep, so nothing it deletes is ever the key it would go on to ask from. A key deleted while the walk stands on nothing is not a problem either: the next request is for the first key of the map, and the deleted one is no longer it. sched_process_exit() deletes keys while this runs and can take out the key the walk is standing on, which there is no way to prevent or to notice. The walk is bounded at max_entries steps so that it ends however often that happens: the map holds no more than that many keys, so a walk that has taken as many steps has either seen them all or been restarted, and the bound is what stops a walk that keeps being restarted from going round for ever. Ending on the bound can leave a dead task behind, which costs one entry of a great many until the session ends, where not ending would cost the session itself. A task is taken to be gone when its /proc entry is not there, and also when /proc reports it as Z or X. Both mean it is in or past do_exit(), which is where sched_process_exit() runs, so waiting for a zombie to be reaped before dropping it would only keep a dead task in the map for longer. Everything else, including not being able to tell, leaves the task in the map. The two mistakes are not equal: a dead task left there holds an entry nothing needs, while a live one taken out stops being traced for the rest of the session, so a read that fails for perf's own reasons, being out of file descriptors or of memory, is not read as the task having exited. Reading that state means finding the end of the command name, which do_task_stat() writes out unescaped, unlike /proc/<pid>/status: it holds whatever the task called itself, ')' and newlines included. The whole of /proc/<pid>/stat is read rather than a line of it, because a line stops at the first embedded newline, short of the name's closing ')', and the last ')' in the file is taken as that one, since no field after the name has parentheses in it. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/builtin-trace.c | 76 ++++++++++++++++++++++++--- tools/perf/util/bpf_trace_augment.c | 79 +++++++++++++++++++++++++++++ tools/perf/util/trace_augment.h | 6 +++ 3 files changed, 153 insertions(+), 8 deletions(-) diff --git a/tools/perf/builtin-trace.c b/tools/perf/builtin-trace.c index 6b52f0d408dd..b318623486e2 100644 --- a/tools/perf/builtin-trace.c +++ b/tools/perf/builtin-trace.c @@ -4897,6 +4897,59 @@ static int trace__collect_target_pids(struct trace *trace, struct pid_list *pids return err; } +/* + * Is the task still running? + * + * A pid with no /proc entry is gone, and one reported as Z (zombie) or X (dead) + * is in or past do_exit(), which is where sched_process_exit() runs. Either way + * the BPF programs will never hear about it again. + * + * Anything that is not one of those, including being unable to tell, is taken + * as alive. The two mistakes are not equal: a dead task left in the map holds + * an entry that nothing needs, while a live one taken out of it stops being + * traced for the rest of the session. + */ +static bool trace__task_is_alive(pid_t pid) +{ + char path[PATH_MAX]; + const char *state; + char *stat = NULL; + bool alive = true; + size_t len; + int err; + + scnprintf(path, sizeof(path), "%s/%d/stat", procfs__mountpoint(), pid); + err = filename__read_str(path, &stat, &len); + if (err) { + /* + * ENOENT and ESRCH are the task being gone, which is what this + * is looking for. Any other error is perf's own, running out + * of file descriptors or of memory, and says nothing about the + * task. + */ + return err != -ENOENT && err != -ESRCH; + } + + /* + * The state is the field after the command name. do_task_stat() writes + * that name out as it is, so it can hold anything a task cares to call + * itself, ')' and newlines included. Nothing after it has parentheses, + * so the last ')' in the file is the one that closes it; the whole file + * is read rather than a line of it because a line stops at the first of + * those newlines, short of the ')' that is being looked for. + * + * A read that returns something the state cannot be read out of, such + * as the empty result of the task exiting midway through it, leaves the + * task alive by the rule above. + */ + state = strrchr(stat, ')'); + if (state != NULL && state[1] == ' ') + alive = state[2] != 'Z' && state[2] != 'X'; + + free(stat); + return alive; +} + /* * Trace the tasks that appeared while perf trace was starting up. * @@ -4919,19 +4972,26 @@ static int trace__collect_target_pids(struct trace *trace, struct pid_list *pids static int trace__set_startup_pids(struct trace *trace) { struct pid_list pids = {}; - int err; + int err = 0; /* - * Nothing to do without a target: 'perf trace -a' does not filter on - * pid at all, and evlist__prepare_workload() keeps a workload blocked - * on a pipe until evlist__start_workload(), well after the attach. + * Only a target is enumerated: 'perf trace -a' does not filter on pid + * at all, and evlist__prepare_workload() keeps a workload blocked on a + * pipe until evlist__start_workload(), well after the attach. */ - if (!target__has_task(&trace->opts.target)) - return 0; + if (target__has_task(&trace->opts.target)) { + err = trace__collect_target_pids(trace, &pids); + if (!err) + err = augmented_syscalls__set_target_pids(pids.nr, pids.entries); + } - err = trace__collect_target_pids(trace, &pids); + /* + * Every session gets the sweep, target or not: --filter-pids names + * tasks to leave out with no target of its own, and those are written + * to a map of their own, before the attach and with the same race. + */ if (!err) - err = augmented_syscalls__set_target_pids(pids.nr, pids.entries); + err = augmented_syscalls__prune_dead_pids(trace__task_is_alive); pid_list__exit(&pids); return err; diff --git a/tools/perf/util/bpf_trace_augment.c b/tools/perf/util/bpf_trace_augment.c index 44f30dba5469..4f93f5c062c1 100644 --- a/tools/perf/util/bpf_trace_augment.c +++ b/tools/perf/util/bpf_trace_augment.c @@ -255,6 +255,85 @@ int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids) return 0; } +/* + * Remove from a pid keyed map every task that is no longer alive. + * + * Deleting the key the walk is standing on leaves bpf_map__get_next_key() to + * resume from a key that is no longer there, which htab_map_get_next_key() + * answers with the first key of the whole map, starting the walk over. The + * cursor is therefore only ever moved onto a key that has been found alive, + * which is a key this function has just decided to keep, so nothing it does + * can send itself back to the beginning. + * + * sched_process_exit() deletes keys while this runs and can take out the + * cursor, which there is no way to prevent or to notice. The walk is bounded + * at max_entries steps so that it ends however often that happens: the map + * holds no more than that many keys, so a walk that has taken as many steps + * has either seen them all or been restarted, and the bound is what stops a + * repeatedly restarted walk from going round for ever. Stopping there can + * leave a dead task behind, which costs one entry of a great many until the + * session ends, where not stopping would cost the session itself. + */ +static int prune_dead_map_pids(struct bpf_map *map, bool (*is_alive)(pid_t pid)) +{ + size_t max_entries = bpf_map__max_entries(map); + pid_t cursor, key; + bool have_cursor = false; + + for (size_t step = 0; step < max_entries; step++) { + int err; + + /* Anything other than success means there is no next key. */ + if (bpf_map__get_next_key(map, have_cursor ? &cursor : NULL, + &key, sizeof(key)) != 0) + break; + + if (is_alive(key)) { + cursor = key; + have_cursor = true; + continue; + } + + err = bpf_map__delete_elem(map, &key, sizeof(key), /*flags=*/0); + /* + * ENOENT means the key went away while this was running, which + * is sched_process_exit() doing the same job. + */ + if (err && err != -ENOENT) + return err; + + /* + * The cursor stays where it was, which is either a key that is + * still in the map or unset. Unset asks the next step for the + * first key of the map, and the one just deleted is no longer + * it, so the walk moves on either way. + */ + } + + return 0; +} + +/* + * Remove the tasks that are no longer alive from both pid maps. + * + * has_pids_to_trace and has_pids_filtered are left alone. They say that the + * maps are in use rather than that they have anything in them, and clearing + * the first would turn a targeted session into a system wide one. + */ +int augmented_syscalls__prune_dead_pids(bool (*is_alive)(pid_t pid)) +{ + int err; + + if (skel == NULL) + return 0; + + err = prune_dead_map_pids(skel->maps.pids_to_trace, is_alive); + if (!err) + err = prune_dead_map_pids(skel->maps.pids_filtered, is_alive); + + return err; +} + /* * Populate syscalls in the BPF syscalls_to_trace map: * - not_syscalls: true if '!' prefix was specified (blacklist mode: trace diff --git a/tools/perf/util/trace_augment.h b/tools/perf/util/trace_augment.h index ad992f5fa726..87dbf8c46c1e 100644 --- a/tools/perf/util/trace_augment.h +++ b/tools/perf/util/trace_augment.h @@ -16,6 +16,7 @@ int augmented_syscalls__create_bpf_output(struct evlist *evlist); void augmented_syscalls__setup_bpf_output(void); int augmented_syscalls__set_filter_pids(unsigned int nr, pid_t *pids); int augmented_syscalls__set_target_pids(unsigned int nr, pid_t *pids); +int augmented_syscalls__prune_dead_pids(bool (*is_alive)(pid_t pid)); int augmented_syscalls__set_target_syscalls(unsigned int nr, int *syscall_ids, bool not_syscalls); int augmented_syscalls__get_map_fds(int *enter_fd, int *exit_fd, int *beauty_fd); struct bpf_program *augmented_syscalls__find_by_title(const char *name); @@ -55,6 +56,11 @@ static inline int augmented_syscalls__set_target_pids(unsigned int nr __maybe_un return 0; } +static inline int augmented_syscalls__prune_dead_pids(bool (*is_alive)(pid_t pid) __maybe_unused) +{ + return 0; +} + static inline int augmented_syscalls__set_target_syscalls(unsigned int nr __maybe_unused, int *syscall_ids __maybe_unused, bool not_syscalls __maybe_unused) -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 16/23] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (14 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 15/23] perf trace: Drop targets that died before they were filtered Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 17/23] perf test common: Only disable probes in clear_all_probes Ian Rogers ` (6 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz test_task_analyzer.sh writes perf.data and temporary files directly into the current working directory, causing collisions when running tests in parallel. As a temporary measure until `perf script report` supports an input file option, resolve perfdir to an absolute path, change directory into $tmpdir for the test duration, and clean up in the exit trap. Remove the (exclusive) tag so the test runs in parallel. perfdir is derived from $0, which may be relative, so it has to be resolved before the cd into $tmpdir, otherwise both PERF_EXEC_PATH and the cleanup trap point at paths that no longer exist. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/test_task_analyzer.sh | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tools/perf/tests/shell/test_task_analyzer.sh b/tools/perf/tests/shell/test_task_analyzer.sh index 0314412e63b4..443b88957b17 100755 --- a/tools/perf/tests/shell/test_task_analyzer.sh +++ b/tools/perf/tests/shell/test_task_analyzer.sh @@ -1,8 +1,18 @@ #!/bin/bash -# perf script task-analyzer tests (exclusive) +# perf script task-analyzer tests # SPDX-License-Identifier: GPL-2.0 -tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) +# Resolve the source directory before changing the working directory below, +# $0 may be a relative path and would no longer resolve from $tmpdir. +perfdir=$(cd "$(dirname "$0")/../.." && pwd) + +tmpdir=$(mktemp -d /tmp/perf-script-task-analyzer-XXXXX) || exit 1 +# The cleanup trap is only installed further down, once the functions it +# calls have been defined, so tidy up by hand if this cd fails. +cd "$tmpdir" || { + rmdir "$tmpdir" + exit 1 +} # TODO: perf script report only supports input from the CWD perf.data file, make # it support input from any file. perfdata="perf.data" @@ -11,7 +21,6 @@ csvsummary="$tmpdir/csvsummary" err=0 # set PERF_EXEC_PATH to find scripts in the source directory -perfdir=$(dirname "$0")/../.. if [ -e "$perfdir/scripts/python/Perf-Trace-Util" ]; then export PERF_EXEC_PATH=$perfdir fi @@ -20,8 +29,9 @@ fi export ASAN_OPTIONS=detect_leaks=0 cleanup() { - rm -f "${perfdata}" - rm -f "${perfdata}".old + # Step out of $tmpdir before removing it. The removal uses an absolute + # path and works from anywhere, so a failure to cd must not skip it. + cd "$perfdir" || cd / || true rm -rf "$tmpdir" trap - exit term int -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 17/23] perf test common: Only disable probes in clear_all_probes 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (15 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 16/23] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 18/23] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers ` (5 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz clear_all_probes() began by writing 0 to /sys/kernel/debug/tracing/events/enable, which disables every tracepoint on the system rather than just the probes the probe tests created. When tests run in parallel that also silences the events of any concurrent perf record, perf trace or ftrace session, so unrelated tests lose the events they are waiting for and fail. The write cannot simply be dropped. Clearing kprobe_events or uprobe_events is all or nothing: dyn_events_release_all() walks every probe of that type first and returns -EBUSY without removing any of them if it finds one that is still enabled, where enabled means TP_FLAG_TRACE from tracefs or TP_FLAG_PROFILE from a perf session. A probe left enabled through tracefs would therefore block the whole clear and leave stale probes behind to collide with the next run. Disable the probes and only the probes. The set to disable is taken from the kprobe_events and uprobe_events listings rather than from a guess at which groups perf uses, so it matches what dyn_events_release_all() is going to inspect: a probe an unrelated session left enabled in, say, the kprobes group would otherwise still abort the clear. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/common/init.sh | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/common/init.sh b/tools/perf/tests/shell/common/init.sh index cbfc78bec974..7c2ca74298ca 100644 --- a/tools/perf/tests/shell/common/init.sh +++ b/tools/perf/tests/shell/common/init.sh @@ -130,9 +130,40 @@ check_uprobes_available() test -e /sys/kernel/debug/tracing/uprobe_events } +# Disable every kprobe and uprobe event. The listings name each probe as +# "TYPE:GROUP/EVENT ARGS...", for instance "p:probe/vfs_read vfs_read", and +# events/GROUP/EVENT/enable is the switch for it. +disable_all_probes() +{ + PROBE_SPECS=`cat /sys/kernel/debug/tracing/kprobe_events \ + /sys/kernel/debug/tracing/uprobe_events 2> /dev/null | + cut -d ' ' -f 1` + for PROBE_SPEC in $PROBE_SPECS + do + case "$PROBE_SPEC" in + *:*/*) ;; + *) continue ;; + esac + PROBE_ENABLE="/sys/kernel/debug/tracing/events/${PROBE_SPEC#*:}/enable" + test -e "$PROBE_ENABLE" && echo 0 > "$PROBE_ENABLE" + done +} + clear_all_probes() { - echo 0 > /sys/kernel/debug/tracing/events/enable + # Disable the probes before removing them. Writing to kprobe_events or + # uprobe_events is all or nothing: dyn_events_release_all() walks every + # probe of that type first and returns -EBUSY without removing any of + # them if it finds one that is still enabled, which would leave stale + # probes behind to collide with the next run. That covers probes this + # test suite never created, so disable all of them and not just the + # ones in the groups perf uses. + # + # Only probes are disabled. Writing to events/enable would also silence + # the tracepoints of any perf record, perf trace or ftrace session + # sharing the machine, which breaks those tests when they run in + # parallel with this one. + disable_all_probes check_kprobes_available && echo > /sys/kernel/debug/tracing/kprobe_events check_uprobes_available && echo > /sys/kernel/debug/tracing/uprobe_events } -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 18/23] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (16 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 17/23] perf test common: Only disable probes in clear_all_probes Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 19/23] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers ` (4 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The probe name `vfs_getname` was hardcoded, causing collisions when tests ran concurrently. Furthermore, `cleanup_probe_vfs_getname()` used `perf probe -d probe:vfs_getname*`, deleting probes registered by other parallel tests. Scope the probe name to the pid, and rename it to `getname_flags_$$` so that it no longer begins with "vfs_getname". perf trace calls evlist__add_vfs_getname(), which opens every event matching a hardcoded "probe:vfs_getname*" wildcard, so a perf trace run by any other test would otherwise pin this probe and make `perf probe -d` fail with -EBUSY. That also unblocks making the perf trace tests non-exclusive later in this series. Enumerate the probes to record and to delete from `perf probe -l`, matching `^probe:${vfs_getname}(_[[:digit:]]+)?$` exactly, rather than globbing on `${vfs_getname}*`. perf probe appends _1, _2, ... when getname_flags is inlined at more than one call site, so the variants do have to be matched, but since the name now ends in a pid a trailing wildcard would also match the probes of a test whose pid merely starts with this one's, e.g. 123 and 1234. Remove the `(exclusive)` tag from probe_vfs_getname.sh and record+script_probe_vfs_getname.sh so they run concurrently in pass 1. trace+probe_vfs_getname.sh has to stay exclusive: it is the one test that wants to be discovered by that wildcard, so it sets vfs_getname to a "vfs_getname_$$" name before sourcing the library, and would then pin its siblings' probes if it ran alongside them. A comment in the test records this. Delete the probes from an exit trap as well as on the way out. A pid scoped name is never seen again, so a run interrupted before cleanup_probe_vfs_getname() would leave its probes behind for good, accumulating a set per run. The fixed name at least meant the next run found, and went on to reuse, whatever the last one left. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/lib/probe_vfs_getname.sh | 51 +++++++++++++++++-- tools/perf/tests/shell/probe_vfs_getname.sh | 3 +- .../shell/record+script_probe_vfs_getname.sh | 18 +++++-- .../tests/shell/trace+probe_vfs_getname.sh | 9 ++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/tools/perf/tests/shell/lib/probe_vfs_getname.sh b/tools/perf/tests/shell/lib/probe_vfs_getname.sh index 88cd0e26d5f6..4e915b6aefc8 100644 --- a/tools/perf/tests/shell/lib/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/lib/probe_vfs_getname.sh @@ -1,15 +1,58 @@ #!/bin/bash # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 -perf probe -l 2>&1 | grep -q probe:vfs_getname +# The name of the getname_flags probe added and removed below. +# +# It is scoped to the pid so that tests running in parallel do not collide, +# and it deliberately does not start with "vfs_getname": perf trace calls +# evlist__add_vfs_getname(), which opens everything matching the hardcoded +# "probe:vfs_getname*" wildcard, so a perf trace running in another test would +# otherwise pin this probe and make the 'perf probe -d' below fail with -EBUSY. +# +# trace+probe_vfs_getname.sh is the one test that does want to be found that +# way, so it sets vfs_getname itself before sourcing this file, and is +# (exclusive) as a result. +: "${vfs_getname:=getname_flags_$$}" + +# Print the probes add_probe_vfs_getname() created. perf probe appends _1, _2, +# ... when getname_flags is inlined at more than one call site, so there can be +# several. Match them exactly rather than with a "${vfs_getname}*" glob: the +# name ends in a pid, so such a glob would also match the probes of a test +# whose pid merely starts with this one's, e.g. 123 and 1234. +probes_vfs_getname() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe:${vfs_getname}(_[[:digit:]]+)?$" +} + +[ -n "$(probes_vfs_getname)" ] had_vfs_getname=$? cleanup_probe_vfs_getname() { if [ $had_vfs_getname -eq 1 ] ; then - perf probe -q -d probe:vfs_getname* + local probe + for probe in $(probes_vfs_getname); do + perf probe -q -d "$probe" + done fi } +# Delete the probes however the test ends, not just when it runs to +# completion. The name is scoped to the pid, so nothing that runs later +# reuses or tidies up a probe an interrupted test left behind, and they +# would accumulate one set per run. The fixed name used before was at least +# picked up again by the next run. +# +# Tests may still call cleanup_probe_vfs_getname directly. Doing so leaves +# nothing for probes_vfs_getname to find, so the trap below then does +# nothing. A test needing cleanup of its own should call +# cleanup_probe_vfs_getname from its own exit trap, since installing one +# replaces this rather than adding to it. +trap cleanup_probe_vfs_getname exit +# Turn a signal into an ordinary exit so that the exit trap above runs. An +# exit trap that returns leaves the exit status alone, so a test exiting 2 +# to skip still skips. +trap 'exit 1' term int + add_probe_vfs_getname() { add_probe_verbose=$1 if [ $had_vfs_getname -eq 1 ] ; then @@ -33,8 +76,8 @@ add_probe_vfs_getname() { return 2 fi - perf probe -q "vfs_getname=getname_flags:${line} pathname=result->name:string" || \ - perf probe $add_probe_verbose "vfs_getname=getname_flags:${line} pathname=filename:ustring" || return 1 + perf probe -q "${vfs_getname}=getname_flags:${line} pathname=result->name:string" || \ + perf probe $add_probe_verbose "${vfs_getname}=getname_flags:${line} pathname=filename:ustring" || return 1 fi } diff --git a/tools/perf/tests/shell/probe_vfs_getname.sh b/tools/perf/tests/shell/probe_vfs_getname.sh index 5fe5682c28ce..05f1d50732b6 100755 --- a/tools/perf/tests/shell/probe_vfs_getname.sh +++ b/tools/perf/tests/shell/probe_vfs_getname.sh @@ -1,6 +1,5 @@ #!/bin/bash -# Add vfs_getname probe to get syscall args filenames (exclusive) - +# Add vfs_getname probe to get syscall args filenames # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 diff --git a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh index 002f7037f182..1d4fb4a4fbfe 100755 --- a/tools/perf/tests/shell/record+script_probe_vfs_getname.sh +++ b/tools/perf/tests/shell/record+script_probe_vfs_getname.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Use vfs_getname probe to get syscall args filenames (exclusive) +# Use vfs_getname probe to get syscall args filenames # Uses the 'perf test shell' library to add probe:vfs_getname to the system # then use it with 'perf record' using 'touch' to write to a temp file, then @@ -17,22 +17,32 @@ skip_if_no_perf_probe || exit 2 # shellcheck source=lib/probe_vfs_getname.sh . "$(dirname "$0")/lib/probe_vfs_getname.sh" +# shellcheck disable=SC2154 # vfs_getname is assigned in lib/probe_vfs_getname.sh record_open_file() { echo "Recording open file:" # Check presence of libtraceevent support to run perf record - skip_no_probe_record_support "probe:vfs_getname*" + skip_no_probe_record_support if [ $? -eq 2 ]; then echo "WARN: Skipping test record_open_file. No libtraceevent support" return 2 fi - perf record -o ${perfdata} -e probe:vfs_getname\* touch $file + # Record every probe the inlining of getname_flags produced, naming + # them exactly rather than with a "${vfs_getname}*" glob, which would + # also match the probes of a test whose pid starts with this one's. + local events + events=$(probes_vfs_getname | paste -sd, -) + if [ -z "${events}" ] ; then + echo "FAIL: no ${vfs_getname} probe to record" + return 1 + fi + perf record -o ${perfdata} -e "${events}" touch $file } perf_script_filenames() { echo "Looking at perf.data file for vfs_getname records for the file we touched:" perf script -i ${perfdata} | \ - grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:vfs_getname[_0-9]*: +\([[:xdigit:]]+\) +pathname=\"${file}\"" + grep -E " +touch +[0-9]+ +\[[0-9]+\] +[0-9]+\.[0-9]+: +probe:${vfs_getname}(_[0-9]+)?: +\([[:xdigit:]]+\) +pathname=\"${file}\"" } add_probe_vfs_getname diff --git a/tools/perf/tests/shell/trace+probe_vfs_getname.sh b/tools/perf/tests/shell/trace+probe_vfs_getname.sh index 7a0b1145d0cd..146305f4d549 100755 --- a/tools/perf/tests/shell/trace+probe_vfs_getname.sh +++ b/tools/perf/tests/shell/trace+probe_vfs_getname.sh @@ -10,6 +10,13 @@ # SPDX-License-Identifier: GPL-2.0 # Arnaldo Carvalho de Melo <acme@kernel.org>, 2017 +# This test must stay exclusive, and is the only one of the probe tests that +# does: it does not name the event it uses. perf trace discovers it with the +# hardcoded "probe:vfs_getname*" wildcard in evlist__add_vfs_getname(), so the +# probe has to carry that prefix, and a parallel run of this test would then +# also match, and pin, the probes of the other tests. The sibling tests avoid +# all of this by using a name that the wildcard cannot reach. + # shellcheck source=lib/probe.sh . "$(dirname $0)"/lib/probe.sh @@ -17,6 +24,8 @@ skip_if_no_perf_probe || exit 2 skip_if_no_perf_trace || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# shellcheck disable=SC2034 # consumed by lib/probe_vfs_getname.sh +vfs_getname="vfs_getname_$$" . "$(dirname $0)"/lib/probe_vfs_getname.sh trace_open_vfs_getname() { -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 19/23] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, and make non-exclusive 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (17 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 18/23] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers @ 2026-09-23 7:13 ` Ian Rogers 2026-09-23 7:14 ` [PATCH v5 20/23] perf test trace_summary: Improve error diagnostics Ian Rogers ` (3 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:13 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The uprobe name was not scoped to PID, and concurrent writes to `/sys/kernel/debug/tracing/uprobe_events` can occasionally return `-EBUSY` when another process holds the tracefs inode lock. Scope the probe event name with `$$` (`inet_pton_$$=inet_pton`) and add a retry loop with backoff for uprobe addition. Drop the `(exclusive)` tag so the test can run in parallel during pass 1. A PID scoped probe is no longer cleaned up by any other test, so add an EXIT/TERM/INT trap to delete it, otherwise an interrupted run leaks the uprobe into the system. The trap is installed only after the root and IPv6 checks that `exit 2` to skip the test, as trap_cleanup() exits 1 and would otherwise turn those skips into failures. Deletion enumerates the probes from `perf probe -l`, matching `^probe_libc:inet_pton_$$(_[[:digit:]]+)?$` exactly, rather than reading $event_name: a signal arriving after perf probe injected the uprobe but before the assignment completed would leave that variable empty and leak the probe, and an `inet_pton_$$*` glob would reach the probe of a test whose pid merely starts with this one's. While here use mktemp rather than mktemp -u for the temporary files: this test runs as root in a world writable /tmp, and predicting a name without creating it allows another user to win the race and plant a symlink. The perf.data check becomes -s rather than -e as mktemp now pre-creates an empty file. Pre-create the temporary files with mktemp rather than reserving names with mktemp -u, and bail out if mktemp fails. The emptiness check on the recorded data quotes its path for the same reason: unquoted, an empty value would leave [ ! -s ] testing the string "-s", which is true, so the negation would skip the failure path and the test would go on to pass without having recorded anything. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../shell/record+probe_libc_inet_pton.sh | 107 ++++++++++++++---- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh index eca629ee83f0..00367f26bfae 100755 --- a/tools/perf/tests/shell/record+probe_libc_inet_pton.sh +++ b/tools/perf/tests/shell/record+probe_libc_inet_pton.sh @@ -1,5 +1,5 @@ #!/bin/bash -# probe libc's inet_pton & backtrace it with ping (exclusive) +# probe libc's inet_pton & backtrace it with ping # Installs a probe on libc's inet_pton function, that will use uprobes, # then use 'perf trace' on a ping to localhost asking for just one packet @@ -21,20 +21,30 @@ nm -Dg $libc 2>/dev/null | grep -F -q inet_pton || exit 254 event_pattern='probe_libc:inet_pton(_[[:digit:]]+)?' add_libc_inet_pton_event() { + local attempts=0 + while [ $attempts -lt 3 ]; do + event_name=$(perf probe -f -x $libc -a "inet_pton_$$=inet_pton" 2>&1 | \ + awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ + ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) + + if [ -n "$event_name" ]; then + return 0 + fi + attempts=$((attempts + 1)) + sleep 0.1 + done - event_name=$(perf probe -f -x $libc -a inet_pton 2>&1 | \ - awk -v ep="$event_pattern" -v l="$libc" '$0 ~ ep && $0 ~ \ - ("\\(on inet_pton in " l "\\)") {print $1}' | head -n 1) - - if [ $? -ne 0 ] || [ -z "$event_name" ] ; then - printf "FAIL: could not add event\n" - return 1 - fi + printf "FAIL: could not add event\n" + return 1 } trace_libc_inet_pton_backtrace() { - expected=`mktemp -u /tmp/expected.XXX` + # Create the files rather than just reserving names with mktemp -u: + # this runs as root and /tmp is world writable, so a predictable name + # that is written to later can be pre-created as a symlink by an + # unprivileged user and used to clobber an arbitrary file. + expected=$(mktemp /tmp/expected.XXX) || return 1 echo "ping[][0-9 \.:]+$event_name: \([[:xdigit:]]+\)" > $expected echo ".*inet_pton\+0x[[:xdigit:]]+[[:space:]]\($libc|inlined\)$" >> $expected @@ -50,8 +60,8 @@ trace_libc_inet_pton_backtrace() { ;; esac - perf_data=`mktemp -u /tmp/perf.data.XXX` - perf_script=`mktemp -u /tmp/perf.script.XXX` + perf_data=$(mktemp /tmp/perf.data.XXX) || return 1 + perf_script=$(mktemp /tmp/perf.script.XXX) || return 1 # Check presence of libtraceevent support to run perf record skip_no_probe_record_support "$event_name/$eventattr/" @@ -61,9 +71,12 @@ trace_libc_inet_pton_backtrace() { fi perf record -e $event_name/$eventattr/ -o $perf_data ping -6 -c 1 ::1 > /dev/null 2>&1 - # check if perf data file got created in above step. - if [ ! -e $perf_data ]; then - printf "FAIL: perf record failed to create \"%s\" \n" "$perf_data" + # Check perf record actually wrote data. mktemp already created the + # file, so test that it is non-empty rather than that it exists. Quote + # the path: were it ever empty, [ ! -s ] would test the string "-s" + # instead and report success. + if [ ! -s "$perf_data" ]; then + printf "FAIL: perf record failed to write \"%s\" \n" "$perf_data" return 1 fi perf script -i $perf_data | tac | grep -m1 ^ping -B9 | tac > $perf_script @@ -97,21 +110,75 @@ trace_libc_inet_pton_backtrace() { # even if the perf script output does not match. } +# Print the pid scoped uprobes this test may have created. perf probe appends +# _1, _2, ... when the name is already taken, so match those too, but anchor +# the match: an "inet_pton_$$*" glob would also match the probe of a test whose +# pid merely starts with this one's, e.g. 123 and 1234. +libc_inet_pton_events() { + perf probe -l 2>/dev/null | awk '{print $1}' | + grep -E "^probe_libc:inet_pton_$$(_[[:digit:]]+)?$" +} + delete_libc_inet_pton_event() { + # Ask the kernel what is actually there rather than trusting + # $event_name: a signal arriving after perf probe injected the uprobe + # but before the assignment to event_name completed would otherwise + # leave the variable empty and leak the probe. + # + # Retry as the addition does. Deleting writes to uprobe_events just as + # adding does, so it can lose the same race with a concurrent test and + # fail with -EBUSY. Re-list rather than assume the delete worked, and + # only give up once the probes are really gone: the name is pid + # scoped, so one left behind here is never reused or overwritten by a + # later run and would sit in the kernel until reboot. + local attempts=0 + local probe + + while [ $attempts -lt 3 ]; do + for probe in $(libc_inet_pton_events); do + perf probe -q -d "$probe" + done - if [ -n "$event_name" ] ; then - perf probe -q -d $event_name - fi + if [ -z "$(libc_inet_pton_events)" ]; then + return 0 + fi + + attempts=$((attempts + 1)) + sleep 0.1 + done + + printf "WARN: could not delete event(s): %s\n" \ + "$(libc_inet_pton_events | tr '\n' ' ')" + return 1 +} + +cleanup() { + rm -f ${perf_data} ${perf_script} ${expected} + delete_libc_inet_pton_event + + trap - EXIT TERM INT +} + +trap_cleanup() { + cleanup + exit 1 } # Check for IPv6 interface existence ip a sh lo | grep -F -q inet6 || exit 2 [ "$(id -u)" = 0 ] || exit 2 +# Install the trap only now that the skips above are out of the way: it exits +# 1, so arming it any earlier would turn an 'exit 2' skip into a failure. +# +# The event name is pid scoped, so unlike the old fixed name an orphan left +# behind by an interrupted run is never overwritten by a later run: it would +# stay in the kernel forever. Always clean up, including on a signal. +trap trap_cleanup EXIT TERM INT + skip_if_no_perf_probe && \ add_libc_inet_pton_event && \ trace_libc_inet_pton_backtrace err=$? -rm -f ${perf_data} ${perf_script} ${expected} -delete_libc_inet_pton_event +cleanup exit $err -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 20/23] perf test trace_summary: Improve error diagnostics 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (18 preceding siblings ...) 2026-09-23 7:13 ` [PATCH v5 19/23] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers @ 2026-09-23 7:14 ` Ian Rogers 2026-09-23 7:14 ` [PATCH v5 21/23] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers ` (2 subsequent siblings) 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:14 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz When pattern matching fails in test_perf_trace(), print the command that failed along with the actual match count, the matching lines found, and the last 20 lines of output to aid debugging. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index b80dea77cec6..4589b4581419 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -28,10 +28,16 @@ test_perf_trace() { count=$(grep -E -c -m 3 "${search}" ${OUTPUT}) if [ "${count}" != "3" ]; then - echo "Error: cannot find enough pattern ${search} in the output" - cat ${OUTPUT} - rm -f ${OUTPUT} - exit 1 + echo "Error: cannot find enough pattern ${search} (count=${count}) in output of:" + echo "Error: perf trace ${args} -- ${workload}" + echo "Error: matched lines:" + grep -E "${search}" ${OUTPUT} || echo "none" + echo "Error: last 20 lines of output:" + # The summary is printed after any trace output, so show the end of + # the file: with -S the head of it is the trace, not what is matched. + tail -n 20 ${OUTPUT} + rm -f ${OUTPUT} + exit 1 fi } -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 21/23] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (19 preceding siblings ...) 2026-09-23 7:14 ` [PATCH v5 20/23] perf test trace_summary: Improve error diagnostics Ian Rogers @ 2026-09-23 7:14 ` Ian Rogers 2026-09-23 7:14 ` [PATCH v5 22/23] perf test trace_summary: Make non-exclusive Ian Rogers 2026-09-23 7:14 ` [PATCH v5 23/23] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:14 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_btf_general.sh used `--max-events=1` with `perf trace` on commands such as `mv`, `echo`, and `sleep`. When background activity occurs or tests run in parallel, `perf trace` can capture an event from an unrelated process and exit prematurely before recording the target command's syscalls. Drop `--max-events=1` and let tracing run until the command completes, checking for the expected output with grep (matching trace_btf_enum.sh). Remove the (exclusive) tag so the test runs in parallel. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_btf_general.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/perf/tests/shell/trace_btf_general.sh b/tools/perf/tests/shell/trace_btf_general.sh index 7a94a5743924..4d654b687a4e 100755 --- a/tools/perf/tests/shell/trace_btf_general.sh +++ b/tools/perf/tests/shell/trace_btf_general.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace BTF general tests (exclusive) +# perf trace BTF general tests # SPDX-License-Identifier: GPL-2.0 err=0 @@ -27,7 +27,7 @@ check_vmlinux() { trace_test_string() { echo "Testing perf trace's string augmentation" - output="$(perf trace --sort-events -e renameat* --max-events=1 -- mv ${file1} ${file2} 2>&1)" + output="$(perf trace --sort-events -e renameat* -- mv ${file1} ${file2} 2>&1)" if ! echo "$output" | grep -q -E "^mv/[0-9]+ renameat(2)?\(.*, \"${file1}\", .*, \"${file2}\", .*\) += +[0-9]+$" then printf "String augmentation test failed, output:\n$output\n" @@ -38,7 +38,7 @@ trace_test_string() { trace_test_buffer() { echo "Testing perf trace's buffer augmentation" # echo will insert a newline (\10) at the end of the buffer - output="$(perf trace --sort-events -e write --max-events=1 -- echo "${buffer}" 2>&1)" + output="$(perf trace --sort-events -e write -- echo "${buffer}" 2>&1)" if ! echo "$output" | grep -q -E "^echo/[0-9]+ write\([0-9]+, ${buffer}.*, [0-9]+\) += +[0-9]+$" then printf "Buffer augmentation test failed, output:\n$output\n" @@ -48,7 +48,7 @@ trace_test_buffer() { trace_test_struct_btf() { echo "Testing perf trace's struct augmentation" - output="$(perf trace --sort-events -e clock_nanosleep --force-btf --max-events=1 -- sleep 1 2>&1)" + output="$(perf trace --sort-events -e clock_nanosleep --force-btf -- sleep 1 2>&1)" if ! echo "$output" | grep -q -E "^sleep/[0-9]+ clock_nanosleep\(0, 0, \{1,.*\}, 0x[0-9a-f]+\) += +[0-9]+$" then printf "BTF struct augmentation test failed, output:\n$output\n" -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 22/23] perf test trace_summary: Make non-exclusive 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (20 preceding siblings ...) 2026-09-23 7:14 ` [PATCH v5 21/23] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers @ 2026-09-23 7:14 ` Ian Rogers 2026-09-23 7:14 ` [PATCH v5 23/23] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:14 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz trace_summary.sh tests various summary modes of `perf trace`. It already directs output to a unique temporary file without polluting the current working directory. Remove the (exclusive) tag so it can run concurrently in parallel test runs. Running perf trace in parallel is only safe now that the probe tests no longer name their probes "vfs_getname...": perf trace opens everything matching a hardcoded "probe:vfs_getname*" wildcard, which used to pin those probes and make their cleanup fail with -EBUSY. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- tools/perf/tests/shell/trace_summary.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/perf/tests/shell/trace_summary.sh b/tools/perf/tests/shell/trace_summary.sh index 4589b4581419..d0196351243d 100755 --- a/tools/perf/tests/shell/trace_summary.sh +++ b/tools/perf/tests/shell/trace_summary.sh @@ -1,5 +1,5 @@ #!/bin/bash -# perf trace summary (exclusive) +# perf trace summary # SPDX-License-Identifier: GPL-2.0 # Check that perf trace works with various summary mode -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
* [PATCH v5 23/23] perf test uprobe_from_different_cu: Scope probe name to PID 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers ` (21 preceding siblings ...) 2026-09-23 7:14 ` [PATCH v5 22/23] perf test trace_summary: Make non-exclusive Ian Rogers @ 2026-09-23 7:14 ` Ian Rogers 22 siblings, 0 replies; 91+ messages in thread From: Ian Rogers @ 2026-09-23 7:14 UTC (permalink / raw) To: irogers, acme, howardchu95, namhyung Cc: adrian.hunter, james.clark, jolsa, linux-kernel, linux-perf-users, mingo, peterz The test builds a binary in a per-run temporary directory and probes its foo function. The directory name is unique, but perf probe derives the event name from the probed function and the group name from the binary's basename, so every run registers the same probe_testfile:foo event. Running the test concurrently with itself, as 'perf test -r3' does, therefore fails in all but one of the runs with: Error: event "foo" already exists. Hint: Remove existing event by 'perf probe -d' and a losing run's cleanup goes on to delete the winning run's probe out from under it. Name the event after the pid, foo_$$, so that parallel runs no longer collide. This lets the test stay in the parallel pass rather than having to be marked (exclusive). Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> --- .../perf/tests/shell/test_uprobe_from_different_cu.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh index 7adf9755d6de..47c99d93436b 100755 --- a/tools/perf/tests/shell/test_uprobe_from_different_cu.sh +++ b/tools/perf/tests/shell/test_uprobe_from_different_cu.sh @@ -18,12 +18,19 @@ fi temp_dir=$(mktemp -d /tmp/perf-uprobe-different-cu-sh.XXXXXXXXXX) +# The name of the uprobe added and removed below. The probe is placed on +# ${temp_dir}/testfile, but perf probe derives the event name from the probed +# function and the group name from the binary's basename, so every run would +# otherwise share one probe_testfile:foo event, and a concurrent run would +# fail with 'event "foo" already exists'. Scope the event name to the pid. +probe_name="foo_$$" + cleanup() { trap - EXIT TERM INT if [[ "${temp_dir}" =~ ^/tmp/perf-uprobe-different-cu-sh.*$ ]]; then echo "--- Cleaning up ---" - perf probe -x ${temp_dir}/testfile -d foo || true + perf probe -x ${temp_dir}/testfile -d ${probe_name} || true rm -f "${temp_dir}/"* rmdir "${temp_dir}" fi @@ -84,6 +91,6 @@ gcc -g -Og -c ${temp_dir}/testfile-main.c -o ${temp_dir}/testfile-main.o gcc -g -Og -o ${temp_dir}/testfile ${temp_dir}/testfile-foo.o ${temp_dir}/testfile-main.o perf probe -x ${temp_dir}/testfile --funcs foo | grep "foo" -perf probe -x ${temp_dir}/testfile foo +perf probe -x ${temp_dir}/testfile ${probe_name}=foo cleanup -- 2.56.0.rc1.315.gc6ed9934b7-goog ^ permalink raw reply [flat|nested] 91+ messages in thread
end of thread, other threads:[~2026-09-23 7:14 UTC | newest] Thread overview: 91+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-09-17 6:42 [PATCH v1 00/13] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-17 6:42 ` [PATCH v1 01/13] perf trace: Start BPF summary before starting workload Ian Rogers 2026-09-17 6:42 ` [PATCH v1 02/13] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers 2026-09-17 6:42 ` [PATCH v1 03/13] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers 2026-09-17 6:42 ` [PATCH v1 04/13] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers 2026-09-17 6:42 ` [PATCH v1 05/13] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers 2026-09-17 6:42 ` [PATCH v1 06/13] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers 2026-09-17 6:42 ` [PATCH v1 07/13] perf test common: Do not globally disable tracing events in clear_all_probes Ian Rogers 2026-09-17 6:42 ` [PATCH v1 08/13] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers 2026-09-17 6:42 ` [PATCH v1 09/13] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers 2026-09-17 6:42 ` [PATCH v1 10/13] perf test trace_summary: Improve error diagnostics Ian Rogers 2026-09-17 6:42 ` [PATCH v1 11/13] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers 2026-09-17 6:42 ` [PATCH v1 12/13] perf test trace_summary: Make non-exclusive Ian Rogers 2026-09-17 6:42 ` [PATCH v1 13/13] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 2026-09-17 16:38 ` [PATCH v2 00/14] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-17 16:38 ` [PATCH v2 01/14] perf trace: Include the headers declaring pid_t and strcmp Ian Rogers 2026-09-17 16:38 ` [PATCH v2 02/14] perf trace: Start BPF summary before starting workload Ian Rogers 2026-09-17 16:38 ` [PATCH v2 03/14] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers 2026-09-17 16:38 ` [PATCH v2 04/14] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers 2026-09-17 16:38 ` [PATCH v2 05/14] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers 2026-09-17 16:38 ` [PATCH v2 06/14] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers 2026-09-17 16:38 ` [PATCH v2 07/14] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers 2026-09-17 16:38 ` [PATCH v2 08/14] perf test common: Do not globally disable tracing events in clear_all_probes Ian Rogers 2026-09-17 16:38 ` [PATCH v2 09/14] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers 2026-09-17 16:38 ` [PATCH v2 10/14] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers 2026-09-17 16:38 ` [PATCH v2 11/14] perf test trace_summary: Improve error diagnostics Ian Rogers 2026-09-17 16:39 ` [PATCH v2 12/14] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers 2026-09-17 16:39 ` [PATCH v2 13/14] perf test trace_summary: Make non-exclusive Ian Rogers 2026-09-17 16:39 ` [PATCH v2 14/14] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 2026-09-18 14:06 ` [PATCH v3 00/16] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-18 14:06 ` [PATCH v3 01/16] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers 2026-09-18 14:06 ` [PATCH v3 02/16] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers 2026-09-18 14:06 ` [PATCH v3 03/16] perf trace: Start BPF summary before starting workload Ian Rogers 2026-09-18 14:06 ` [PATCH v3 04/16] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers 2026-09-18 14:06 ` [PATCH v3 05/16] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers 2026-09-18 14:06 ` [PATCH v3 06/16] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers 2026-09-18 14:06 ` [PATCH v3 07/16] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers 2026-09-18 14:06 ` [PATCH v3 08/16] perf trace: Enumerate the target again once BPF is attached Ian Rogers 2026-09-18 14:06 ` [PATCH v3 09/16] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers 2026-09-18 14:06 ` [PATCH v3 10/16] perf test common: Only disable probes in clear_all_probes Ian Rogers 2026-09-18 14:06 ` [PATCH v3 11/16] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers 2026-09-18 14:06 ` [PATCH v3 12/16] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers 2026-09-18 14:06 ` [PATCH v3 13/16] perf test trace_summary: Improve error diagnostics Ian Rogers 2026-09-18 14:06 ` [PATCH v3 14/16] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers 2026-09-18 14:06 ` [PATCH v3 15/16] perf test trace_summary: Make non-exclusive Ian Rogers 2026-09-18 14:06 ` [PATCH v3 16/16] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 2026-09-18 21:19 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Ian Rogers 2026-09-18 21:19 ` [PATCH v4 01/18] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers 2026-09-18 21:19 ` [PATCH v4 02/18] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers 2026-09-18 21:19 ` [PATCH v4 03/18] perf evsel: Report an allocation failure as ENOMEM when setting filters Ian Rogers 2026-09-18 21:19 ` [PATCH v4 04/18] perf trace: Start BPF summary before starting workload Ian Rogers 2026-09-18 21:19 ` [PATCH v4 05/18] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers 2026-09-18 21:19 ` [PATCH v4 06/18] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers 2026-09-18 21:19 ` [PATCH v4 07/18] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers 2026-09-18 21:19 ` [PATCH v4 08/18] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers 2026-09-18 21:19 ` [PATCH v4 09/18] perf trace: Enumerate the target again once BPF is attached Ian Rogers 2026-09-18 21:19 ` [PATCH v4 10/18] perf trace: Drop targets that died before they were filtered Ian Rogers 2026-09-18 21:19 ` [PATCH v4 11/18] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers 2026-09-18 21:19 ` [PATCH v4 12/18] perf test common: Only disable probes in clear_all_probes Ian Rogers 2026-09-18 21:19 ` [PATCH v4 13/18] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers 2026-09-18 21:19 ` [PATCH v4 14/18] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers 2026-09-18 21:19 ` [PATCH v4 15/18] perf test trace_summary: Improve error diagnostics Ian Rogers 2026-09-18 21:19 ` [PATCH v4 16/18] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers 2026-09-18 21:19 ` [PATCH v4 17/18] perf test trace_summary: Make non-exclusive Ian Rogers 2026-09-18 21:19 ` [PATCH v4 18/18] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers 2026-09-22 13:11 ` [PATCH v4 00/18] perf trace: Fix BPF filtering and make tracing tests non-exclusive Arnaldo Carvalho de Melo 2026-09-22 22:43 ` Ian Rogers 2026-09-23 7:13 ` [PATCH v5 00/23] " Ian Rogers 2026-09-23 7:13 ` [PATCH v5 01/23] perf trace: Set the augmented arg header in the augmenters that omit it Ian Rogers 2026-09-23 7:13 ` [PATCH v5 02/23] perf trace: Include the augmented arg header in nanosleep's payload length Ian Rogers 2026-09-23 7:13 ` [PATCH v5 03/23] perf trace: Include the headers declaring pid_t, strcmp and assert Ian Rogers 2026-09-23 7:13 ` [PATCH v5 04/23] perf trace: Free the whole evsel_trace in evsel__put_and_free_priv Ian Rogers 2026-09-23 7:13 ` [PATCH v5 05/23] perf evsel: Report an allocation failure as ENOMEM when setting filters Ian Rogers 2026-09-23 7:13 ` [PATCH v5 06/23] perf trace: Start BPF summary before starting workload Ian Rogers 2026-09-23 7:13 ` [PATCH v5 07/23] perf trace: Skip internal tracepoint fields in formatting and beauty map Ian Rogers 2026-09-23 7:13 ` [PATCH v5 08/23] perf trace: Bounds check augmented arguments before reading them Ian Rogers 2026-09-23 7:13 ` [PATCH v5 09/23] perf trace: Bound the fixed size augmented argument beautifiers Ian Rogers 2026-09-23 7:13 ` [PATCH v5 10/23] perf trace: Do not read sample padding as an augmented argument Ian Rogers 2026-09-23 7:13 ` [PATCH v5 11/23] perf trace: Do not set unaugmented BPF program on sys_exit map Ian Rogers 2026-09-23 7:13 ` [PATCH v5 12/23] perf trace: Filter events in BPF and avoid tracepoint vetoes Ian Rogers 2026-09-23 7:13 ` [PATCH v5 13/23] perf trace: Handle fork and exit directly in BPF filter maps Ian Rogers 2026-09-23 7:13 ` [PATCH v5 14/23] perf trace: Enumerate the target again once BPF is attached Ian Rogers 2026-09-23 7:13 ` [PATCH v5 15/23] perf trace: Drop targets that died before they were filtered Ian Rogers 2026-09-23 7:13 ` [PATCH v5 16/23] perf test test_task_analyzer: Isolate in temporary directory and make non-exclusive Ian Rogers 2026-09-23 7:13 ` [PATCH v5 17/23] perf test common: Only disable probes in clear_all_probes Ian Rogers 2026-09-23 7:13 ` [PATCH v5 18/23] perf test probe_vfs_getname: Scope probe name to PID and make non-exclusive Ian Rogers 2026-09-23 7:13 ` [PATCH v5 19/23] perf test record+probe_libc_inet_pton: Scope event to PID, add retries, " Ian Rogers 2026-09-23 7:14 ` [PATCH v5 20/23] perf test trace_summary: Improve error diagnostics Ian Rogers 2026-09-23 7:14 ` [PATCH v5 21/23] perf test trace_btf_general: Drop --max-events=1 and make non-exclusive Ian Rogers 2026-09-23 7:14 ` [PATCH v5 22/23] perf test trace_summary: Make non-exclusive Ian Rogers 2026-09-23 7:14 ` [PATCH v5 23/23] perf test uprobe_from_different_cu: Scope probe name to PID Ian Rogers
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®