From b6c4dd07150dfd2d984dc306bada8f2dd4a2a73e Mon Sep 17 00:00:00 2001 From: Martin Kaiser Date: Thu, 2 Jul 2026 21:12:39 +0900 Subject: [PATCH 01/37] tracing: trace_fprobe: fix typo in function name The function name should be __register_tracepoint_fprobe. Link: https://lore.kernel.org/all/20260507081041.885781-5-martin@kaiser.cx/ Signed-off-by: Martin Kaiser Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_fprobe.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel/trace/trace_fprobe.c b/kernel/trace/trace_fprobe.c index 9f5f08c0e7c2..4d1abbf66229 100644 --- a/kernel/trace/trace_fprobe.c +++ b/kernel/trace/trace_fprobe.c @@ -764,7 +764,7 @@ static int unregister_fprobe_event(struct trace_fprobe *tf) return trace_probe_unregister_event_call(&tf->tp); } -static int __regsiter_tracepoint_fprobe(struct trace_fprobe *tf) +static int __register_tracepoint_fprobe(struct trace_fprobe *tf) { struct tracepoint_user *tuser __free(tuser_put) = NULL; struct module *mod __free(module_put) = NULL; @@ -836,7 +836,7 @@ static int __register_trace_fprobe(struct trace_fprobe *tf) tf->fp.flags &= ~FPROBE_FL_DISABLED; if (trace_fprobe_is_tracepoint(tf)) - return __regsiter_tracepoint_fprobe(tf); + return __register_tracepoint_fprobe(tf); /* TODO: handle filter, nofilter or symbol list */ return register_fprobe(&tf->fp, tf->symbol, NULL); From 50133489c33764219a505ddb7e9b42cc6d8bfafa Mon Sep 17 00:00:00 2001 From: Yudistira Putra Date: Thu, 2 Jul 2026 21:12:39 +0900 Subject: [PATCH 02/37] tracing/probes: make file offset error message probe-agnostic The shared probe argument parser rejects file offsets for kernel probes. This path is used outside the kprobe event parser too, but the diagnostic currently says "with kprobe" even when emitted from another probe path. Make the diagnostic probe-agnostic. Link: https://lore.kernel.org/all/20260622160032.99834-1-pyudistira519@gmail.com/ Signed-off-by: Yudistira Putra Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 2 +- kernel/trace/trace_probe.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index d17cfee77d9c..9a58c0789ac5 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -1230,7 +1230,7 @@ parse_probe_arg(char *arg, const struct fetch_type *type, code->op = FETCH_OP_IMM; code->immediate = param; } else if (arg[1] == '+') { - /* kprobes don't support file offsets */ + /* Kernel probes do not support file offsets */ if (ctx->flags & TPARG_FL_KERNEL) { trace_probe_log_err(ctx->offset, FILE_ON_KPROBE); return -EINVAL; diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index 0f09f7aaf93f..43e6954c14c5 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -516,7 +516,7 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call, C(BAD_MEM_ADDR, "Invalid memory address"), \ C(BAD_IMM, "Invalid immediate value"), \ C(IMMSTR_NO_CLOSE, "String is not closed with '\"'"), \ - C(FILE_ON_KPROBE, "File offset is not available with kprobe"), \ + C(FILE_ON_KPROBE, "File offset is not available for kernel probes"), \ C(BAD_FILE_OFFS, "Invalid file offset value"), \ C(SYM_ON_UPROBE, "Symbol is not available with uprobe"), \ C(TOO_MANY_OPS, "Dereference is too much nested"), \ From 7e07baaa9e41c4140924a90f1c5cf8feeb9879fb Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:39 +0900 Subject: [PATCH 03/37] tracing/probes: Rename FETCH_OP_DATA to FETCH_OP_IMMSTR Since FETCH_OP_DATA is used solely to store immediate string values, rename it to the more specific FETCH_OP_IMMSTR. No behavior change, just rename it. Link: https://lore.kernel.org/all/178217906914.643090.2958799486090467704.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 12 ++++++------ kernel/trace/trace_probe.h | 2 +- kernel/trace/trace_probe_tmpl.h | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 9a58c0789ac5..348a7f97d6c7 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -1309,7 +1309,7 @@ parse_probe_arg(char *arg, const struct fetch_type *type, break; ctx->offset = cur_offs; if (code->op == FETCH_OP_COMM || - code->op == FETCH_OP_DATA) { + code->op == FETCH_OP_IMMSTR) { trace_probe_log_err(ctx->offset, COMM_CANT_DEREF); return -EINVAL; } @@ -1330,7 +1330,7 @@ parse_probe_arg(char *arg, const struct fetch_type *type, ret = __parse_imm_string(arg + 2, &tmp, ctx->offset + 2); if (ret) break; - code->op = FETCH_OP_DATA; + code->op = FETCH_OP_IMMSTR; code->data = tmp; } else { ret = str_to_immediate(arg + 1, &code->immediate); @@ -1485,7 +1485,7 @@ static int finalize_fetch_insn(struct fetch_insn *code, } else { if (code->op != FETCH_OP_DEREF && code->op != FETCH_OP_UDEREF && code->op != FETCH_OP_IMM && code->op != FETCH_OP_COMM && - code->op != FETCH_OP_DATA && code->op != FETCH_OP_TP_ARG) { + code->op != FETCH_OP_IMMSTR && code->op != FETCH_OP_TP_ARG) { trace_probe_log_err(ctx->offset + type_offset, BAD_STRING); return -EINVAL; @@ -1494,7 +1494,7 @@ static int finalize_fetch_insn(struct fetch_insn *code, if (!strcmp(parg->type->name, "symstr") || (code->op == FETCH_OP_IMM || code->op == FETCH_OP_COMM || - code->op == FETCH_OP_DATA) || code->op == FETCH_OP_TP_ARG || + code->op == FETCH_OP_IMMSTR) || code->op == FETCH_OP_TP_ARG || parg->count) { /* * IMM, DATA and COMM is pointing actual address, those @@ -1670,7 +1670,7 @@ static int traceprobe_parse_probe_arg_body(const char *argv, ssize_t *size, if (ret < 0) { for (code = tmp; code < tmp + FETCH_INSN_MAX; code++) if (code->op == FETCH_NOP_SYMBOL || - code->op == FETCH_OP_DATA) + code->op == FETCH_OP_IMMSTR) kfree(code->data); } kfree(tmp); @@ -1769,7 +1769,7 @@ void traceprobe_free_probe_arg(struct probe_arg *arg) while (code && code->op != FETCH_OP_END) { if (code->op == FETCH_NOP_SYMBOL || - code->op == FETCH_OP_DATA) + code->op == FETCH_OP_IMMSTR) kfree(code->data); code++; } diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index 43e6954c14c5..40b53b5b58a9 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -94,7 +94,7 @@ enum fetch_op { FETCH_OP_COMM, /* Current comm */ FETCH_OP_ARG, /* Function argument : .param */ FETCH_OP_FOFFS, /* File offset: .immediate */ - FETCH_OP_DATA, /* Allocated data: .data */ + FETCH_OP_IMMSTR, /* Allocated string: .data */ FETCH_OP_EDATA, /* Entry data: .offset */ // Stage 2 (dereference) op FETCH_OP_DEREF, /* Dereference: .offset */ diff --git a/kernel/trace/trace_probe_tmpl.h b/kernel/trace/trace_probe_tmpl.h index f39b37fcdb3b..51436f19083b 100644 --- a/kernel/trace/trace_probe_tmpl.h +++ b/kernel/trace/trace_probe_tmpl.h @@ -109,7 +109,7 @@ process_common_fetch_insn(struct fetch_insn *code, unsigned long *val) case FETCH_OP_COMM: *val = (unsigned long)current->comm; break; - case FETCH_OP_DATA: + case FETCH_OP_IMMSTR: *val = (unsigned long)code->data; break; default: From 823b37855829bc328d46102a56e4d0b2f7a3d0d1 Mon Sep 17 00:00:00 2001 From: Martin Kaiser Date: Thu, 2 Jul 2026 21:12:40 +0900 Subject: [PATCH 04/37] tracing/probes: ignore id update from btf_type_skip_modifiers We can pass NULL as id pointer to btf_type_skip_modifiers if we do not need the id of the returned btf_type. Link: https://lore.kernel.org/all/20260623132937.3494895-1-martin@kaiser.cx/ Signed-off-by: Martin Kaiser Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 348a7f97d6c7..0da7c0b53ba7 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -356,9 +356,8 @@ static bool btf_type_is_char_ptr(struct btf *btf, const struct btf_type *type) { const struct btf_type *real_type; u32 intdata; - s32 tid; - real_type = btf_type_skip_modifiers(btf, type->type, &tid); + real_type = btf_type_skip_modifiers(btf, type->type, NULL); if (!real_type) return false; @@ -375,14 +374,13 @@ static bool btf_type_is_char_array(struct btf *btf, const struct btf_type *type) const struct btf_type *real_type; const struct btf_array *array; u32 intdata; - s32 tid; if (BTF_INFO_KIND(type->info) != BTF_KIND_ARRAY) return false; array = (const struct btf_array *)(type + 1); - real_type = btf_type_skip_modifiers(btf, array->type, &tid); + real_type = btf_type_skip_modifiers(btf, array->type, NULL); intdata = btf_type_int(real_type); return !(BTF_INT_ENCODING(intdata) & BTF_INT_SIGNED) @@ -585,7 +583,6 @@ static int parse_btf_field(char *fieldname, const struct btf_type *type, struct btf *btf = ctx_btf(ctx); char *next; int is_ptr; - s32 tid; do { if (!is_struct) { @@ -596,7 +593,7 @@ static int parse_btf_field(char *fieldname, const struct btf_type *type, } /* Convert a struct pointer type to a struct type */ - type = btf_type_skip_modifiers(btf, type->type, &tid); + type = btf_type_skip_modifiers(btf, type->type, NULL); if (!type) { trace_probe_log_err(ctx->offset, BAD_BTF_TID); return -EINVAL; @@ -636,7 +633,7 @@ static int parse_btf_field(char *fieldname, const struct btf_type *type, ctx->last_bitsize = 0; } - type = btf_type_skip_modifiers(btf, field->type, &tid); + type = btf_type_skip_modifiers(btf, field->type, NULL); if (!type) { trace_probe_log_err(ctx->offset, BAD_BTF_TID); return -EINVAL; @@ -755,7 +752,7 @@ static int parse_btf_arg(char *varname, return -ENOENT; found: - type = btf_type_skip_modifiers(ctx->btf, tid, &tid); + type = btf_type_skip_modifiers(ctx->btf, tid, NULL); found_type: if (!type) { trace_probe_log_err(ctx->offset, BAD_BTF_TID); From ffd9dada630d9149fe0572efea00e1d99981bc50 Mon Sep 17 00:00:00 2001 From: Thomas Huth Date: Thu, 2 Jul 2026 21:12:40 +0900 Subject: [PATCH 05/37] kprobes: Replace __ASSEMBLY__ with __ASSEMBLER__ in header file While the GCC and Clang compilers already define __ASSEMBLER__ automatically when compiling assembly code, __ASSEMBLY__ is a macro that only gets defined by the Makefiles in the kernel. This can be very confusing when switching between userspace and kernelspace coding, or when dealing with uapi headers that rather should use __ASSEMBLER__ instead. So let's standardize now on the __ASSEMBLER__ macro that is provided by the compilers. Link: https://lore.kernel.org/all/20260619161434.88270-1-thuth@redhat.com/ Signed-off-by: Thomas Huth Signed-off-by: Masami Hiramatsu (Google) --- include/asm-generic/kprobes.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/asm-generic/kprobes.h b/include/asm-generic/kprobes.h index 5290a2b2e15a..16f16963d503 100644 --- a/include/asm-generic/kprobes.h +++ b/include/asm-generic/kprobes.h @@ -2,7 +2,7 @@ #ifndef _ASM_GENERIC_KPROBES_H #define _ASM_GENERIC_KPROBES_H -#if defined(__KERNEL__) && !defined(__ASSEMBLY__) +#if defined(__KERNEL__) && !defined(__ASSEMBLER__) #ifdef CONFIG_KPROBES /* * Blacklist ganerating macro. Specify functions which is not probed @@ -21,6 +21,6 @@ static unsigned long __used \ # define __kprobes # define nokprobe_inline inline #endif -#endif /* defined(__KERNEL__) && !defined(__ASSEMBLY__) */ +#endif /* defined(__KERNEL__) && !defined(__ASSEMBLER__) */ #endif /* _ASM_GENERIC_KPROBES_H */ From 427c240a027fca465e4c339ae08584632b525bfa Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:40 +0900 Subject: [PATCH 06/37] tracing/probes: Allow eprobe to use variable without $ prefix The commit 69efd863a785 ("tracing/eprobes: Allow use of BTF names to dereference pointers") allows eprobe to use event field without "$" prefix when it is used with typecast, it is natual to allow it without typecast. Link: https://lore.kernel.org/all/178271358117.1176915.13520279547897961254.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 12 +++++++++++- kernel/trace/trace_probe.h | 1 + .../ftrace/test.d/dynevent/eprobes_syntax_errors.tc | 3 +-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 0da7c0b53ba7..2ce7d62471cb 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -1341,7 +1341,17 @@ parse_probe_arg(char *arg, const struct fetch_type *type, ret = handle_typecast(arg, pcode, end, ctx); break; default: - if (isalpha(arg[0]) || arg[0] == '_') { /* BTF variable */ + if (isalpha(arg[0]) || arg[0] == '_') { + /* BTF variable or event field*/ + if (ctx->flags & TPARG_FL_TEVENT) { + ret = parse_trace_event(arg, *pcode, ctx); + if (ret < 0) { + trace_probe_log_err(ctx->offset, + NO_EVENT_FIELD); + return -EINVAL; + } + break; + } if (!tparg_is_function_entry(ctx->flags) && !tparg_is_function_return(ctx->flags)) { trace_probe_log_err(ctx->offset, NOSUP_BTFARG); diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index 40b53b5b58a9..2e0d8384ee5c 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -559,6 +559,7 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call, C(NO_PTR_STRCT, "This is not a pointer to union/structure."), \ C(NOSUP_DAT_ARG, "Non pointer structure/union argument is not supported."),\ C(BAD_HYPHEN, "Failed to parse single hyphen. Forgot '>'?"), \ + C(NO_EVENT_FIELD, "This event field is not found."), \ C(NO_BTF_FIELD, "This field is not found."), \ C(BAD_BTF_TID, "Failed to get BTF type info."),\ C(BAD_TYPE4STR, "This type does not fit for string."),\ diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc index 2a680c086047..0e65e787e426 100644 --- a/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc @@ -10,7 +10,7 @@ check_error() { # command-with-error-pos-by-^ check_error 'e ^a.' # NO_EVENT_INFO check_error 'e ^.b' # NO_EVENT_INFO check_error 'e ^a.b' # BAD_ATTACH_EVENT -check_error 'e syscalls/sys_enter_openat ^foo' # BAD_ATTACH_ARG +check_error 'e syscalls/sys_enter_openat ^foo' # NO_EVENT_FIELD check_error 'e:^/bar syscalls/sys_enter_openat' # NO_GROUP_NAME check_error 'e:^12345678901234567890123456789012345678901234567890123456789012345/bar syscalls/sys_enter_openat' # GROUP_TOO_LONG @@ -19,7 +19,6 @@ check_error 'e:^ syscalls/sys_enter_openat' # NO_EVENT_NAME check_error 'e:foo/^12345678901234567890123456789012345678901234567890123456789012345 syscalls/sys_enter_openat' # EVENT_TOO_LONG check_error 'e:foo/^bar.1 syscalls/sys_enter_openat' # BAD_EVENT_NAME -check_error 'e:foo/bar syscalls/sys_enter_openat arg=^dfd' # BAD_FETCH_ARG check_error 'e:foo/bar syscalls/sys_enter_openat arg=^$foo' # BAD_ATTACH_ARG if grep -q '\..*\[if \]' README; then From dccddc39aa7d0167e16e9fb94942fd37a30d581b Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:40 +0900 Subject: [PATCH 07/37] tracing/probes: Support dumping fetcharg program for debugging dynamic events For debugging probe events, it is helpful to verify the compiled fetch instructions for each probe argument. This introduces a new kernel config CONFIG_PROBE_EVENTS_DUMP_FETCHARG to decode the instruction sequence of each argument and display it under a commented line starting with '#' immediately following the dynamic event definition (such as in dynamic_events, kprobe_events, uprobe_events, etc.). For example: /sys/kernel/tracing # cat dynamic_events p:kprobes/p_vfs_read_0 vfs_read arg1=+0(file):ustring arg2=%ax:x16 # arg1: ARG(0) -> ST_USTRING(offset=0,size=4) -> END # arg2: REG(80) -> ST_RAW(size=2) -> END Link: https://lore.kernel.org/all/178271359508.1176915.11895926894608440183.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/Kconfig | 12 +++++ kernel/trace/trace_eprobe.c | 2 + kernel/trace/trace_fprobe.c | 2 + kernel/trace/trace_kprobe.c | 2 + kernel/trace/trace_probe.c | 96 +++++++++++++++++++++++++++++++++++++ kernel/trace/trace_probe.h | 79 +++++++++++++++++------------- kernel/trace/trace_uprobe.c | 3 ++ 7 files changed, 164 insertions(+), 32 deletions(-) diff --git a/kernel/trace/Kconfig b/kernel/trace/Kconfig index 084f34dc6c9f..0ab5916575a9 100644 --- a/kernel/trace/Kconfig +++ b/kernel/trace/Kconfig @@ -779,6 +779,18 @@ config PROBE_EVENTS_BTF_ARGS kernel function entry or a tracepoint. This is available only if BTF (BPF Type Format) support is enabled. +config PROBE_EVENTS_DUMP_FETCHARG + bool "Dump of dynamic probe event fetch-arguments" + depends on PROBE_EVENTS + default n + help + This shows the dump of fetch-arguments of dynamic probe events + alongside their event definitions in the dynamic_events file + as comment lines. This is useful to debug the probe events. + Since this exposes the raw values in the dynamic_events file, + it might be a security risk. Only enable it if you need to debug + probe events themselves. + config KPROBE_EVENTS depends on KPROBES depends on HAVE_REGS_AND_STACK_ACCESS_API diff --git a/kernel/trace/trace_eprobe.c b/kernel/trace/trace_eprobe.c index 50518b071414..462c31145733 100644 --- a/kernel/trace/trace_eprobe.c +++ b/kernel/trace/trace_eprobe.c @@ -87,6 +87,8 @@ static int eprobe_dyn_event_show(struct seq_file *m, struct dyn_event *ev) seq_printf(m, " %s=%s", ep->tp.args[i].name, ep->tp.args[i].comm); seq_putc(m, '\n'); + trace_probe_dump_args(m, &ep->tp); + return 0; } diff --git a/kernel/trace/trace_fprobe.c b/kernel/trace/trace_fprobe.c index 4d1abbf66229..536781cd4c47 100644 --- a/kernel/trace/trace_fprobe.c +++ b/kernel/trace/trace_fprobe.c @@ -1449,6 +1449,8 @@ static int trace_fprobe_show(struct seq_file *m, struct dyn_event *ev) seq_printf(m, " %s=%s", tf->tp.args[i].name, tf->tp.args[i].comm); seq_putc(m, '\n'); + trace_probe_dump_args(m, &tf->tp); + return 0; } diff --git a/kernel/trace/trace_kprobe.c b/kernel/trace/trace_kprobe.c index a8420e6abb56..cfa807d8e760 100644 --- a/kernel/trace/trace_kprobe.c +++ b/kernel/trace/trace_kprobe.c @@ -1320,6 +1320,8 @@ static int trace_kprobe_show(struct seq_file *m, struct dyn_event *ev) seq_printf(m, " %s=%s", tk->tp.args[i].name, tk->tp.args[i].comm); seq_putc(m, '\n'); + trace_probe_dump_args(m, &tk->tp); + return 0; } diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 2ce7d62471cb..0908019aea12 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -2403,3 +2403,99 @@ int trace_probe_print_args(struct trace_seq *s, struct probe_arg *args, int nr_a } return 0; } + +#ifdef CONFIG_PROBE_EVENTS_DUMP_FETCHARG + +struct fetch_op_decode { + const char *name; + void (*decode)(struct seq_file *m, struct fetch_insn *insn); +}; + +static const struct fetch_op_decode fetch_op_decode[]; + +static void fetcharg_decode_none(struct seq_file *m, struct fetch_insn *insn) +{ + seq_puts(m, fetch_op_decode[insn->op].name); +} + +static void fetcharg_decode_param(struct seq_file *m, struct fetch_insn *insn) +{ + seq_printf(m, "%s(%u)", fetch_op_decode[insn->op].name, insn->param); +} + +static void fetcharg_decode_imm(struct seq_file *m, struct fetch_insn *insn) +{ + seq_printf(m, "%s(0x%lx)", fetch_op_decode[insn->op].name, insn->immediate); +} + +static void fetcharg_decode_string(struct seq_file *m, struct fetch_insn *insn) +{ + seq_printf(m, "%s(%s)", fetch_op_decode[insn->op].name, (char *)insn->data); +} + +static void fetcharg_decode_symbol(struct seq_file *m, struct fetch_insn *insn) +{ + seq_printf(m, "%s(%s)", fetch_op_decode[insn->op].name, (char *)insn->data); +} + +static void fetcharg_decode_offset(struct seq_file *m, struct fetch_insn *insn) +{ + seq_printf(m, "%s(offset=%d)", fetch_op_decode[insn->op].name, insn->offset); +} + +static void fetcharg_decode_store(struct seq_file *m, struct fetch_insn *insn) +{ + if (insn->op == FETCH_OP_ST_RAW) + seq_printf(m, "%s(size=%u)", fetch_op_decode[insn->op].name, insn->size); + else + seq_printf(m, "%s(offset=%d,size=%u)", fetch_op_decode[insn->op].name, + insn->offset, insn->size); +} + +static void fetcharg_decode_bf(struct seq_file *m, struct fetch_insn *insn) +{ + seq_printf(m, "%s(basesize=%u,lshift=%u,rshift=%u)", + fetch_op_decode[insn->op].name, insn->basesize, insn->lshift, insn->rshift); +} + +static void fetcharg_decode_tp_arg(struct seq_file *m, struct fetch_insn *insn) +{ + struct ftrace_event_field *field = insn->data; + + seq_printf(m, "%s(%s)", fetch_op_decode[insn->op].name, field->name); +} + +#define FETCH_OP(opname, decode_fn) \ + [FETCH_OP_##opname] = { .name = #opname, .decode = fetcharg_decode_##decode_fn } + +static const struct fetch_op_decode fetch_op_decode[] = FETCH_OP_LIST; +#undef FETCH_OP + +static void trace_probe_dump_arg(struct seq_file *m, struct probe_arg *parg) +{ + int i; + + seq_printf(m, "# %s: ", parg->name); + for (i = 0; i < FETCH_INSN_MAX; i++) { + struct fetch_insn *insn = parg->code + i; + + if (insn->op >= ARRAY_SIZE(fetch_op_decode) || !fetch_op_decode[insn->op].decode) + seq_printf(m, "unknown(%d)", insn->op); + else + fetch_op_decode[insn->op].decode(m, insn); + + if (insn->op == FETCH_OP_END) + break; + seq_puts(m, " -> "); + } + seq_putc(m, '\n'); +} + +void trace_probe_dump_args(struct seq_file *m, struct trace_probe *tp) +{ + int i; + + for (i = 0; i < tp->nr_args; i++) + trace_probe_dump_arg(m, &tp->args[i]); +} +#endif /* CONFIG_PROBE_EVENTS_DUMP_FETCHARG */ diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index 2e0d8384ee5c..e36cfe39e9a8 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -83,38 +83,46 @@ static nokprobe_inline u32 update_data_loc(u32 loc, int consumed) /* Printing function type */ typedef int (*print_type_func_t)(struct trace_seq *, void *, void *); -enum fetch_op { - FETCH_OP_NOP = 0, - // Stage 1 (load) ops - FETCH_OP_REG, /* Register : .param = offset */ - FETCH_OP_STACK, /* Stack : .param = index */ - FETCH_OP_STACKP, /* Stack pointer */ - FETCH_OP_RETVAL, /* Return value */ - FETCH_OP_IMM, /* Immediate : .immediate */ - FETCH_OP_COMM, /* Current comm */ - FETCH_OP_ARG, /* Function argument : .param */ - FETCH_OP_FOFFS, /* File offset: .immediate */ - FETCH_OP_IMMSTR, /* Allocated string: .data */ - FETCH_OP_EDATA, /* Entry data: .offset */ - // Stage 2 (dereference) op - FETCH_OP_DEREF, /* Dereference: .offset */ - FETCH_OP_UDEREF, /* User-space Dereference: .offset */ - // Stage 3 (store) ops - FETCH_OP_ST_RAW, /* Raw: .size */ - FETCH_OP_ST_MEM, /* Mem: .offset, .size */ - FETCH_OP_ST_UMEM, /* Mem: .offset, .size */ - FETCH_OP_ST_STRING, /* String: .offset, .size */ - FETCH_OP_ST_USTRING, /* User String: .offset, .size */ - FETCH_OP_ST_SYMSTR, /* Kernel Symbol String: .offset, .size */ - FETCH_OP_ST_EDATA, /* Store Entry Data: .offset */ - // Stage 4 (modify) op - FETCH_OP_MOD_BF, /* Bitfield: .basesize, .lshift, .rshift */ - // Stage 5 (loop) op - FETCH_OP_LP_ARRAY, /* Array: .param = loop count */ - FETCH_OP_TP_ARG, /* Trace Point argument */ - FETCH_OP_END, - FETCH_NOP_SYMBOL, /* Unresolved Symbol holder */ -}; +#define FETCH_OP_LIST { \ + /* Stage 1 (load) ops */ \ + FETCH_OP(NOP, none), /* NOP */ \ + FETCH_OP(REG, param), /* Register: .param = offset */ \ + FETCH_OP(STACK, param), /* Stack: .param = index */ \ + FETCH_OP(STACKP, none), /* Stack pointer */ \ + FETCH_OP(RETVAL, none), /* Return value */ \ + FETCH_OP(IMM, imm), /* Immediate: .immediate */ \ + FETCH_OP(COMM, none), /* Current comm */ \ + FETCH_OP(ARG, param), /* Argument: .param = index */ \ + FETCH_OP(FOFFS, imm), /* File offset: .immediate */ \ + FETCH_OP(IMMSTR, string), /* Allocated string: .data */ \ + FETCH_OP(EDATA, offset), /* Entry data: .offset */ \ + FETCH_OP(TP_ARG, tp_arg), /* Tracepoint argument: .data */\ + /* Stage 2 (dereference) ops */ \ + FETCH_OP(DEREF, offset), /* Dereference: .offset */ \ + FETCH_OP(UDEREF, offset), /* User-space dereference: .offset */\ + /* Stage 3 (store) ops */ \ + FETCH_OP(ST_RAW, store), /* Raw value: .size */ \ + FETCH_OP(ST_MEM, store), /* Memory: .offset, .size */ \ + FETCH_OP(ST_UMEM, store), /* User memory: .offset, .size */\ + FETCH_OP(ST_STRING, store), /* String: .offset, .size */ \ + FETCH_OP(ST_USTRING, store), /* User string: .offset, .size */\ + FETCH_OP(ST_SYMSTR, store), /* Symbol name: .offset, .size */\ + FETCH_OP(ST_EDATA, offset), /* Entry data: .offset */ \ + /* Stage 4 (modify) op */ \ + FETCH_OP(MOD_BF, bf), /* Bitfield: .basesize, .lshift, .rshift*/\ + /* Stage 5 (loop) op */ \ + FETCH_OP(LP_ARRAY, param), /* Loop array: .param = count */\ + /* End */ \ + FETCH_OP(END, none), \ + /* Unresolved Symbol holder */ \ + FETCH_OP(NOP_SYMBOL, symbol), /* Non loaded symbol: .data = symbol name */\ +} + +#define FETCH_OP(opname, decode_fn) FETCH_OP_##opname +enum fetch_op FETCH_OP_LIST; +#undef FETCH_OP + +#define FETCH_NOP_SYMBOL FETCH_OP_NOP_SYMBOL struct fetch_insn { enum fetch_op op; @@ -370,6 +378,13 @@ bool trace_probe_match_command_args(struct trace_probe *tp, int trace_probe_create(const char *raw_command, int (*createfn)(int, const char **)); int trace_probe_print_args(struct trace_seq *s, struct probe_arg *args, int nr_args, u8 *data, void *field); +#ifdef CONFIG_PROBE_EVENTS_DUMP_FETCHARG +void trace_probe_dump_args(struct seq_file *m, struct trace_probe *tp); +#else +static inline void trace_probe_dump_args(struct seq_file *m, struct trace_probe *tp) +{ +} +#endif #ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API int traceprobe_get_entry_data_size(struct trace_probe *tp); diff --git a/kernel/trace/trace_uprobe.c b/kernel/trace/trace_uprobe.c index c274346853d1..b2e264a4b96c 100644 --- a/kernel/trace/trace_uprobe.c +++ b/kernel/trace/trace_uprobe.c @@ -765,6 +765,9 @@ static int trace_uprobe_show(struct seq_file *m, struct dyn_event *ev) seq_printf(m, " %s=%s", tu->tp.args[i].name, tu->tp.args[i].comm); seq_putc(m, '\n'); + + trace_probe_dump_args(m, &tu->tp); + return 0; } From 777bcbd2ac7d0af949d9f9741a93e939b81b04e7 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:40 +0900 Subject: [PATCH 08/37] tools/bootconfig: Ignore comment lines in dynamic_events/kprobe_events file Since dynamic_events/kprobe_events files show the fetcharg debug information as comment lines, its reader needs to ignore it. Link: https://lore.kernel.org/all/178271360563.1176915.423090353815990910.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- tools/bootconfig/scripts/ftrace2bconf.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/bootconfig/scripts/ftrace2bconf.sh b/tools/bootconfig/scripts/ftrace2bconf.sh index 1603801cf126..8eed445c295e 100755 --- a/tools/bootconfig/scripts/ftrace2bconf.sh +++ b/tools/bootconfig/scripts/ftrace2bconf.sh @@ -57,6 +57,8 @@ EOF kprobe_event_options() { cat $TRACEFS/kprobe_events | while read p args; do case $p in + \#*) + continue;; r*) cat 1>&2 << EOF # WARN: A return probe found but it is not supported by bootconfig. Skip it. From 438a8d6a82931f12305f80bc3e7ef067c2ac0551 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:40 +0900 Subject: [PATCH 09/37] tracing/probes: Support typecast for various probe events Support BTF typecast feature on other probe events, but only if it is kernel function entry or return, and must use function parameter name or $retval. This means you can do: (STRUCT)PARAM->MEMBER Note: you can not use other variables like $stackN, %reg etc. That needs nesting support. To support other probe events, we just need to use last_struct type when we find a function parameter in parse_btf_arg(). This also updates /README file to show struct typecast. Link: https://lore.kernel.org/all/178271362928.1176915.12235759508786922490.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- Documentation/trace/fprobetrace.rst | 3 +++ Documentation/trace/kprobetrace.rst | 4 ++++ kernel/trace/trace.c | 2 +- kernel/trace/trace_probe.c | 23 +++++++++++++++++------ kernel/trace/trace_probe.h | 5 +++++ 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Documentation/trace/fprobetrace.rst b/Documentation/trace/fprobetrace.rst index b4c2ca3d02c1..7435ded2d66d 100644 --- a/Documentation/trace/fprobetrace.rst +++ b/Documentation/trace/fprobetrace.rst @@ -57,6 +57,9 @@ Synopsis of fprobe-events (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types (x8/x16/x32/x64), "char", "string", "ustring", "symbol", "symstr" and bitfield are supported. + (STRUCT)FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to + a pointer to STRUCT and then derference the pointer defined by + ->MEMBER. (\*1) This is available only when BTF is enabled. (\*2) only for the probe on function entry (offs == 0). Note, this argument access diff --git a/Documentation/trace/kprobetrace.rst b/Documentation/trace/kprobetrace.rst index 3b6791c17e9b..f73614997d52 100644 --- a/Documentation/trace/kprobetrace.rst +++ b/Documentation/trace/kprobetrace.rst @@ -61,6 +61,10 @@ Synopsis of kprobe_events (x8/x16/x32/x64), VFS layer common type(%pd/%pD), "char", "string", "ustring", "symbol", "symstr" and bitfield are supported. + (STRUCT)FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to + a pointer to STRUCT and then derference the pointer defined by + ->MEMBER. Note that this is available only when the probe is + on function entry. (\*1) only for the probe on function entry (offs == 0). Note, this argument access is best effort, because depending on the argument type, it may be passed on diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c index 1146b83b711a..280a3dccd13f 100644 --- a/kernel/trace/trace.c +++ b/kernel/trace/trace.c @@ -4322,7 +4322,7 @@ static const char readme_msg[] = #ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API "\t $stack, $stack, $retval, $comm, $arg,\n" #ifdef CONFIG_PROBE_EVENTS_BTF_ARGS - "\t [->field[->field|.field...]],\n" + "\t [(structname)][->field[->field|.field...]],\n" #endif #else "\t $stack, $stack, $retval, $comm,\n" diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 0908019aea12..e6cc9f3d6c8b 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -699,7 +699,7 @@ static int parse_btf_arg(char *varname, if (ctx->flags & TPARG_FL_RETURN && !strcmp(varname, "$retval")) { code->op = FETCH_OP_RETVAL; - /* Check whether the function return type is not void */ + /* Check whether the function return type is not void, even with typecast. */ if (query_btf_context(ctx) == 0) { if (ctx->proto->type == 0) { trace_probe_log_err(ctx->offset, NO_RETVAL); @@ -708,6 +708,13 @@ static int parse_btf_arg(char *varname, tid = ctx->proto->type; goto found; } + /* + * Even if we can not find appropriate BTF info, we can still access + * the field via typecast. + */ + if (ctx->struct_btf) + goto found; + if (field) { trace_probe_log_err(ctx->offset + field - varname, NO_BTF_ENTRY); @@ -752,7 +759,10 @@ static int parse_btf_arg(char *varname, return -ENOENT; found: - type = btf_type_skip_modifiers(ctx->btf, tid, NULL); + if (ctx->struct_btf) + type = ctx->last_struct; + else + type = btf_type_skip_modifiers(ctx->btf, tid, NULL); found_type: if (!type) { trace_probe_log_err(ctx->offset, BAD_BTF_TID); @@ -829,10 +839,11 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, char *tmp; int ret; - /* Currently this only works for eprobes */ - if (!(ctx->flags & TPARG_FL_TEVENT)) { - trace_probe_log_err(ctx->offset, TYPECAST_NOT_EVENT); - return -EINVAL; + if (!(tparg_is_event_probe(ctx->flags) || + tparg_is_function_entry(ctx->flags) || + tparg_is_function_return(ctx->flags))) { + trace_probe_log_err(ctx->offset, NOSUP_BTFARG); + return -EOPNOTSUPP; } tmp = strchr(arg, ')'); diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index e36cfe39e9a8..aa72e2ffdd93 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -429,6 +429,11 @@ static inline bool tparg_is_function_return(unsigned int flags) return (flags & TPARG_FL_LOC_MASK) == (TPARG_FL_KERNEL | TPARG_FL_RETURN); } +static inline bool tparg_is_event_probe(unsigned int flags) +{ + return !!(flags & TPARG_FL_TEVENT); +} + struct traceprobe_parse_context { struct trace_event_call *event; /* BTF related parameters */ From 60432df556d5876d0c6451a9a4c5c01c513f19e1 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:41 +0900 Subject: [PATCH 10/37] tracing/probes: Support nested typecast When we hit an open parenthesis right after typecast closing parenthesis, it means we have nested typecast. This allows us to typecast a generic data member in a structure to a pointer to another structure. For example, to cast a DATA_MEMBER of VAR structure to STRUCT pointer and get MEMBER value. (STRUCT)(VAR->DATA_MEMBER)->MEMBER Also, we can nest typecast. (STRUCT1)((STRUCT2)$ARG->FIELD2)->FIELD1 Currently the max nest level is limited to 3. This also allows user to use typecasting for registers or stacks on kprobe events. e.g. (STRUCT)(%ax)->MEMBER (STRUCT)($stack0)->MEMBER Link: https://lore.kernel.org/all/178271363855.1176915.16793301788257446529.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- Documentation/trace/eprobetrace.rst | 2 + Documentation/trace/fprobetrace.rst | 2 + Documentation/trace/kprobetrace.rst | 2 + kernel/trace/trace.c | 1 + kernel/trace/trace_probe.c | 83 ++++++++++++++++++++++++++--- kernel/trace/trace_probe.h | 7 ++- 6 files changed, 88 insertions(+), 9 deletions(-) diff --git a/Documentation/trace/eprobetrace.rst b/Documentation/trace/eprobetrace.rst index fe3602540569..cd0b4aa7f896 100644 --- a/Documentation/trace/eprobetrace.rst +++ b/Documentation/trace/eprobetrace.rst @@ -50,6 +50,8 @@ Synopsis of eprobe_events a pointer to STRUCT and then derference the pointer defined by ->MEMBER. Note that when this is used, the FIELD name does not need to be prefixed with a '$'. + (STRUCT)(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can + also be used with another FETCHARG instead of FIELD. Types ----- diff --git a/Documentation/trace/fprobetrace.rst b/Documentation/trace/fprobetrace.rst index 7435ded2d66d..6b8bb27bb62d 100644 --- a/Documentation/trace/fprobetrace.rst +++ b/Documentation/trace/fprobetrace.rst @@ -60,6 +60,8 @@ Synopsis of fprobe-events (STRUCT)FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to a pointer to STRUCT and then derference the pointer defined by ->MEMBER. + (STRUCT)(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can + also be used with another FETCHARG instead of FIELD. (\*1) This is available only when BTF is enabled. (\*2) only for the probe on function entry (offs == 0). Note, this argument access diff --git a/Documentation/trace/kprobetrace.rst b/Documentation/trace/kprobetrace.rst index f73614997d52..c4382765d5b2 100644 --- a/Documentation/trace/kprobetrace.rst +++ b/Documentation/trace/kprobetrace.rst @@ -65,6 +65,8 @@ Synopsis of kprobe_events a pointer to STRUCT and then derference the pointer defined by ->MEMBER. Note that this is available only when the probe is on function entry. + (STRUCT)(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can + also be used with another FETCHARG instead of FIELD. (\*1) only for the probe on function entry (offs == 0). Note, this argument access is best effort, because depending on the argument type, it may be passed on diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c index 280a3dccd13f..e56ee034c486 100644 --- a/kernel/trace/trace.c +++ b/kernel/trace/trace.c @@ -4323,6 +4323,7 @@ static const char readme_msg[] = "\t $stack, $stack, $retval, $comm, $arg,\n" #ifdef CONFIG_PROBE_EVENTS_BTF_ARGS "\t [(structname)][->field[->field|.field...]],\n" + "\t [(structname)](fetcharg)->field[->field|.field...],\n" #endif #else "\t $stack, $stack, $retval, $comm,\n" diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index e6cc9f3d6c8b..827ae04f6351 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -832,10 +832,35 @@ static int query_btf_struct(const char *sname, struct traceprobe_parse_context * return 0; } +/* Find the matching closing parenthesis for a given opening parenthesis. */ +static char *find_matched_close_paren(char *s) +{ + char *p = s; + int count = 0; + + while (*p) { + if (*p == '(') + count++; + else if (*p == ')') { + if (--count == 0) + return p; + } + p++; + } + return NULL; +} + +static int +parse_probe_arg(char *arg, const struct fetch_type *type, + struct fetch_insn **pcode, struct fetch_insn *end, + struct traceprobe_parse_context *ctx); + static int handle_typecast(char *arg, struct fetch_insn **pcode, struct fetch_insn *end, struct traceprobe_parse_context *ctx) { + int orig_offset = ctx->offset; + bool nested = false; char *tmp; int ret; @@ -852,19 +877,56 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, DEREF_OPEN_BRACE); return -EINVAL; } - *tmp = '\0'; - ret = query_btf_struct(arg + 1, ctx); - *tmp = ')'; + *tmp++ = '\0'; + /* Handle the nested structure like (STRUCT)(VAR->FIELD)->... */ + if (*tmp == '(') { + char *close = find_matched_close_paren(tmp); + + ctx->offset += tmp - arg; + if (!close) { + trace_probe_log_err(ctx->offset, DEREF_OPEN_BRACE); + return -EINVAL; + } + /* We expect a field access for typecast */ + if (close[1] != '-' || close[2] != '>') { + trace_probe_log_err(ctx->offset + close - tmp + 1, + TYPECAST_REQ_FIELD); + return -EINVAL; + } + + ctx->nested_level++; + if (ctx->nested_level > TRACEPROBE_MAX_NESTED_LEVEL) { + trace_probe_log_err(ctx->offset, TOO_MANY_NESTED); + return -E2BIG; + } + *close = '\0'; + + ctx->offset += 1; /* for the '(' */ + /* We need to parse the nested one */ + ret = parse_probe_arg(tmp + 1, find_fetch_type(NULL, ctx->flags), + pcode, end, ctx); + if (ret < 0) + return ret; + ctx->nested_level--; + clear_struct_btf(ctx); + + tmp = close + 3;/* Skip "->" after closing parenthesis */ + nested = true; + } + + ret = query_btf_struct(arg + 1, ctx); if (ret < 0) { - trace_probe_log_err(ctx->offset + 1, NO_PTR_STRCT); + trace_probe_log_err(orig_offset + 1, NO_PTR_STRCT); return -EINVAL; } - tmp++; - - ctx->offset += tmp - arg; - ret = parse_btf_arg(tmp, pcode, end, ctx); + ctx->offset = orig_offset + tmp - arg; + /* If it is nested, tmp points to the field name. */ + if (nested) + ret = parse_btf_field(tmp, ctx->last_struct, pcode, end, ctx); + else + ret = parse_btf_arg(tmp, pcode, end, ctx); return ret; } @@ -1638,6 +1700,11 @@ static int traceprobe_parse_probe_arg_body(const char *argv, ssize_t *size, ctx); if (ret < 0) goto fail; + /* nested_level must be 0 here, otherwise there is a bug. */ + if (WARN_ON_ONCE(ctx->nested_level)) { + ret = -EINVAL; + goto fail; + } /* Update storing type if BTF is available */ if (IS_ENABLED(CONFIG_PROBE_EVENTS_BTF_ARGS) && diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index aa72e2ffdd93..7d71925244e8 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -450,8 +450,11 @@ struct traceprobe_parse_context { struct trace_probe *tp; unsigned int flags; int offset; + int nested_level; }; +#define TRACEPROBE_MAX_NESTED_LEVEL 3 + extern int traceprobe_parse_probe_arg(struct trace_probe *tp, int i, const char *argv, struct traceprobe_parse_context *ctx); @@ -587,7 +590,9 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call, C(TOO_MANY_ARGS, "Too many arguments are specified"), \ C(TOO_MANY_EARGS, "Too many entry arguments specified"), \ C(EVENT_TOO_BIG, "Event too big (too many fields?)"), \ - C(TYPECAST_NOT_EVENT, "Typecasts are only for eprobe fields"), + C(TYPECAST_NOT_EVENT, "Typecasts are only for eprobe fields"), \ + C(TYPECAST_REQ_FIELD, "Typecast requires a field access"), \ + C(TOO_MANY_NESTED, "Too many nested typecasts/dereferences"), #undef C #define C(a, b) TP_ERR_##a From 52c76ec18ebacdcf7e227c06f4215bc49e5d284c Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:41 +0900 Subject: [PATCH 11/37] tracing/probes: Type casting always involves nested calls This allows type casting to various fetchargs without parentheses by recursively calling parse_probe_arg on the target when type casting is used. For example, this allows the following expressions: - (STRUCT)%REG->FIELD - (STRUCT)$stackN->FIELD - (STRUCT)@SYM->FIELD Note that @SYM+/-OFFSET with typecast needs parentheses like: - (STRUCT)(@SYM-8)->FIELD Link: https://lore.kernel.org/all/178271364804.1176915.17600229399446264813.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 123 ++++++++++++++++++++++--------------- kernel/trace/trace_probe.h | 4 +- 2 files changed, 75 insertions(+), 52 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 827ae04f6351..1b97b125e9cb 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -684,19 +684,6 @@ static int parse_btf_arg(char *varname, return -EOPNOTSUPP; } - if (ctx->flags & TPARG_FL_TEVENT) { - ret = parse_trace_event(varname, code, ctx); - if (ret < 0) { - trace_probe_log_err(ctx->offset, BAD_ATTACH_ARG); - return ret; - } - /* TEVENT is only here via a typecast */ - if (WARN_ON_ONCE(ctx->struct_btf == NULL)) - return -EINVAL; - type = ctx->last_struct; - goto found_type; - } - if (ctx->flags & TPARG_FL_RETURN && !strcmp(varname, "$retval")) { code->op = FETCH_OP_RETVAL; /* Check whether the function return type is not void, even with typecast. */ @@ -708,13 +695,6 @@ static int parse_btf_arg(char *varname, tid = ctx->proto->type; goto found; } - /* - * Even if we can not find appropriate BTF info, we can still access - * the field via typecast. - */ - if (ctx->struct_btf) - goto found; - if (field) { trace_probe_log_err(ctx->offset + field - varname, NO_BTF_ENTRY); @@ -759,11 +739,7 @@ static int parse_btf_arg(char *varname, return -ENOENT; found: - if (ctx->struct_btf) - type = ctx->last_struct; - else - type = btf_type_skip_modifiers(ctx->btf, tid, NULL); -found_type: + type = btf_type_skip_modifiers(ctx->btf, tid, NULL); if (!type) { trace_probe_log_err(ctx->offset, BAD_BTF_TID); return -EINVAL; @@ -860,7 +836,7 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, struct traceprobe_parse_context *ctx) { int orig_offset = ctx->offset; - bool nested = false; + char *close; char *tmp; int ret; @@ -871,6 +847,17 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, return -EOPNOTSUPP; } + /* + * Always consider the token after typecast as a nested call + * For example: (STRUCT)VAR->FIELD and (STRUCT)(VAR)->FIELD are same. + * VAR is solved in the nested call. + */ + ctx->nested_level++; + if (ctx->nested_level > TRACEPROBE_MAX_NESTED_LEVEL) { + trace_probe_log_err(ctx->offset, TOO_MANY_NESTED); + return -E2BIG; + } + tmp = strchr(arg, ')'); if (!tmp) { trace_probe_log_err(ctx->offset + strlen(arg), @@ -879,11 +866,10 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, } *tmp++ = '\0'; - /* Handle the nested structure like (STRUCT)(VAR->FIELD)->... */ + ctx->offset += tmp - arg; if (*tmp == '(') { - char *close = find_matched_close_paren(tmp); + close = find_matched_close_paren(tmp); - ctx->offset += tmp - arg; if (!close) { trace_probe_log_err(ctx->offset, DEREF_OPEN_BRACE); return -EINVAL; @@ -894,27 +880,66 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, TYPECAST_REQ_FIELD); return -EINVAL; } + /* Skip '(' */ + ctx->offset += 1; + tmp++; + } else if (*tmp == '+' || *tmp == '-') { + /* Dereference can have another field access inside it. */ + char *open = strchr(tmp + 1, '('); - ctx->nested_level++; - if (ctx->nested_level > TRACEPROBE_MAX_NESTED_LEVEL) { - trace_probe_log_err(ctx->offset, TOO_MANY_NESTED); - return -E2BIG; + if (!open) { + trace_probe_log_err(ctx->offset, + DEREF_NEED_BRACE); + return -EINVAL; + } + close = find_matched_close_paren(open); + if (!close) { + trace_probe_log_err(ctx->offset + strlen(tmp), + DEREF_OPEN_BRACE); + return -EINVAL; + } + close++; + /* We expect a field access for typecast */ + if (close[0] != '-' || close[1] != '>') { + trace_probe_log_err(ctx->offset + close - tmp, + TYPECAST_REQ_FIELD); + return -EINVAL; + } + } else { + if (tmp[0] == '@') { + /* @sym+offset is not allowed without parenthesized */ + close = strpbrk(tmp, "+-"); + if (close && isdigit(close[1])) { + trace_probe_log_err(ctx->offset, + TYPECAST_SYM_OFFSET); + return -EINVAL; + } + } + /* Inner variable name */ + close = strchr(tmp, '-'); + if (!close || close[1] != '>') { + trace_probe_log_err(ctx->offset + strlen(tmp), + TYPECAST_REQ_FIELD); + return -EINVAL; } - *close = '\0'; - - ctx->offset += 1; /* for the '(' */ - /* We need to parse the nested one */ - ret = parse_probe_arg(tmp + 1, find_fetch_type(NULL, ctx->flags), - pcode, end, ctx); - if (ret < 0) - return ret; - ctx->nested_level--; - clear_struct_btf(ctx); - - tmp = close + 3;/* Skip "->" after closing parenthesis */ - nested = true; } + *close = '\0'; + /* We need to parse the nested one */ + ret = parse_probe_arg(tmp, find_fetch_type(NULL, ctx->flags), + pcode, end, ctx); + if (ret < 0) + return ret; + ctx->nested_level--; + clear_struct_btf(ctx); + + /* Let tmp point the field name. */ + if (close[1] == '-') + tmp = close + 3; /* Skip "->" after closing parenthesis */ + else + tmp = close + 2; /* Skip ">" after inner variable name */ + + /* resolve the typecast struct name */ ret = query_btf_struct(arg + 1, ctx); if (ret < 0) { trace_probe_log_err(orig_offset + 1, NO_PTR_STRCT); @@ -922,11 +947,7 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, } ctx->offset = orig_offset + tmp - arg; - /* If it is nested, tmp points to the field name. */ - if (nested) - ret = parse_btf_field(tmp, ctx->last_struct, pcode, end, ctx); - else - ret = parse_btf_arg(tmp, pcode, end, ctx); + ret = parse_btf_field(tmp, ctx->last_struct, pcode, end, ctx); return ret; } diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index 7d71925244e8..f4fbe3010978 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -453,6 +453,7 @@ struct traceprobe_parse_context { int nested_level; }; +/* Each typecast consumes nested level. So the max number of typecast is 3. */ #define TRACEPROBE_MAX_NESTED_LEVEL 3 extern int traceprobe_parse_probe_arg(struct trace_probe *tp, int i, @@ -592,7 +593,8 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call, C(EVENT_TOO_BIG, "Event too big (too many fields?)"), \ C(TYPECAST_NOT_EVENT, "Typecasts are only for eprobe fields"), \ C(TYPECAST_REQ_FIELD, "Typecast requires a field access"), \ - C(TOO_MANY_NESTED, "Too many nested typecasts/dereferences"), + C(TOO_MANY_NESTED, "Too many nested typecasts/dereferences"), \ + C(TYPECAST_SYM_OFFSET, "@SYM+/-OFFSET with typecast needs parentheses") #undef C #define C(a, b) TP_ERR_##a From f2183452402508ad68ea01eed18ae8eecfcd8f0a Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:41 +0900 Subject: [PATCH 12/37] tracing/probes: Support field specifier option for typecast Add a field specifier option for the typecast. This works like container_of() macro. (STRUCT[,FIELD[.FIELD2...]])VAR This is equivalent to : container_of(VAR, struct STRUCT, FIELD[.FIELD2...]) For example: echo "f tick_nohz_handler next_tick=(tick_sched,sched_timer)timer->next_tick" >> dynamic_events This will trace tick_nohz_handler() with its tick_sched::next_tick which is converted from @timer by contianer_of(tick, struct tick_sched, sched_timer). So, if you enabkle both fprobes:tick_nohz_handler__entry and timer:hrtimer_expire_entry events, we will see something like: -0 [002] d.h1. 3778.087272: hrtimer_expire_entry: hrtimer=00000000d63db328 f unction=tick_nohz_handler now=3777450051040 -0 [002] d.h1. 3778.087281: tick_nohz_handler__entry: (tick_nohz_handler+0x4 /0x140) next_tick=3777450000000 Link: https://lore.kernel.org/all/178271365745.1176915.725923927180862257.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- Documentation/trace/eprobetrace.rst | 5 +- Documentation/trace/fprobetrace.rst | 8 +- Documentation/trace/kprobetrace.rst | 8 +- kernel/trace/trace.c | 4 +- kernel/trace/trace_probe.c | 169 +++++++++++++++++++--------- kernel/trace/trace_probe.h | 5 +- 6 files changed, 135 insertions(+), 64 deletions(-) diff --git a/Documentation/trace/eprobetrace.rst b/Documentation/trace/eprobetrace.rst index cd0b4aa7f896..680e0af43d5d 100644 --- a/Documentation/trace/eprobetrace.rst +++ b/Documentation/trace/eprobetrace.rst @@ -49,7 +49,10 @@ Synopsis of eprobe_events (STRUCT)FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to a pointer to STRUCT and then derference the pointer defined by ->MEMBER. Note that when this is used, the FIELD name does not - need to be prefixed with a '$'. + need to be prefixed with a '$'. ASGN can be specified optionally. + If ASGN is specified, FIELD will be cast to the same offset + position as the ASGN member, rather than to the beginning of + the STRUCT. (STRUCT)(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can also be used with another FETCHARG instead of FIELD. diff --git a/Documentation/trace/fprobetrace.rst b/Documentation/trace/fprobetrace.rst index 6b8bb27bb62d..290a9e6f7491 100644 --- a/Documentation/trace/fprobetrace.rst +++ b/Documentation/trace/fprobetrace.rst @@ -57,10 +57,12 @@ Synopsis of fprobe-events (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types (x8/x16/x32/x64), "char", "string", "ustring", "symbol", "symstr" and bitfield are supported. - (STRUCT)FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to + (STRUCT[,ASGN])FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to a pointer to STRUCT and then derference the pointer defined by - ->MEMBER. - (STRUCT)(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can + ->MEMBER. ASGN can be specified optionally. If ASGN is specified, + FIELD will be cast to the same offset position as the ASGN member, + rather than to the beginning of the STRUCT. + (STRUCT[,ASGN])(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can also be used with another FETCHARG instead of FIELD. (\*1) This is available only when BTF is enabled. diff --git a/Documentation/trace/kprobetrace.rst b/Documentation/trace/kprobetrace.rst index c4382765d5b2..a62707e6a9f2 100644 --- a/Documentation/trace/kprobetrace.rst +++ b/Documentation/trace/kprobetrace.rst @@ -61,11 +61,13 @@ Synopsis of kprobe_events (x8/x16/x32/x64), VFS layer common type(%pd/%pD), "char", "string", "ustring", "symbol", "symstr" and bitfield are supported. - (STRUCT)FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to + (STRUCT[,ASGN])FIELD->MEMBER[->MEMBER] : If BTF is supported, typecast FIELD to a pointer to STRUCT and then derference the pointer defined by ->MEMBER. Note that this is available only when the probe is - on function entry. - (STRUCT)(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can + on function entry. ASGN can be specified optionally. If ASGN + is specified, FIELD will be cast to the same offset position + as the ASGN member, rather than to the beginning of the STRUCT. + (STRUCT[,ASGN])(FETCHARG)->MEMBER[->MEMBER] : typecast can nest, so the above can also be used with another FETCHARG instead of FIELD. (\*1) only for the probe on function entry (offs == 0). Note, this argument access diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c index e56ee034c486..5670c4b91dc0 100644 --- a/kernel/trace/trace.c +++ b/kernel/trace/trace.c @@ -4322,8 +4322,8 @@ static const char readme_msg[] = #ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API "\t $stack, $stack, $retval, $comm, $arg,\n" #ifdef CONFIG_PROBE_EVENTS_BTF_ARGS - "\t [(structname)][->field[->field|.field...]],\n" - "\t [(structname)](fetcharg)->field[->field|.field...],\n" + "\t [(structname[,field])][->field[->field|.field...]],\n" + "\t [(structname[,field])](fetcharg)->field[->field|.field...],\n" #endif #else "\t $stack, $stack, $retval, $comm,\n" diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 1b97b125e9cb..fd006b415c68 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -568,6 +568,64 @@ static int split_next_field(char *varname, char **next_field, return ret; } +/* Inner loop for solving dot operator ('.'). Return bit-offset of the given field */ +static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **ptype, + struct traceprobe_parse_context *ctx) +{ + const struct btf_type *type = *ptype; + const struct btf_member *field; + struct btf *btf = ctx_btf(ctx); + char *fieldname = *pfieldname; + int bitoffs = 0; + u32 anon_offs; + char *next; + int is_ptr; + + do { + next = NULL; + is_ptr = split_next_field(fieldname, &next, ctx); + if (is_ptr < 0) + return is_ptr; + + anon_offs = 0; + field = btf_find_struct_member(btf, type, fieldname, + &anon_offs); + if (IS_ERR(field)) { + trace_probe_log_err(ctx->offset, BAD_BTF_TID); + return PTR_ERR(field); + } + if (!field) { + trace_probe_log_err(ctx->offset, NO_BTF_FIELD); + return -ENOENT; + } + /* Add anonymous structure/union offset */ + bitoffs += anon_offs; + + /* Accumulate the bit-offsets of the dot-connected fields */ + if (btf_type_kflag(type)) { + bitoffs += BTF_MEMBER_BIT_OFFSET(field->offset); + ctx->last_bitsize = BTF_MEMBER_BITFIELD_SIZE(field->offset); + } else { + bitoffs += field->offset; + ctx->last_bitsize = 0; + } + + type = btf_type_skip_modifiers(btf, field->type, NULL); + if (!type) { + trace_probe_log_err(ctx->offset, BAD_BTF_TID); + return -EINVAL; + } + + if (next) + ctx->offset += next - fieldname; + fieldname = next; + } while (!is_ptr && fieldname); + + *pfieldname = fieldname; + *ptype = type; + + return bitoffs; +} /* * Parse the field of data structure. The @type must be a pointer type * pointing the target data structure type. @@ -577,15 +635,13 @@ static int parse_btf_field(char *fieldname, const struct btf_type *type, struct traceprobe_parse_context *ctx) { struct fetch_insn *code = *pcode; - const struct btf_member *field; - u32 bitoffs, anon_offs; - bool is_struct = ctx->struct_btf != NULL; struct btf *btf = ctx_btf(ctx); - char *next; - int is_ptr; + bool is_first_field = true; + int bitoffs; do { - if (!is_struct) { + /* For the first field of typecast, @type will be the target structure type. */ + if (!(is_first_field && ctx->struct_btf)) { /* Outer loop for solving arrow operator ('->') */ if (BTF_INFO_KIND(type->info) != BTF_KIND_PTR) { trace_probe_log_err(ctx->offset, NO_PTR_STRCT); @@ -599,60 +655,25 @@ static int parse_btf_field(char *fieldname, const struct btf_type *type, return -EINVAL; } } - /* Only the first type can skip being a pointer */ - is_struct = false; - - bitoffs = 0; - do { - /* Inner loop for solving dot operator ('.') */ - next = NULL; - is_ptr = split_next_field(fieldname, &next, ctx); - if (is_ptr < 0) - return is_ptr; - - anon_offs = 0; - field = btf_find_struct_member(btf, type, fieldname, - &anon_offs); - if (IS_ERR(field)) { - trace_probe_log_err(ctx->offset, BAD_BTF_TID); - return PTR_ERR(field); - } - if (!field) { - trace_probe_log_err(ctx->offset, NO_BTF_FIELD); - return -ENOENT; - } - /* Add anonymous structure/union offset */ - bitoffs += anon_offs; - - /* Accumulate the bit-offsets of the dot-connected fields */ - if (btf_type_kflag(type)) { - bitoffs += BTF_MEMBER_BIT_OFFSET(field->offset); - ctx->last_bitsize = BTF_MEMBER_BITFIELD_SIZE(field->offset); - } else { - bitoffs += field->offset; - ctx->last_bitsize = 0; - } - - type = btf_type_skip_modifiers(btf, field->type, NULL); - if (!type) { - trace_probe_log_err(ctx->offset, BAD_BTF_TID); - return -EINVAL; - } - - ctx->offset += next - fieldname; - fieldname = next; - } while (!is_ptr && fieldname); + bitoffs = get_bitoffset_of_field(&fieldname, &type, ctx); + if (bitoffs < 0) + return bitoffs; if (++code == end) { trace_probe_log_err(ctx->offset, TOO_MANY_OPS); return -EINVAL; } code->op = FETCH_OP_DEREF; /* TODO: user deref support */ code->offset = bitoffs / 8; + if (is_first_field && ctx->struct_btf) { + /* The first field can be typecasted with field option. */ + code->offset -= ctx->prefix_byteoffs; + } *pcode = code; ctx->last_bitoffs = bitoffs % 8; ctx->last_type = type; + is_first_field = false; } while (fieldname); return 0; @@ -808,6 +829,46 @@ static int query_btf_struct(const char *sname, struct traceprobe_parse_context * return 0; } +static int parse_btf_casttype(char *casttype, struct traceprobe_parse_context *ctx) +{ + char *field; + int ret; + + /* Field option - evaluated later. */ + field = strchr(casttype, ','); + if (field) + *field++ = '\0'; + + ret = query_btf_struct(casttype, ctx); + if (ret < 0) { + trace_probe_log_err(ctx->offset, NO_PTR_STRCT); + return -EINVAL; + } + + if (field) { + struct btf_type *type = (struct btf_type *)ctx->last_struct; + + ctx->offset += field - casttype; + ret = get_bitoffset_of_field(&field, &ctx->last_struct, ctx); + if (ret < 0) + return ret; + if (ret % 8) { + trace_probe_log_err(ctx->offset, TYPECAST_NOT_ALIGNED); + return -EINVAL; + } + if (field != NULL) { + /* this means @field skips an arrow operator ("->"). */ + trace_probe_log_err(ctx->offset - 2, TYPECAST_BAD_ARROW); + return -EINVAL; + } + ctx->prefix_byteoffs = ret / 8; + /* Restore the original struct type (overwritten by get_bitoffset_of_field) */ + ctx->last_struct = type; + } + + return ret; +} + /* Find the matching closing parenthesis for a given opening parenthesis. */ static char *find_matched_close_paren(char *s) { @@ -940,14 +1001,14 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, tmp = close + 2; /* Skip ">" after inner variable name */ /* resolve the typecast struct name */ - ret = query_btf_struct(arg + 1, ctx); - if (ret < 0) { - trace_probe_log_err(orig_offset + 1, NO_PTR_STRCT); - return -EINVAL; - } + ctx->offset = orig_offset + 1; /* for the '(' */ + ret = parse_btf_casttype(arg + 1, ctx); + if (ret < 0) + return ret; ctx->offset = orig_offset + tmp - arg; ret = parse_btf_field(tmp, ctx->last_struct, pcode, end, ctx); + ctx->prefix_byteoffs = 0; return ret; } diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index f4fbe3010978..6a0859e00bf4 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -451,6 +451,7 @@ struct traceprobe_parse_context { unsigned int flags; int offset; int nested_level; + int prefix_byteoffs; /* The byte offset of the prefix field of typecast */ }; /* Each typecast consumes nested level. So the max number of typecast is 3. */ @@ -594,7 +595,9 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call, C(TYPECAST_NOT_EVENT, "Typecasts are only for eprobe fields"), \ C(TYPECAST_REQ_FIELD, "Typecast requires a field access"), \ C(TOO_MANY_NESTED, "Too many nested typecasts/dereferences"), \ - C(TYPECAST_SYM_OFFSET, "@SYM+/-OFFSET with typecast needs parentheses") + C(TYPECAST_SYM_OFFSET, "@SYM+/-OFFSET with typecast needs parentheses"), \ + C(TYPECAST_NOT_ALIGNED, "Typecast field option is not byte-aligned"), \ + C(TYPECAST_BAD_ARROW, "Typecast field option does not support -> operator"), #undef C #define C(a, b) TP_ERR_##a From 738b78ee4bfa5e290f155be1e946bc2ae5660dfd Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:41 +0900 Subject: [PATCH 13/37] tracing/probes: Add $current variable support Since we can use the BTF to cast value to a structure pointer type, it is useful to introduce "$current" special variable support to fetcharg. User can define a fetcharg to access current task_struct properties using BTF info. e.g. $current->cpus_ptr Link: https://lore.kernel.org/all/178271366709.1176915.15320906169981578568.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- Documentation/trace/fprobetrace.rst | 1 + Documentation/trace/kprobetrace.rst | 1 + kernel/trace/trace.c | 4 ++-- kernel/trace/trace_probe.c | 37 ++++++++++++++++++++++++++++- kernel/trace/trace_probe.h | 1 + kernel/trace/trace_probe_tmpl.h | 3 +++ 6 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Documentation/trace/fprobetrace.rst b/Documentation/trace/fprobetrace.rst index 290a9e6f7491..3392cab016b3 100644 --- a/Documentation/trace/fprobetrace.rst +++ b/Documentation/trace/fprobetrace.rst @@ -50,6 +50,7 @@ Synopsis of fprobe-events $argN : Fetch the Nth function argument. (N >= 1) (\*2) $retval : Fetch return value.(\*3) $comm : Fetch current task comm. + $current : Fetch the address of the current task_struct. +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*4)(\*5) \IMM : Store an immediate value to the argument. NAME=FETCHARG : Set NAME as the argument name of FETCHARG. diff --git a/Documentation/trace/kprobetrace.rst b/Documentation/trace/kprobetrace.rst index a62707e6a9f2..81e4fe38791d 100644 --- a/Documentation/trace/kprobetrace.rst +++ b/Documentation/trace/kprobetrace.rst @@ -53,6 +53,7 @@ Synopsis of kprobe_events $argN : Fetch the Nth function argument. (N >= 1) (\*1) $retval : Fetch return value.(\*2) $comm : Fetch current task comm. + $current : Fetch the address of the current task_struct. +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*3)(\*4) \IMM : Store an immediate value to the argument. NAME=FETCHARG : Set NAME as the argument name of FETCHARG. diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c index 5670c4b91dc0..2b0b4f9acb2e 100644 --- a/kernel/trace/trace.c +++ b/kernel/trace/trace.c @@ -4320,13 +4320,13 @@ static const char readme_msg[] = "\t args: =fetcharg[:type]\n" "\t fetcharg: (%|$), @
, @[+|-],\n" #ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API - "\t $stack, $stack, $retval, $comm, $arg,\n" + "\t $stack, $stack, $retval, $comm, $arg, $current\n" #ifdef CONFIG_PROBE_EVENTS_BTF_ARGS "\t [(structname[,field])][->field[->field|.field...]],\n" "\t [(structname[,field])](fetcharg)->field[->field|.field...],\n" #endif #else - "\t $stack, $stack, $retval, $comm,\n" + "\t $stack, $stack, $retval, $comm, $current\n" #endif "\t +|-[u](), \\imm-value, \\\"imm-string\"\n" "\t kernel return probes support: $retval, $arg, $comm\n" diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index fd006b415c68..999dec84275d 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -692,7 +692,9 @@ static int parse_btf_arg(char *varname, int i, is_ptr, ret; u32 tid; - if (!ctx->funcname && !(ctx->flags & TPARG_FL_TEVENT)) + /* Note: field is not separated at this point, so check prefix. */ + if (!str_has_prefix(varname, "$current") && + !ctx->funcname && !(ctx->flags & TPARG_FL_TEVENT)) return -EINVAL; is_ptr = split_next_field(varname, &field, ctx); @@ -705,6 +707,20 @@ static int parse_btf_arg(char *varname, return -EOPNOTSUPP; } + if (!strcmp(varname, "$current")) { + code->op = FETCH_OP_CURRENT; + /* If no typecast is specified for $current, use task_struct by default */ + ret = bpf_find_btf_id("task_struct", BTF_KIND_STRUCT, &ctx->struct_btf); + if (ret < 0) { + trace_probe_log_err(ctx->offset, NO_BTF_ENTRY); + return -ENOENT; + } + tid = (u32)ret; + type = ctx->last_struct = + btf_type_skip_modifiers(ctx->struct_btf, tid, NULL); + goto found_type; + } + if (ctx->flags & TPARG_FL_RETURN && !strcmp(varname, "$retval")) { code->op = FETCH_OP_RETVAL; /* Check whether the function return type is not void, even with typecast. */ @@ -761,6 +777,7 @@ static int parse_btf_arg(char *varname, found: type = btf_type_skip_modifiers(ctx->btf, tid, NULL); +found_type: if (!type) { trace_probe_log_err(ctx->offset, BAD_BTF_TID); return -EINVAL; @@ -1270,6 +1287,24 @@ static int parse_probe_vars(char *orig_arg, const struct fetch_type *t, return 0; } + /* $current returns the address of the current task_struct. */ + if (str_has_prefix(arg, "current")) { + /* $current is only supported by kernel probe. */ + if (!(ctx->flags & TPARG_FL_KERNEL)) { + err = TP_ERR_BAD_VAR; + goto inval; + } + arg += strlen("current"); + if (*arg == '-' && IS_ENABLED(CONFIG_PROBE_EVENTS_BTF_ARGS)) + return parse_btf_arg(orig_arg, pcode, end, ctx); + + if (*arg != '\0') + goto inval; + + code->op = FETCH_OP_CURRENT; + return 0; + } + #ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API len = str_has_prefix(arg, "arg"); if (len) { diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index 6a0859e00bf4..195096f5b8b1 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -92,6 +92,7 @@ typedef int (*print_type_func_t)(struct trace_seq *, void *, void *); FETCH_OP(RETVAL, none), /* Return value */ \ FETCH_OP(IMM, imm), /* Immediate: .immediate */ \ FETCH_OP(COMM, none), /* Current comm */ \ + FETCH_OP(CURRENT, none), /* Current task_struct address */\ FETCH_OP(ARG, param), /* Argument: .param = index */ \ FETCH_OP(FOFFS, imm), /* File offset: .immediate */ \ FETCH_OP(IMMSTR, string), /* Allocated string: .data */ \ diff --git a/kernel/trace/trace_probe_tmpl.h b/kernel/trace/trace_probe_tmpl.h index 51436f19083b..d0e9662cde00 100644 --- a/kernel/trace/trace_probe_tmpl.h +++ b/kernel/trace/trace_probe_tmpl.h @@ -112,6 +112,9 @@ process_common_fetch_insn(struct fetch_insn *code, unsigned long *val) case FETCH_OP_IMMSTR: *val = (unsigned long)code->data; break; + case FETCH_OP_CURRENT: + *val = (unsigned long)current; + break; default: return -EILSEQ; } From 49b0a3f6052975d5c25721ec75dce557cc933d90 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:42 +0900 Subject: [PATCH 14/37] tracing/probes: Add this_cpu_read() and this_cpu_ptr() dereference method to fetcharg When tracing the kernel local variables, sometimes we need to get the CPU local variables. To access it, current simple dereference is not enough. Thus, introduce a special this_cpu_read() dereference to access per-cpu variable for the current CPU (accessing other CPU variable may race with updates on other CPUs). Also this_cpu_ptr() is for accessing per-cpu pointer. Those are working as same as the kernel percpu macro. Link: https://lore.kernel.org/all/178271367680.1176915.4711734074448973989.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- Documentation/trace/fprobetrace.rst | 2 + Documentation/trace/kprobetrace.rst | 2 + kernel/trace/trace.c | 1 + kernel/trace/trace_probe.c | 152 +++++++++++++++++++++------- kernel/trace/trace_probe.h | 4 +- kernel/trace/trace_probe_tmpl.h | 22 ++-- 6 files changed, 138 insertions(+), 45 deletions(-) diff --git a/Documentation/trace/fprobetrace.rst b/Documentation/trace/fprobetrace.rst index 3392cab016b3..3439bc9bd351 100644 --- a/Documentation/trace/fprobetrace.rst +++ b/Documentation/trace/fprobetrace.rst @@ -52,6 +52,8 @@ Synopsis of fprobe-events $comm : Fetch current task comm. $current : Fetch the address of the current task_struct. +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*4)(\*5) + this_cpu_read(FETCHARG) : Read the value of the per-CPU variable FETCHARG on the current CPU. + this_cpu_ptr(FETCHARG) : Get the address of the per-CPU variable FETCHARG on the current CPU. \IMM : Store an immediate value to the argument. NAME=FETCHARG : Set NAME as the argument name of FETCHARG. FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types diff --git a/Documentation/trace/kprobetrace.rst b/Documentation/trace/kprobetrace.rst index 81e4fe38791d..9ae330eb0a52 100644 --- a/Documentation/trace/kprobetrace.rst +++ b/Documentation/trace/kprobetrace.rst @@ -55,6 +55,8 @@ Synopsis of kprobe_events $comm : Fetch current task comm. $current : Fetch the address of the current task_struct. +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*3)(\*4) + this_cpu_read(FETCHARG) : Read the value of the per-CPU variable FETCHARG on the current CPU. + this_cpu_ptr(FETCHARG) : Get the address of the per-CPU variable FETCHARG on the current CPU. \IMM : Store an immediate value to the argument. NAME=FETCHARG : Set NAME as the argument name of FETCHARG. FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types diff --git a/kernel/trace/trace.c b/kernel/trace/trace.c index 2b0b4f9acb2e..c9e182d40059 100644 --- a/kernel/trace/trace.c +++ b/kernel/trace/trace.c @@ -4329,6 +4329,7 @@ static const char readme_msg[] = "\t $stack, $stack, $retval, $comm, $current\n" #endif "\t +|-[u](), \\imm-value, \\\"imm-string\"\n" + "\t this_cpu_read(), this_cpu_ptr()\n" "\t kernel return probes support: $retval, $arg, $comm\n" "\t type: s8/16/32/64, u8/16/32/64, x8/16/32/64, char, string, symbol,\n" "\t b@/, ustring,\n" diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 999dec84275d..18c212122344 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -345,6 +345,109 @@ static int parse_trace_event(char *arg, struct fetch_insn *code, return -EINVAL; } +/* this_cpu_* parser */ +#define THIS_CPU_PTR_PREFIX "this_cpu_ptr(" +#define THIS_CPU_READ_PREFIX "this_cpu_read(" +#define THIS_CPU_PTR_LEN (sizeof(THIS_CPU_PTR_PREFIX) - 1) +#define THIS_CPU_READ_LEN (sizeof(THIS_CPU_READ_PREFIX) - 1) + +static int +parse_probe_arg(char *arg, const struct fetch_type *type, + struct fetch_insn **pcode, struct fetch_insn *end, + struct traceprobe_parse_context *ctx); + +/* handle dereference nested call */ +static inline int handle_dereference(char *arg, struct fetch_insn **pcode, + struct fetch_insn *end, struct traceprobe_parse_context *ctx, + int deref, long offset) +{ + const struct fetch_type *type = find_fetch_type(NULL, ctx->flags); + struct fetch_insn *code = *pcode; + int cur_offs = ctx->offset; + char *tmp; + int ret; + + tmp = strrchr(arg, ')'); + if (!tmp) { + trace_probe_log_err(ctx->offset + strlen(arg), + DEREF_OPEN_BRACE); + return -EINVAL; + } + + *tmp = '\0'; + ret = parse_probe_arg(arg, type, &code, end, ctx); + if (ret) + return ret; + ctx->offset = cur_offs; + if (code->op == FETCH_OP_COMM || code->op == FETCH_OP_IMMSTR) { + trace_probe_log_err(ctx->offset, COMM_CANT_DEREF); + return -EINVAL; + } + + /* + * this_cpu_ptr(@SYM) does not use SYM value, but use SYM address. + * So we overwrite the last FETCH_OP_DEREF with FETCH_OP_CPU_PTR. + */ + if (!(deref == FETCH_OP_CPU_PTR && *arg == '@')) { + code++; + if (code == end) { + trace_probe_log_err(ctx->offset, TOO_MANY_OPS); + return -EINVAL; + } + } + *pcode = code; + + code->op = deref; + code->offset = offset; + /* Reset the last type if used */ + ctx->last_type = NULL; + return 0; +} + +static int parse_this_cpu(char *arg, struct fetch_insn **pcode, + struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + struct fetch_insn *code; + bool is_ptr = false; + int ret; + + /* + * This is only for kernel probes, excluding eprobe, because per-cpu + * pointer should not be recorded by events. + */ + if (!(ctx->flags & TPARG_FL_KERNEL) || + (ctx->flags & TPARG_FL_TEVENT)) { + trace_probe_log_err(ctx->offset, NOSUP_PERCPU); + return -EINVAL; + } + if (str_has_prefix(arg, THIS_CPU_PTR_PREFIX)) { + arg += THIS_CPU_PTR_LEN; + ctx->offset += THIS_CPU_PTR_LEN; + is_ptr = true; + } else if (str_has_prefix(arg, THIS_CPU_READ_PREFIX)) { + arg += THIS_CPU_READ_LEN; + ctx->offset += THIS_CPU_READ_LEN; + } else + return -EINVAL; + + ret = handle_dereference(arg, pcode, end, ctx, FETCH_OP_CPU_PTR, 0); + if (ret || is_ptr) + return ret; + + /* this_cpu_read(VAR) -> +0(this_cpu_ptr(VAR)) */ + code = *pcode; + code++; + if (code == end) { + trace_probe_log_err(ctx->offset, TOO_MANY_OPS); + return -EINVAL; + } + code->op = FETCH_OP_DEREF; + code->offset = 0; + *pcode = code; + return 0; +} + #ifdef CONFIG_PROBE_EVENTS_BTF_ARGS static u32 btf_type_int(const struct btf_type *t) @@ -904,11 +1007,6 @@ static char *find_matched_close_paren(char *s) return NULL; } -static int -parse_probe_arg(char *arg, const struct fetch_type *type, - struct fetch_insn **pcode, struct fetch_insn *end, - struct traceprobe_parse_context *ctx); - static int handle_typecast(char *arg, struct fetch_insn **pcode, struct fetch_insn *end, struct traceprobe_parse_context *ctx) @@ -961,7 +1059,9 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, /* Skip '(' */ ctx->offset += 1; tmp++; - } else if (*tmp == '+' || *tmp == '-') { + } else if (*tmp == '+' || *tmp == '-' || + str_has_prefix(tmp, THIS_CPU_PTR_PREFIX) || + str_has_prefix(tmp, THIS_CPU_READ_PREFIX)) { /* Dereference can have another field access inside it. */ char *open = strchr(tmp + 1, '('); @@ -1481,36 +1581,9 @@ parse_probe_arg(char *arg, const struct fetch_type *type, } ctx->offset += (tmp + 1 - arg) + (arg[0] != '-' ? 1 : 0); arg = tmp + 1; - tmp = strrchr(arg, ')'); - if (!tmp) { - trace_probe_log_err(ctx->offset + strlen(arg), - DEREF_OPEN_BRACE); - return -EINVAL; - } else { - const struct fetch_type *t2 = find_fetch_type(NULL, ctx->flags); - int cur_offs = ctx->offset; - - *tmp = '\0'; - ret = parse_probe_arg(arg, t2, &code, end, ctx); - if (ret) - break; - ctx->offset = cur_offs; - if (code->op == FETCH_OP_COMM || - code->op == FETCH_OP_IMMSTR) { - trace_probe_log_err(ctx->offset, COMM_CANT_DEREF); - return -EINVAL; - } - if (++code == end) { - trace_probe_log_err(ctx->offset, TOO_MANY_OPS); - return -EINVAL; - } - *pcode = code; - - code->op = deref; - code->offset = offset; - /* Reset the last type if used */ - ctx->last_type = NULL; - } + ret = handle_dereference(arg, pcode, end, ctx, deref, offset); + if (ret < 0) + return ret; break; case '\\': /* Immediate value */ if (arg[1] == '"') { /* Immediate string */ @@ -1531,7 +1604,10 @@ parse_probe_arg(char *arg, const struct fetch_type *type, ret = handle_typecast(arg, pcode, end, ctx); break; default: - if (isalpha(arg[0]) || arg[0] == '_') { + if (str_has_prefix(arg, THIS_CPU_PTR_PREFIX) || + str_has_prefix(arg, THIS_CPU_READ_PREFIX)) { + ret = parse_this_cpu(arg, pcode, end, ctx); + } else if (isalpha(arg[0]) || arg[0] == '_') { /* BTF variable or event field*/ if (ctx->flags & TPARG_FL_TEVENT) { ret = parse_trace_event(arg, *pcode, ctx); @@ -1548,8 +1624,8 @@ parse_probe_arg(char *arg, const struct fetch_type *type, return -EINVAL; } ret = parse_btf_arg(arg, pcode, end, ctx); - break; } + break; } if (!ret && code->op == FETCH_OP_NOP) { /* Parsed, but do not find fetch method */ diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index 195096f5b8b1..e6268a8dc378 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -101,6 +101,7 @@ typedef int (*print_type_func_t)(struct trace_seq *, void *, void *); /* Stage 2 (dereference) ops */ \ FETCH_OP(DEREF, offset), /* Dereference: .offset */ \ FETCH_OP(UDEREF, offset), /* User-space dereference: .offset */\ + FETCH_OP(CPU_PTR, none), /* Per-CPU pointer: .offset */ \ /* Stage 3 (store) ops */ \ FETCH_OP(ST_RAW, store), /* Raw value: .size */ \ FETCH_OP(ST_MEM, store), /* Memory: .offset, .size */ \ @@ -598,7 +599,8 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call, C(TOO_MANY_NESTED, "Too many nested typecasts/dereferences"), \ C(TYPECAST_SYM_OFFSET, "@SYM+/-OFFSET with typecast needs parentheses"), \ C(TYPECAST_NOT_ALIGNED, "Typecast field option is not byte-aligned"), \ - C(TYPECAST_BAD_ARROW, "Typecast field option does not support -> operator"), + C(TYPECAST_BAD_ARROW, "Typecast field option does not support -> operator"), \ + C(NOSUP_PERCPU, "Per-cpu variable access is only for kernel probes"), #undef C #define C(a, b) TP_ERR_##a diff --git a/kernel/trace/trace_probe_tmpl.h b/kernel/trace/trace_probe_tmpl.h index d0e9662cde00..8db12f758fda 100644 --- a/kernel/trace/trace_probe_tmpl.h +++ b/kernel/trace/trace_probe_tmpl.h @@ -129,25 +129,35 @@ process_fetch_insn_bottom(struct fetch_insn *code, unsigned long val, struct fetch_insn *s3 = NULL; int total = 0, ret = 0, i = 0; u32 loc = 0; - unsigned long lval = val; + unsigned long lval, llval = val; stage2: /* 2nd stage: dereference memory if needed */ do { - if (code->op == FETCH_OP_DEREF) { - lval = val; + lval = val; + switch (code->op) { + case FETCH_OP_DEREF: ret = probe_mem_read(&val, (void *)val + code->offset, sizeof(val)); - } else if (code->op == FETCH_OP_UDEREF) { - lval = val; + break; + case FETCH_OP_UDEREF: ret = probe_mem_read_user(&val, (void *)val + code->offset, sizeof(val)); - } else break; + case FETCH_OP_CPU_PTR: + val = (unsigned long)this_cpu_ptr((void __percpu *)val); + ret = 0; + break; + default: + lval = llval; + goto out; + } if (ret) return ret; + llval = lval; code++; } while (1); +out: s3 = code; stage3: From 7d8d6ad659c02ed5d2387777194c22e8e81dbb2b Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Thu, 2 Jul 2026 21:12:42 +0900 Subject: [PATCH 15/37] tracing/probes: Add a new testcase for BTF typecasts With the introduction of container_of-style BTF typecasting and per-CPU variable access support in trace probes, we need a way to verify their functionality and prevent regressions. Add a new ftrace kselftest and update the trace event sample module to test and validate these features. Specifically, update the trace-events-sample module to set up a periodic timer whose callback accesses a per-CPU counter. Introduce a new sample trace event, foo_timer_fn, to trace this callback and log the current counter value. Then, add a new test case, btf_probe_event.tc, which defines a dynamic probe on the timer callback. The probe uses BTF typecasting to recover the parent structure from the timer argument and this_cpu_read() to fetch the per-CPU counter. The test verifies the integrity of the implementation by ensuring the values recorded by the dynamic probe match those from the static tracepoint. Link: https://lore.kernel.org/all/178271368625.1176915.13502654445483530822.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- samples/trace_events/trace-events-sample.c | 40 ++++++- samples/trace_events/trace-events-sample.h | 34 +++++- .../ftrace/test.d/dynevent/btf_probe_event.tc | 51 +++++++++ .../test.d/dynevent/btf_typecast_accepted.tc | 103 ++++++++++++++++++ .../test.d/dynevent/eprobes_syntax_errors.tc | 9 ++ .../test.d/dynevent/fprobe_syntax_errors.tc | 12 ++ .../test.d/kprobe/kprobe_syntax_errors.tc | 12 ++ .../test.d/kprobe/uprobe_syntax_errors.tc | 5 + 8 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 tools/testing/selftests/ftrace/test.d/dynevent/btf_probe_event.tc create mode 100644 tools/testing/selftests/ftrace/test.d/dynevent/btf_typecast_accepted.tc diff --git a/samples/trace_events/trace-events-sample.c b/samples/trace_events/trace-events-sample.c index 0b7a6efdb247..ca5d98c360cb 100644 --- a/samples/trace_events/trace-events-sample.c +++ b/samples/trace_events/trace-events-sample.c @@ -94,6 +94,20 @@ static int simple_thread_fn(void *arg) static DEFINE_MUTEX(thread_mutex); static int simple_thread_cnt; +static struct foo_timer_data *foo_timer_data; + +static void sample_timer_cb(struct timer_list *t) +{ + struct foo_timer_data *data = container_of(t, struct foo_timer_data, timer); + + get_cpu(); + trace_foo_timer_fn(data); + (*this_cpu_ptr(data->counter))++; + put_cpu(); + + mod_timer(t, jiffies + HZ); +} + int foo_bar_reg(void) { mutex_lock(&thread_mutex); @@ -132,9 +146,27 @@ void foo_bar_unreg(void) static int __init trace_event_init(void) { + foo_timer_data = kzalloc_obj(*foo_timer_data, GFP_KERNEL); + if (!foo_timer_data) + return -ENOMEM; + + foo_timer_data->name = "sample_timer_counter"; + foo_timer_data->counter = alloc_percpu(int); + if (!foo_timer_data->counter) { + kfree(foo_timer_data); + return -ENOMEM; + } + + timer_setup(&foo_timer_data->timer, sample_timer_cb, 0); + mod_timer(&foo_timer_data->timer, jiffies + HZ); + simple_tsk = kthread_run(simple_thread, NULL, "event-sample"); - if (IS_ERR(simple_tsk)) - return -1; + if (IS_ERR(simple_tsk)) { + timer_shutdown_sync(&foo_timer_data->timer); + free_percpu(foo_timer_data->counter); + kfree(foo_timer_data); + return PTR_ERR(simple_tsk); + } return 0; } @@ -147,6 +179,10 @@ static void __exit trace_event_exit(void) kthread_stop(simple_tsk_fn); simple_tsk_fn = NULL; mutex_unlock(&thread_mutex); + + timer_shutdown_sync(&foo_timer_data->timer); + free_percpu(foo_timer_data->counter); + kfree(foo_timer_data); } module_init(trace_event_init); diff --git a/samples/trace_events/trace-events-sample.h b/samples/trace_events/trace-events-sample.h index 1a05fc153353..816848a456a2 100644 --- a/samples/trace_events/trace-events-sample.h +++ b/samples/trace_events/trace-events-sample.h @@ -247,12 +247,14 @@ */ /* - * It is OK to have helper functions in the file, but they need to be protected - * from being defined more than once. Remember, this file gets included more - * than once. + * It is OK to have helper functions and data structures in the file, but they + * need to be protected from being defined more than once. Remember, this file + * gets included more than once. */ #ifndef __TRACE_EVENT_SAMPLE_HELPER_FUNCTIONS #define __TRACE_EVENT_SAMPLE_HELPER_FUNCTIONS +#include + static inline int __length_of(const int *list) { int i; @@ -270,6 +272,13 @@ enum { TRACE_SAMPLE_BAR = 4, TRACE_SAMPLE_ZOO = 8, }; + +struct foo_timer_data { + const char *name; + struct timer_list timer; + int __percpu *counter; +}; + #endif /* @@ -595,6 +604,25 @@ TRACE_EVENT(foo_rel_loc, __get_rel_bitmask(bitmask), __get_rel_cpumask(cpumask)) ); + +TRACE_EVENT(foo_timer_fn, + + TP_PROTO(struct foo_timer_data *data), + + TP_ARGS(data), + + TP_STRUCT__entry( + __string( name, data->name ) + __field( int, count ) + ), + + TP_fast_assign( + __assign_str(name); + __entry->count = *this_cpu_ptr(data->counter); + ), + + TP_printk("name=%s count=%d", __get_str(name), __entry->count) +); #endif /***** NOTICE! The #if protection ends here. *****/ diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/btf_probe_event.tc b/tools/testing/selftests/ftrace/test.d/dynevent/btf_probe_event.tc new file mode 100644 index 000000000000..bf71368c31a4 --- /dev/null +++ b/tools/testing/selftests/ftrace/test.d/dynevent/btf_probe_event.tc @@ -0,0 +1,51 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# description: BTF event with typecast and percpu access +# requires: dynamic_events "this_cpu_read()":README "[(structname[,field])][->field[->field|.field...]]":README + +# Check if the sample module is loaded +if ! lsmod | grep -q trace_events_sample; then + modprobe trace-events-sample || exit_unresolved +fi + +echo 0 > events/enable +echo > dynamic_events + +# The sample_timer_cb(struct timer_list *t) is called. +# We want to check (STRUCT,FIELD)VAR typecast and this_cpu_read() access. +# (foo_timer_data,timer)t converts t to struct foo_timer_data * using container_of. +# data->counter is a per-cpu pointer to int. +# this_cpu_read(data->counter) should give the value of the counter. + +echo 'f:mysample/myevent sample_timer_cb name=(foo_timer_data,timer)t->name:string count=this_cpu_read((foo_timer_data,timer)t->counter)' >> dynamic_events + +echo 1 > events/mysample/myevent/enable +echo 1 > events/sample-trace/foo_timer_fn/enable + +sleep 2 + +echo 0 > events/mysample/myevent/enable +echo 0 > events/sample-trace/foo_timer_fn/enable + +# Compare the values. +MATCH=0 +while read line; do + if echo $line | grep -q "foo_timer_fn:"; then + NAME=`echo $line | sed 's/.*name=\([^ ]*\) .*/\1/'` + COUNT=`echo $line | sed 's/.*count=\([^ ]*\).*/\1/'` + if grep -q "myevent:.*name=\"${NAME}\" count=$COUNT" trace; then + MATCH=$((MATCH+1)) + fi + fi +done < trace + +if [ $MATCH -eq 0 ]; then + echo "No matching events found" + exit_fail +fi + +# Clean up +echo 0 > events/mysample/myevent/enable +echo 0 > events/sample-trace/foo_timer_fn/enable +echo > dynamic_events +clear_trace diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/btf_typecast_accepted.tc b/tools/testing/selftests/ftrace/test.d/dynevent/btf_typecast_accepted.tc new file mode 100644 index 000000000000..dd5552727054 --- /dev/null +++ b/tools/testing/selftests/ftrace/test.d/dynevent/btf_typecast_accepted.tc @@ -0,0 +1,103 @@ +#!/bin/sh +# SPDX-License-Identifier: GPL-2.0 +# description: BTF typecast and percpu access syntax validation +# requires: dynamic_events "this_cpu_read()":README "[(structname[,field])][->field[->field|.field...]]":README + +KPROBES= +FPROBES= + +if grep -qF "p[:[/][]] []" README ; then + KPROBES=yes +fi +if grep -qF "f[:[/][]] [%return] []" README ; then + FPROBES=yes +fi + +if [ -z "$KPROBES" -a -z "$FPROBES" ] ; then + exit_unsupported +fi + +echo 0 > events/enable +echo > dynamic_events + +# Load trace-events-sample module if available to have per-CPU counter structure defined +if ! lsmod | grep -q trace_events_sample; then + modprobe trace-events-sample || exit_unresolved +fi + +if [ "$FPROBES" ] ; then + # 1. Test basic typecast on fprobe + echo 'f:fpevent1 vfs_read name=(file)file->f_path.dentry->d_name.name:string' >> dynamic_events + # 2. Test parenthesized typecast target on fprobe + echo 'f:fpevent2 vfs_read name=(file)(file)->f_path.dentry->d_name.name:string' >> dynamic_events + # 3. Test nested typecasts on fprobe + echo 'f:fpevent3 vfs_read name=(dentry)((file)file->f_path.dentry)->d_name.name:string' >> dynamic_events + # 4. Test container_of-style typecast with field option on fprobe + echo 'f:fpevent4 vfs_read name=(file,f_path)file->f_mode' >> dynamic_events + # 5. Test typecast on return value on fprobe + echo 'f:fpevent5 vfs_read%return name=(file)$retval->f_path.dentry->d_name.name:string' >> dynamic_events + # 6. Test $current variable support on fprobe + echo 'f:fpevent6 vfs_read pid=$current->pid' >> dynamic_events + echo 'f:fpevent7 vfs_read pid=(task_struct)$current->pid' >> dynamic_events + echo 'f:fpevent8 vfs_read pid=(task_struct,group_leader)$current->pid' >> dynamic_events + + # Test this_cpu_read and this_cpu_ptr on fprobe + echo 'f:fpevent9 sample_timer_cb name=(foo_timer_data,timer)t->name:string count=this_cpu_read((foo_timer_data,timer)t->counter)' >> dynamic_events + echo 'f:fpevent10 sample_timer_cb ptr=this_cpu_ptr((foo_timer_data,timer)t->counter)' >> dynamic_events +fi + +if [ "$KPROBES" ] ; then + # 7. Test basic typecast on kprobe + echo 'p:kpevent1 vfs_read name=(file)file->f_path.dentry->d_name.name:string' >> dynamic_events + # 8. Test parenthesized typecast target on kprobe + echo 'p:kpevent2 vfs_read name=(file)(file)->f_path.dentry->d_name.name:string' >> dynamic_events + # 9. Test nested typecasts on kprobe + echo 'p:kpevent3 vfs_read name=(dentry)((file)file->f_path.dentry)->d_name.name:string' >> dynamic_events + # 10. Test container_of-style typecast with field option on kprobe + echo 'p:kpevent4 vfs_read name=(file,f_path)file->f_mode' >> dynamic_events + # 11. Test typecast on return value on kretprobe + echo 'r:kpevent5 vfs_read name=(file)$retval->f_path.dentry->d_name.name:string' >> dynamic_events + # 12. Test $current variable support on kprobe + echo 'p:kpevent6 vfs_read pid=$current->pid' >> dynamic_events + echo 'p:kpevent7 vfs_read pid=(task_struct)$current->pid' >> dynamic_events + echo 'p:kpevent8 vfs_read pid=(task_struct,group_leader)$current->pid' >> dynamic_events + + # Test this_cpu_read and this_cpu_ptr on kprobe + echo 'p:kpevent9 sample_timer_cb name=(foo_timer_data,timer)t->name:string count=this_cpu_read((foo_timer_data,timer)t->counter)' >> dynamic_events + echo 'p:kpevent10 sample_timer_cb ptr=this_cpu_ptr((foo_timer_data,timer)t->counter)' >> dynamic_events +fi + +# Verify the events exist in dynamic_events +if [ "$FPROBES" ] ; then + grep -q "fpevent1 " dynamic_events + grep -q "fpevent2 " dynamic_events + grep -q "fpevent3 " dynamic_events + grep -q "fpevent4 " dynamic_events + grep -q "fpevent5 " dynamic_events + grep -q "fpevent6 " dynamic_events + grep -q "fpevent7 " dynamic_events + grep -q "fpevent8 " dynamic_events + if lsmod | grep -q trace_events_sample; then + grep -q "fpevent9 " dynamic_events + grep -q "fpevent10 " dynamic_events + fi +fi + +if [ "$KPROBES" ] ; then + grep -q "kpevent1 " dynamic_events + grep -q "kpevent2 " dynamic_events + grep -q "kpevent3 " dynamic_events + grep -q "kpevent4 " dynamic_events + grep -q "kpevent5 " dynamic_events + grep -q "kpevent6 " dynamic_events + grep -q "kpevent7 " dynamic_events + grep -q "kpevent8 " dynamic_events + if lsmod | grep -q trace_events_sample; then + grep -q "kpevent9 " dynamic_events + grep -q "kpevent10 " dynamic_events + fi +fi + +# Clean up +echo > dynamic_events +clear_trace diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc index 0e65e787e426..c2e3f9d19f13 100644 --- a/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/dynevent/eprobes_syntax_errors.tc @@ -21,8 +21,17 @@ check_error 'e:foo/^bar.1 syscalls/sys_enter_openat' # BAD_EVENT_NAME check_error 'e:foo/bar syscalls/sys_enter_openat arg=^$foo' # BAD_ATTACH_ARG +check_error 'e:foo/bar syscalls/sys_enter_openat arg=^COMM' # NO_EVENT_FIELD +if grep -q "\$current.*" README; then + check_error 'e:foo/bar syscalls/sys_enter_openat arg=^current' # NO_EVENT_FIELD +fi + if grep -q '\..*\[if \]' README; then check_error 'e:foo/bar syscalls/sys_enter_openat if ^' # NO_EP_FILTER fi +if grep -q 'this_cpu_read()' README; then + check_error 'e:foo/bar syscalls/sys_enter_openat arg=^this_cpu_read(file)' # NOSUP_PERCPU +fi + exit 0 diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc index fee479295e2f..e9d7e6919c7f 100644 --- a/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc @@ -112,6 +112,18 @@ check_error 'f vfs_read%return $retval->^foo' # NO_PTR_STRCT check_error 'f vfs_read file->^foo' # NO_BTF_FIELD check_error 'f vfs_read file^-.foo' # BAD_HYPHEN check_error 'f vfs_read ^file:string' # BAD_TYPE4STR +if grep -qF "[(structname" README ; then +check_error 'f vfs_read arg1=(task_struct)file^' # TYPECAST_REQ_FIELD +check_error 'f vfs_read arg1=(a)((b)((c)(^(d)file->d)->c)->b)->a' # TOO_MANY_NESTED +check_error 'f vfs_read arg1=(task_struct,^in_execve)file->comm' # TYPECAST_NOT_ALIGNED +check_error 'f vfs_read arg1=(task_struct,^foo_bar)file->pid' # NO_BTF_FIELD +check_error 'f vfs_read arg1=(^task_struct1234)file->pid' # NO_PTR_STRCT +check_error 'f vfs_read arg1=(task_struct,se^->group_node)file->comm' # TYPECAST_BAD_ARROW +check_error 'f vfs_read arg1=(task_struct,^->pid)file->comm' # NO_BTF_FIELD +check_error 'f vfs_read arg1=(task_struct,^.pid)file->comm' # NO_BTF_FIELD +check_error 'f vfs_read arg1=(task_struct,^.)file->comm' # NO_BTF_FIELD +check_error 'f vfs_read arg1=(task_struct)^@symbol+10->comm' # TYPECAST_SYM_OFFSET +fi fi else diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc index 8f1c58f0c239..21ce8414459f 100644 --- a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc @@ -115,6 +115,18 @@ check_error 'p vfs_read+20 ^$arg*' # NOFENTRY_ARGS check_error 'p vfs_read ^hoge' # NO_BTFARG check_error 'p kfree ^$arg10' # NO_BTFARG (exceed the number of parameters) check_error 'r kfree ^$retval' # NO_RETVAL +if grep -qF "[(structname" README ; then +check_error 'p vfs_read arg1=(task_struct)file^' # TYPECAST_REQ_FIELD +check_error 'p vfs_read arg1=(a)((b)((c)(^(d)file->d)->c)->b)->a' # TOO_MANY_NESTED +check_error 'p vfs_read arg1=(task_struct,^in_execve)file->comm' # TYPECAST_NOT_ALIGNED +check_error 'p vfs_read arg1=(task_struct,^foo_bar)file->pid' # NO_BTF_FIELD +check_error 'p vfs_read arg1=(^task_struct1234)file->pid' # NO_PTR_STRCT +check_error 'p vfs_read arg1=(task_struct,se^->group_node)file->comm' # TYPECAST_BAD_ARROW +check_error 'p vfs_read arg1=(task_struct,^->pid)file->comm' # NO_BTF_FIELD +check_error 'p vfs_read arg1=(task_struct,^.pid)file->comm' # NO_BTF_FIELD +check_error 'p vfs_read arg1=(task_struct,^.)file->comm' # NO_BTF_FIELD +check_error 'p vfs_read arg1=(task_struct)^@symbol+10->comm' # TYPECAST_SYM_OFFSET +fi else check_error 'p vfs_read ^$arg*' # NOSUP_BTFARG fi diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/uprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/kprobe/uprobe_syntax_errors.tc index c817158b99db..e12dc967ec76 100644 --- a/tools/testing/selftests/ftrace/test.d/kprobe/uprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/kprobe/uprobe_syntax_errors.tc @@ -28,4 +28,9 @@ if grep -q ".*symstr.*" README; then check_error 'p /bin/sh:10 $stack0:^symstr' # BAD_TYPE fi +# $current is not supported by uprobe +if grep -q "\$current.*" README; then +check_error 'p /bin/sh:10 ^$current:u8' # BAD_VAR +fi + exit 0 From b20c8177092f3ff4804e0271e3fd338d34e09097 Mon Sep 17 00:00:00 2001 From: "Markus Schneider-Pargmann (The Capable Hub)" Date: Tue, 28 Apr 2026 10:30:29 +0200 Subject: [PATCH 16/37] tracing: fprobe: Remove __packed from generic __fprobe_header fp pointer and unsigned long have the same size on all relevant architectures that build Linux. Furthermore this struct is only used in architectures that do not set ARCH_DEFINE_ENCODE_FPROBE_HEADER which is set only for 64bit architectures (apart from LoongArch). Both fields are aligned on these architectures so the struct with __packed and without it are the same. Remove the __packed as it is unnecessary. [Masami: Removed Fixes tag because this is not fixing any problem.] Link: https://lore.kernel.org/all/20260428-topic-fprobe-packed-v7-1-v1-1-9abc9b866b4c@baylibre.com/ Signed-off-by: Markus Schneider-Pargmann (The Capable Hub) Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/fprobe.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/trace/fprobe.c b/kernel/trace/fprobe.c index f215990b9061..2727f8861fa3 100644 --- a/kernel/trace/fprobe.c +++ b/kernel/trace/fprobe.c @@ -181,7 +181,7 @@ static inline void read_fprobe_header(unsigned long *stack, struct __fprobe_header { struct fprobe *fp; unsigned long size_words; -} __packed; +}; #define FPROBE_HEADER_SIZE_IN_LONG SIZE_IN_LONG(sizeof(struct __fprobe_header)) From 2590d67a1ce85a8d26980f4ff512ab7bcc1b8d89 Mon Sep 17 00:00:00 2001 From: Masami Hiramatsu Date: Tue, 14 Jul 2026 10:09:36 +0900 Subject: [PATCH 17/37] tracing/probes: Refactor parse_probe_vars() Decompose parse_probe_vars() by extracting retval, stack, current task struct, and function argument parsing logic into dedicated static helper functions (parse_probe_var_retval, parse_probe_var_stack, parse_probe_var_current, and parse_probe_var_arg). This simplifies parse_probe_vars() and improves its readability. Link: https://lore.kernel.org/all/178399137612.27810.14869178899276413372.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 249 ++++++++++++++++++++++--------------- 1 file changed, 146 insertions(+), 103 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 18c212122344..8af97e29f096 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -1319,6 +1319,133 @@ NOKPROBE_SYMBOL(store_trace_entry_data) #define PARAM_MAX_STACK (THREAD_SIZE / sizeof(unsigned long)) +static int parse_probe_var_retval(char *orig_arg, char *arg, + struct fetch_insn **pcode, + struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + struct fetch_insn *code = *pcode; + + if (!(ctx->flags & TPARG_FL_RETURN)) { + trace_probe_log_err(ctx->offset, RETVAL_ON_PROBE); + return -EINVAL; + } + if (!(ctx->flags & TPARG_FL_KERNEL) || + !IS_ENABLED(CONFIG_PROBE_EVENTS_BTF_ARGS)) { + code->op = FETCH_OP_RETVAL; + return 0; + } + return parse_btf_arg(orig_arg, pcode, end, ctx); +} + +static int parse_probe_var_stack(char *arg, int len, struct fetch_insn *code, + struct traceprobe_parse_context *ctx) +{ + unsigned long param; + int ret; + + if (arg[len] == '\0') { + code->op = FETCH_OP_STACKP; + return 0; + } + + if (isdigit(arg[len])) { + ret = kstrtoul(arg + len, 10, ¶m); + if (ret) { + trace_probe_log_err(ctx->offset, BAD_VAR); + return ret; + } + + if ((ctx->flags & TPARG_FL_KERNEL) && + param > PARAM_MAX_STACK) { + trace_probe_log_err(ctx->offset, BAD_STACK_NUM); + return -EINVAL; + } + code->op = FETCH_OP_STACK; + code->param = (unsigned int)param; + return 0; + } + + trace_probe_log_err(ctx->offset, BAD_VAR); + return -EINVAL; +} + +static int parse_probe_var_current(char *orig_arg, char *arg, + struct fetch_insn **pcode, + struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + struct fetch_insn *code = *pcode; + + /* $current is only supported by kernel probe. */ + if (!(ctx->flags & TPARG_FL_KERNEL)) { + trace_probe_log_err(ctx->offset, BAD_VAR); + return -EINVAL; + } + arg += strlen("current"); + if (*arg == '-' && IS_ENABLED(CONFIG_PROBE_EVENTS_BTF_ARGS)) + return parse_btf_arg(orig_arg, pcode, end, ctx); + + if (*arg != '\0') { + trace_probe_log_err(ctx->offset, BAD_VAR); + return -EINVAL; + } + + code->op = FETCH_OP_CURRENT; + return 0; +} + +#ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API +static int parse_probe_var_arg(char *arg, int len, struct fetch_insn *code, + struct traceprobe_parse_context *ctx) +{ + unsigned long param; + int ret; + + ret = kstrtoul(arg + len, 10, ¶m); + if (ret) { + trace_probe_log_err(ctx->offset, BAD_VAR); + return ret; + } + + if (!param || param > PARAM_MAX_STACK) { + trace_probe_log_err(ctx->offset, BAD_ARG_NUM); + return -EINVAL; + } + param--; /* argN starts from 1, but internal arg[N] starts from 0 */ + + if (tparg_is_function_entry(ctx->flags)) { + code->op = FETCH_OP_ARG; + code->param = (unsigned int)param; + /* + * The tracepoint probe will probe a stub function, and the + * first parameter of the stub is a dummy and should be ignored. + */ + if (ctx->flags & TPARG_FL_TPOINT) + code->param++; + } else if (tparg_is_function_return(ctx->flags)) { + /* function entry argument access from return probe */ + ret = __store_entry_arg(ctx->tp, param); + if (ret < 0) /* This error should be an internal error */ + return ret; + + code->op = FETCH_OP_EDATA; + code->offset = ret; + } else { + trace_probe_log_err(ctx->offset, NOFENTRY_ARGS); + return -EINVAL; + } + return 0; +} +#else +static int parse_probe_var_arg(char *arg, int len, struct fetch_insn *code, + struct traceprobe_parse_context *ctx) +{ + trace_probe_log_err(ctx->offset, BAD_VAR); + return -EINVAL; +} +#endif + /* Parse $vars. @orig_arg points '$', which syncs to @ctx->offset */ static int parse_probe_vars(char *orig_arg, const struct fetch_type *t, struct fetch_insn **pcode, @@ -1326,60 +1453,13 @@ static int parse_probe_vars(char *orig_arg, const struct fetch_type *t, struct traceprobe_parse_context *ctx) { struct fetch_insn *code = *pcode; - int err = TP_ERR_BAD_VAR; char *arg = orig_arg + 1; - unsigned long param; - int ret = 0; - int len; + int len, ret; if (ctx->flags & TPARG_FL_TEVENT) { - if (parse_trace_event(arg, code, ctx) < 0) { - /* 'comm' should be checked after field parsing. */ - if (strcmp(arg, "comm") == 0 || strcmp(arg, "COMM") == 0) { - code->op = FETCH_OP_COMM; - return 0; - } - goto inval; - } - return 0; - } - - if (str_has_prefix(arg, "retval")) { - if (!(ctx->flags & TPARG_FL_RETURN)) { - err = TP_ERR_RETVAL_ON_PROBE; - goto inval; - } - if (!(ctx->flags & TPARG_FL_KERNEL) || - !IS_ENABLED(CONFIG_PROBE_EVENTS_BTF_ARGS)) { - code->op = FETCH_OP_RETVAL; + ret = parse_trace_event(arg, code, ctx); + if (!ret) return 0; - } - return parse_btf_arg(orig_arg, pcode, end, ctx); - } - - len = str_has_prefix(arg, "stack"); - if (len) { - - if (arg[len] == '\0') { - code->op = FETCH_OP_STACKP; - return 0; - } - - if (isdigit(arg[len])) { - ret = kstrtoul(arg + len, 10, ¶m); - if (ret) - goto inval; - - if ((ctx->flags & TPARG_FL_KERNEL) && - param > PARAM_MAX_STACK) { - err = TP_ERR_BAD_STACK_NUM; - goto inval; - } - code->op = FETCH_OP_STACK; - code->param = (unsigned int)param; - return 0; - } - goto inval; } if (strcmp(arg, "comm") == 0 || strcmp(arg, "COMM") == 0) { @@ -1387,64 +1467,27 @@ static int parse_probe_vars(char *orig_arg, const struct fetch_type *t, return 0; } + /* eprobe only support event fields or '$comm'. */ + if (ctx->flags & TPARG_FL_TEVENT) + goto inval; + + if (str_has_prefix(arg, "retval")) + return parse_probe_var_retval(orig_arg, arg, pcode, end, ctx); + + len = str_has_prefix(arg, "stack"); + if (len) + return parse_probe_var_stack(arg, len, code, ctx); + /* $current returns the address of the current task_struct. */ - if (str_has_prefix(arg, "current")) { - /* $current is only supported by kernel probe. */ - if (!(ctx->flags & TPARG_FL_KERNEL)) { - err = TP_ERR_BAD_VAR; - goto inval; - } - arg += strlen("current"); - if (*arg == '-' && IS_ENABLED(CONFIG_PROBE_EVENTS_BTF_ARGS)) - return parse_btf_arg(orig_arg, pcode, end, ctx); + if (str_has_prefix(arg, "current")) + return parse_probe_var_current(orig_arg, arg, pcode, end, ctx); - if (*arg != '\0') - goto inval; - - code->op = FETCH_OP_CURRENT; - return 0; - } - -#ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API len = str_has_prefix(arg, "arg"); - if (len) { - ret = kstrtoul(arg + len, 10, ¶m); - if (ret) - goto inval; - - if (!param || param > PARAM_MAX_STACK) { - err = TP_ERR_BAD_ARG_NUM; - goto inval; - } - param--; /* argN starts from 1, but internal arg[N] starts from 0 */ - - if (tparg_is_function_entry(ctx->flags)) { - code->op = FETCH_OP_ARG; - code->param = (unsigned int)param; - /* - * The tracepoint probe will probe a stub function, and the - * first parameter of the stub is a dummy and should be ignored. - */ - if (ctx->flags & TPARG_FL_TPOINT) - code->param++; - } else if (tparg_is_function_return(ctx->flags)) { - /* function entry argument access from return probe */ - ret = __store_entry_arg(ctx->tp, param); - if (ret < 0) /* This error should be an internal error */ - return ret; - - code->op = FETCH_OP_EDATA; - code->offset = ret; - } else { - err = TP_ERR_NOFENTRY_ARGS; - goto inval; - } - return 0; - } -#endif + if (len) + return parse_probe_var_arg(arg, len, code, ctx); inval: - __trace_probe_log_err(ctx->offset, err); + trace_probe_log_err(ctx->offset, BAD_VAR); return -EINVAL; } From 7e79d7fbaf1fe30a341ae128ad88f4586efb94f1 Mon Sep 17 00:00:00 2001 From: Masami Hiramatsu Date: Tue, 14 Jul 2026 10:09:45 +0900 Subject: [PATCH 18/37] tracing/probes: Refactor parse_probe_arg() Decompose parse_probe_arg() by extracting register, memory/symbol, dereference, immediate, and default BTF/CPU parsing handlers into dedicated static helper functions (parse_probe_arg_register, parse_probe_arg_mem_symbol, parse_probe_arg_deref, parse_probe_arg_imm, and parse_probe_arg_default). This modularizes the recursive argument parser and improves readability. Link: https://lore.kernel.org/all/178399138581.27810.9471730417467607229.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 302 +++++++++++++++++++++---------------- 1 file changed, 175 insertions(+), 127 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 8af97e29f096..eb9f7bd13c19 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -1516,6 +1516,175 @@ static int __parse_imm_string(char *str, char **pbuf, int offs) return 0; } +static int parse_probe_arg_register(char *arg, struct fetch_insn *code, + struct traceprobe_parse_context *ctx) +{ + int ret; + + if (ctx->flags & (TPARG_FL_TEVENT | TPARG_FL_FPROBE)) { + /* eprobe and fprobe do not handle registers */ + trace_probe_log_err(ctx->offset, BAD_VAR); + return -EINVAL; + } + ret = regs_query_register_offset(arg + 1); + if (ret >= 0) { + code->op = FETCH_OP_REG; + code->param = (unsigned int)ret; + return 0; + } + trace_probe_log_err(ctx->offset, BAD_REG_NAME); + return -EINVAL; +} + +static int parse_probe_arg_mem_symbol(char *arg, struct fetch_insn **pcode, + struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + struct fetch_insn *code = *pcode; + unsigned long param; + long offset = 0; + int ret; + + if (isdigit(arg[1])) { + ret = kstrtoul(arg + 1, 0, ¶m); + if (ret) { + trace_probe_log_err(ctx->offset, BAD_MEM_ADDR); + return ret; + } + /* load address */ + code->op = FETCH_OP_IMM; + code->immediate = param; + } else if (arg[1] == '+') { + /* Kernel probes do not support file offsets */ + if (ctx->flags & TPARG_FL_KERNEL) { + trace_probe_log_err(ctx->offset, FILE_ON_KPROBE); + return -EINVAL; + } + ret = kstrtol(arg + 2, 0, &offset); + if (ret) { + trace_probe_log_err(ctx->offset, BAD_FILE_OFFS); + return ret; + } + + code->op = FETCH_OP_FOFFS; + code->immediate = (unsigned long)offset; + offset = 0; + } else { + /* uprobes don't support symbols */ + if (!(ctx->flags & TPARG_FL_KERNEL)) { + trace_probe_log_err(ctx->offset, SYM_ON_UPROBE); + return -EINVAL; + } + /* Preserve symbol for updating */ + code->op = FETCH_NOP_SYMBOL; + code->data = kstrdup(arg + 1, GFP_KERNEL); + if (!code->data) + return -ENOMEM; + if (++code == end) { + trace_probe_log_err(ctx->offset, TOO_MANY_OPS); + return -EINVAL; + } + code->op = FETCH_OP_IMM; + code->immediate = 0; + } + /* These are fetching from memory */ + if (++code == end) { + trace_probe_log_err(ctx->offset, TOO_MANY_OPS); + return -EINVAL; + } + *pcode = code; + code->op = FETCH_OP_DEREF; + code->offset = offset; + return 0; +} + +static int parse_probe_arg_deref(char *arg, struct fetch_insn **pcode, + struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + int deref = FETCH_OP_DEREF; + long offset = 0; + char *tmp; + int ret; + + if (arg[1] == 'u') { + deref = FETCH_OP_UDEREF; + arg[1] = arg[0]; + arg++; + } + if (arg[0] == '+') + arg++; /* Skip '+', because kstrtol() rejects it. */ + tmp = strchr(arg, '('); + if (!tmp) { + trace_probe_log_err(ctx->offset, DEREF_NEED_BRACE); + return -EINVAL; + } + *tmp = '\0'; + ret = kstrtol(arg, 0, &offset); + if (ret) { + trace_probe_log_err(ctx->offset, BAD_DEREF_OFFS); + return ret; + } + ctx->offset += (tmp + 1 - arg) + (arg[0] != '-' ? 1 : 0); + arg = tmp + 1; + return handle_dereference(arg, pcode, end, ctx, deref, offset); +} + +static int parse_probe_arg_imm(char *arg, struct fetch_insn *code, + struct traceprobe_parse_context *ctx) +{ + char *tmp; + int ret; + + if (arg[1] == '"') { /* Immediate string */ + ret = __parse_imm_string(arg + 2, &tmp, ctx->offset + 2); + if (ret) + return ret; + code->op = FETCH_OP_IMMSTR; + code->data = tmp; + } else { + ret = str_to_immediate(arg + 1, &code->immediate); + if (ret) { + trace_probe_log_err(ctx->offset + 1, BAD_IMM); + return ret; + } + code->op = FETCH_OP_IMM; + } + return 0; +} + +static int parse_probe_arg_default(char *arg, struct fetch_insn **pcode, + struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + int ret; + + if (str_has_prefix(arg, THIS_CPU_PTR_PREFIX) || + str_has_prefix(arg, THIS_CPU_READ_PREFIX)) { + return parse_this_cpu(arg, pcode, end, ctx); + } + + if (isalpha(arg[0]) || arg[0] == '_') { + /* BTF variable or event field */ + if (ctx->flags & TPARG_FL_TEVENT) { + ret = parse_trace_event(arg, *pcode, ctx); + if (ret < 0) { + trace_probe_log_err(ctx->offset, NO_EVENT_FIELD); + return -EINVAL; + } + return 0; + } + if (!tparg_is_function_entry(ctx->flags) && + !tparg_is_function_return(ctx->flags)) { + trace_probe_log_err(ctx->offset, NOSUP_BTFARG); + return -EINVAL; + } + return parse_btf_arg(arg, pcode, end, ctx); + } + + return 0; +} + /* Recursive argument parser */ static int parse_probe_arg(char *arg, const struct fetch_type *type, @@ -1523,151 +1692,30 @@ parse_probe_arg(char *arg, const struct fetch_type *type, struct traceprobe_parse_context *ctx) { struct fetch_insn *code = *pcode; - unsigned long param; - int deref = FETCH_OP_DEREF; - long offset = 0; - char *tmp; - int ret = 0; + int ret; switch (arg[0]) { case '$': ret = parse_probe_vars(arg, type, pcode, end, ctx); break; - case '%': /* named register */ - if (ctx->flags & (TPARG_FL_TEVENT | TPARG_FL_FPROBE)) { - /* eprobe and fprobe do not handle registers */ - trace_probe_log_err(ctx->offset, BAD_VAR); - break; - } - ret = regs_query_register_offset(arg + 1); - if (ret >= 0) { - code->op = FETCH_OP_REG; - code->param = (unsigned int)ret; - ret = 0; - } else - trace_probe_log_err(ctx->offset, BAD_REG_NAME); + ret = parse_probe_arg_register(arg, code, ctx); break; - case '@': /* memory, file-offset or symbol */ - if (isdigit(arg[1])) { - ret = kstrtoul(arg + 1, 0, ¶m); - if (ret) { - trace_probe_log_err(ctx->offset, BAD_MEM_ADDR); - break; - } - /* load address */ - code->op = FETCH_OP_IMM; - code->immediate = param; - } else if (arg[1] == '+') { - /* Kernel probes do not support file offsets */ - if (ctx->flags & TPARG_FL_KERNEL) { - trace_probe_log_err(ctx->offset, FILE_ON_KPROBE); - return -EINVAL; - } - ret = kstrtol(arg + 2, 0, &offset); - if (ret) { - trace_probe_log_err(ctx->offset, BAD_FILE_OFFS); - break; - } - - code->op = FETCH_OP_FOFFS; - code->immediate = (unsigned long)offset; // imm64? - offset = 0; - } else { - /* uprobes don't support symbols */ - if (!(ctx->flags & TPARG_FL_KERNEL)) { - trace_probe_log_err(ctx->offset, SYM_ON_UPROBE); - return -EINVAL; - } - /* Preserve symbol for updating */ - code->op = FETCH_NOP_SYMBOL; - code->data = kstrdup(arg + 1, GFP_KERNEL); - if (!code->data) - return -ENOMEM; - if (++code == end) { - trace_probe_log_err(ctx->offset, TOO_MANY_OPS); - return -EINVAL; - } - code->op = FETCH_OP_IMM; - code->immediate = 0; - } - /* These are fetching from memory */ - if (++code == end) { - trace_probe_log_err(ctx->offset, TOO_MANY_OPS); - return -EINVAL; - } - *pcode = code; - code->op = FETCH_OP_DEREF; - code->offset = offset; + ret = parse_probe_arg_mem_symbol(arg, pcode, end, ctx); break; - case '+': /* deref memory */ case '-': - if (arg[1] == 'u') { - deref = FETCH_OP_UDEREF; - arg[1] = arg[0]; - arg++; - } - if (arg[0] == '+') - arg++; /* Skip '+', because kstrtol() rejects it. */ - tmp = strchr(arg, '('); - if (!tmp) { - trace_probe_log_err(ctx->offset, DEREF_NEED_BRACE); - return -EINVAL; - } - *tmp = '\0'; - ret = kstrtol(arg, 0, &offset); - if (ret) { - trace_probe_log_err(ctx->offset, BAD_DEREF_OFFS); - break; - } - ctx->offset += (tmp + 1 - arg) + (arg[0] != '-' ? 1 : 0); - arg = tmp + 1; - ret = handle_dereference(arg, pcode, end, ctx, deref, offset); - if (ret < 0) - return ret; + ret = parse_probe_arg_deref(arg, pcode, end, ctx); break; case '\\': /* Immediate value */ - if (arg[1] == '"') { /* Immediate string */ - ret = __parse_imm_string(arg + 2, &tmp, ctx->offset + 2); - if (ret) - break; - code->op = FETCH_OP_IMMSTR; - code->data = tmp; - } else { - ret = str_to_immediate(arg + 1, &code->immediate); - if (ret) - trace_probe_log_err(ctx->offset + 1, BAD_IMM); - else - code->op = FETCH_OP_IMM; - } + ret = parse_probe_arg_imm(arg, code, ctx); break; case '(': ret = handle_typecast(arg, pcode, end, ctx); break; default: - if (str_has_prefix(arg, THIS_CPU_PTR_PREFIX) || - str_has_prefix(arg, THIS_CPU_READ_PREFIX)) { - ret = parse_this_cpu(arg, pcode, end, ctx); - } else if (isalpha(arg[0]) || arg[0] == '_') { - /* BTF variable or event field*/ - if (ctx->flags & TPARG_FL_TEVENT) { - ret = parse_trace_event(arg, *pcode, ctx); - if (ret < 0) { - trace_probe_log_err(ctx->offset, - NO_EVENT_FIELD); - return -EINVAL; - } - break; - } - if (!tparg_is_function_entry(ctx->flags) && - !tparg_is_function_return(ctx->flags)) { - trace_probe_log_err(ctx->offset, NOSUP_BTFARG); - return -EINVAL; - } - ret = parse_btf_arg(arg, pcode, end, ctx); - } + ret = parse_probe_arg_default(arg, pcode, end, ctx); break; } if (!ret && code->op == FETCH_OP_NOP) { From 0b420da72e45bc71fc2a8c9b230cd37585452188 Mon Sep 17 00:00:00 2001 From: Masami Hiramatsu Date: Tue, 14 Jul 2026 10:09:55 +0900 Subject: [PATCH 19/37] tracing/probes: Sort ERRORS list in trace_probe.h alphabetically Sort the C-macro ERRORS list alphabetically in trace_probe.h to make it easier to find and maintain error entries. Link: https://lore.kernel.org/all/178399139516.27810.14930549550788208142.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.h | 170 ++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index e6268a8dc378..e64e323244a5 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -512,95 +512,95 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call, #undef ERRORS #define ERRORS \ - C(FILE_NOT_FOUND, "Failed to find the given file"), \ - C(NO_REGULAR_FILE, "Not a regular file"), \ - C(BAD_REFCNT, "Invalid reference counter offset"), \ - C(REFCNT_OPEN_BRACE, "Reference counter brace is not closed"), \ - C(BAD_REFCNT_SUFFIX, "Reference counter has wrong suffix"), \ - C(BAD_UPROBE_OFFS, "Invalid uprobe offset"), \ - C(BAD_MAXACT_TYPE, "Maxactive is only for function exit"), \ - C(BAD_MAXACT, "Invalid maxactive number"), \ - C(MAXACT_TOO_BIG, "Maxactive is too big"), \ - C(BAD_PROBE_ADDR, "Invalid probed address or symbol"), \ - C(NON_UNIQ_SYMBOL, "The symbol is not unique"), \ - C(BAD_RETPROBE, "Retprobe address must be an function entry"), \ - C(NO_TRACEPOINT, "Tracepoint is not found"), \ - C(BAD_TP_NAME, "Invalid character in tracepoint name"),\ - C(BAD_ADDR_SUFFIX, "Invalid probed address suffix"), \ - C(NO_GROUP_NAME, "Group name is not specified"), \ - C(GROUP_TOO_LONG, "Group name is too long"), \ - C(BAD_GROUP_NAME, "Group name must follow the same rules as C identifiers"), \ - C(NO_EVENT_NAME, "Event name is not specified"), \ - C(EVENT_TOO_LONG, "Event name is too long"), \ - C(BAD_EVENT_NAME, "Event name must follow the same rules as C identifiers"), \ - C(EVENT_EXIST, "Given group/event name is already used by another event"), \ - C(RETVAL_ON_PROBE, "$retval is not available on probe"), \ - C(NO_RETVAL, "This function returns 'void' type"), \ - C(BAD_STACK_NUM, "Invalid stack number"), \ - C(BAD_ARG_NUM, "Invalid argument number"), \ - C(BAD_VAR, "Invalid $-variable specified"), \ - C(BAD_REG_NAME, "Invalid register name"), \ - C(BAD_MEM_ADDR, "Invalid memory address"), \ - C(BAD_IMM, "Invalid immediate value"), \ - C(IMMSTR_NO_CLOSE, "String is not closed with '\"'"), \ - C(FILE_ON_KPROBE, "File offset is not available for kernel probes"), \ - C(BAD_FILE_OFFS, "Invalid file offset value"), \ - C(SYM_ON_UPROBE, "Symbol is not available with uprobe"), \ - C(TOO_MANY_OPS, "Dereference is too much nested"), \ - C(DEREF_NEED_BRACE, "Dereference needs a brace"), \ - C(BAD_DEREF_OFFS, "Invalid dereference offset"), \ - C(DEREF_OPEN_BRACE, "Dereference brace is not closed"), \ - C(COMM_CANT_DEREF, "$comm can not be dereferenced"), \ - C(BAD_FETCH_ARG, "Invalid fetch argument"), \ - C(ARRAY_NO_CLOSE, "Array is not closed"), \ - C(BAD_ARRAY_SUFFIX, "Array has wrong suffix"), \ - C(BAD_ARRAY_NUM, "Invalid array size"), \ - C(ARRAY_TOO_BIG, "Array number is too big"), \ - C(BAD_TYPE, "Unknown type is specified"), \ - C(BAD_STRING, "String accepts only memory argument"), \ - C(BAD_SYMSTRING, "Symbol String doesn't accept data/userdata"), \ - C(BAD_BITFIELD, "Invalid bitfield"), \ - C(ARG_NAME_TOO_LONG, "Argument name is too long"), \ - C(NO_ARG_NAME, "Argument name is not specified"), \ - C(BAD_ARG_NAME, "Argument name must follow the same rules as C identifiers"), \ - C(USED_ARG_NAME, "This argument name is already used"), \ - C(ARG_TOO_LONG, "Argument expression is too long"), \ - C(NO_ARG_BODY, "No argument expression"), \ - C(BAD_INSN_BNDRY, "Probe point is not an instruction boundary"),\ - C(FAIL_REG_PROBE, "Failed to register probe event"),\ - C(DIFF_PROBE_TYPE, "Probe type is different from existing probe"),\ - C(DIFF_ARG_TYPE, "Argument type or name is different from existing probe"),\ - C(SAME_PROBE, "There is already the exact same probe event"),\ - C(NO_EVENT_INFO, "This requires both group and event name to attach"),\ - C(BAD_ATTACH_EVENT, "Attached event does not exist"),\ - C(BAD_ATTACH_ARG, "Attached event does not have this field"),\ - C(NO_EP_FILTER, "No filter rule after 'if'"), \ - C(NOSUP_BTFARG, "BTF is not available or not supported"), \ - C(NO_BTFARG, "This variable is not found at this probe point"),\ - C(NO_BTF_ENTRY, "No BTF entry for this probe point"), \ - C(BAD_VAR_ARGS, "$arg* must be an independent parameter without name etc."),\ - C(NOFENTRY_ARGS, "$arg* can be used only on function entry or exit"), \ - C(DOUBLE_ARGS, "$arg* can be used only once in the parameters"), \ - C(ARGS_2LONG, "$arg* failed because the argument list is too long"), \ C(ARGIDX_2BIG, "$argN index is too big"), \ - C(NO_PTR_STRCT, "This is not a pointer to union/structure."), \ - C(NOSUP_DAT_ARG, "Non pointer structure/union argument is not supported."),\ + C(ARGS_2LONG, "$arg* failed because the argument list is too long"), \ + C(ARG_NAME_TOO_LONG, "Argument name is too long"), \ + C(ARG_TOO_LONG, "Argument expression is too long"), \ + C(ARRAY_NO_CLOSE, "Array is not closed"), \ + C(ARRAY_TOO_BIG, "Array number is too big"), \ + C(BAD_ADDR_SUFFIX, "Invalid probed address suffix"), \ + C(BAD_ARG_NAME, "Argument name must follow the same rules as C identifiers"), \ + C(BAD_ARG_NUM, "Invalid argument number"), \ + C(BAD_ARRAY_NUM, "Invalid array size"), \ + C(BAD_ARRAY_SUFFIX, "Array has wrong suffix"), \ + C(BAD_ATTACH_ARG, "Attached event does not have this field"), \ + C(BAD_ATTACH_EVENT, "Attached event does not exist"), \ + C(BAD_BITFIELD, "Invalid bitfield"), \ + C(BAD_BTF_TID, "Failed to get BTF type info."), \ + C(BAD_DEREF_OFFS, "Invalid dereference offset"), \ + C(BAD_EVENT_NAME, "Event name must follow the same rules as C identifiers"), \ + C(BAD_FETCH_ARG, "Invalid fetch argument"), \ + C(BAD_FILE_OFFS, "Invalid file offset value"), \ + C(BAD_GROUP_NAME, "Group name must follow the same rules as C identifiers"), \ C(BAD_HYPHEN, "Failed to parse single hyphen. Forgot '>'?"), \ - C(NO_EVENT_FIELD, "This event field is not found."), \ - C(NO_BTF_FIELD, "This field is not found."), \ - C(BAD_BTF_TID, "Failed to get BTF type info."),\ - C(BAD_TYPE4STR, "This type does not fit for string."),\ - C(NEED_STRING_TYPE, "$comm and immediate-string only accepts string type"),\ - C(TOO_MANY_ARGS, "Too many arguments are specified"), \ + C(BAD_IMM, "Invalid immediate value"), \ + C(BAD_INSN_BNDRY, "Probe point is not an instruction boundary"), \ + C(BAD_MAXACT, "Invalid maxactive number"), \ + C(BAD_MAXACT_TYPE, "Maxactive is only for function exit"), \ + C(BAD_MEM_ADDR, "Invalid memory address"), \ + C(BAD_PROBE_ADDR, "Invalid probed address or symbol"), \ + C(BAD_REFCNT, "Invalid reference counter offset"), \ + C(BAD_REFCNT_SUFFIX, "Reference counter has wrong suffix"), \ + C(BAD_REG_NAME, "Invalid register name"), \ + C(BAD_RETPROBE, "Retprobe address must be an function entry"), \ + C(BAD_STACK_NUM, "Invalid stack number"), \ + C(BAD_STRING, "String accepts only memory argument"), \ + C(BAD_SYMSTRING, "Symbol String doesn't accept data/userdata"), \ + C(BAD_TP_NAME, "Invalid character in tracepoint name"), \ + C(BAD_TYPE, "Unknown type is specified"), \ + C(BAD_TYPE4STR, "This type does not fit for string."), \ + C(BAD_UPROBE_OFFS, "Invalid uprobe offset"), \ + C(BAD_VAR, "Invalid $-variable specified"), \ + C(BAD_VAR_ARGS, "$arg* must be an independent parameter without name etc."), \ + C(COMM_CANT_DEREF, "$comm can not be dereferenced"), \ + C(DEREF_NEED_BRACE, "Dereference needs a brace"), \ + C(DEREF_OPEN_BRACE, "Dereference brace is not closed"), \ + C(DIFF_ARG_TYPE, "Argument type or name is different from existing probe"), \ + C(DIFF_PROBE_TYPE, "Probe type is different from existing probe"), \ + C(DOUBLE_ARGS, "$arg* can be used only once in the parameters"), \ + C(EVENT_EXIST, "Given group/event name is already used by another event"), \ + C(EVENT_TOO_BIG, "Event too big (too many fields?)"), \ + C(EVENT_TOO_LONG, "Event name is too long"), \ + C(FAIL_REG_PROBE, "Failed to register probe event"), \ + C(FILE_NOT_FOUND, "Failed to find the given file"), \ + C(FILE_ON_KPROBE, "File offset is not available for kernel probes"), \ + C(GROUP_TOO_LONG, "Group name is too long"), \ + C(IMMSTR_NO_CLOSE, "String is not closed with '\"'"), \ + C(MAXACT_TOO_BIG, "Maxactive is too big"), \ + C(NEED_STRING_TYPE, "$comm and immediate-string only accepts string type"), \ + C(NOFENTRY_ARGS, "$arg* can be used only on function entry or exit"), \ + C(NON_UNIQ_SYMBOL, "The symbol is not unique"), \ + C(NOSUP_BTFARG, "BTF is not available or not supported"), \ + C(NOSUP_DAT_ARG, "Non pointer structure/union argument is not supported."), \ + C(NOSUP_PERCPU, "Per-cpu variable access is only for kernel probes"), \ + C(NO_ARG_BODY, "No argument expression"), \ + C(NO_ARG_NAME, "Argument name is not specified"), \ + C(NO_BTFARG, "This variable is not found at this probe point"), \ + C(NO_BTF_ENTRY, "No BTF entry for this probe point"), \ + C(NO_BTF_FIELD, "This field is not found."), \ + C(NO_EP_FILTER, "No filter rule after 'if'"), \ + C(NO_EVENT_FIELD, "This event field is not found."), \ + C(NO_EVENT_INFO, "This requires both group and event name to attach"), \ + C(NO_EVENT_NAME, "Event name is not specified"), \ + C(NO_GROUP_NAME, "Group name is not specified"), \ + C(NO_PTR_STRCT, "This is not a pointer to union/structure."), \ + C(NO_REGULAR_FILE, "Not a regular file"), \ + C(NO_RETVAL, "This function returns 'void' type"), \ + C(NO_TRACEPOINT, "Tracepoint is not found"), \ + C(REFCNT_OPEN_BRACE, "Reference counter brace is not closed"), \ + C(RETVAL_ON_PROBE, "$retval is not available on probe"), \ + C(SAME_PROBE, "There is already the exact same probe event"), \ + C(SYM_ON_UPROBE, "Symbol is not available with uprobe"), \ + C(TOO_MANY_ARGS, "Too many arguments are specified"), \ C(TOO_MANY_EARGS, "Too many entry arguments specified"), \ - C(EVENT_TOO_BIG, "Event too big (too many fields?)"), \ - C(TYPECAST_NOT_EVENT, "Typecasts are only for eprobe fields"), \ + C(TOO_MANY_NESTED, "Too many nested typecasts/dereferences"), \ + C(TOO_MANY_OPS, "Dereference is too much nested"), \ + C(TYPECAST_BAD_ARROW, "Typecast field option does not support -> operator"), \ + C(TYPECAST_NOT_ALIGNED, "Typecast field option is not byte-aligned"), \ + C(TYPECAST_NOT_EVENT, "Typecasts are only for eprobe fields"), \ C(TYPECAST_REQ_FIELD, "Typecast requires a field access"), \ - C(TOO_MANY_NESTED, "Too many nested typecasts/dereferences"), \ - C(TYPECAST_SYM_OFFSET, "@SYM+/-OFFSET with typecast needs parentheses"), \ - C(TYPECAST_NOT_ALIGNED, "Typecast field option is not byte-aligned"), \ - C(TYPECAST_BAD_ARROW, "Typecast field option does not support -> operator"), \ - C(NOSUP_PERCPU, "Per-cpu variable access is only for kernel probes"), + C(TYPECAST_SYM_OFFSET, "@SYM+/-OFFSET with typecast needs parentheses"), \ + C(USED_ARG_NAME, "This argument name is already used"), #undef C #define C(a, b) TP_ERR_##a From 47e4ec68716624642f7108535960835bae7eff13 Mon Sep 17 00:00:00 2001 From: Masami Hiramatsu Date: Tue, 14 Jul 2026 10:10:04 +0900 Subject: [PATCH 20/37] tracing/probes: Extend max length of argument string To support BTF argument parsing (such as accessing fields within nested structures via typecasting), the maximum argument string length needs to be extended. Extend MAX_ARGSTR_LEN from 63 to 255. Since MAX_ARGSTR_LEN was previously reused to format command heads in trace_*probe_match_command_head() functions, introduce a dedicated MAX_COMMON_HEAD_LEN (63) macro for matching command heads and switch these functions to use the new macro. Link: https://lore.kernel.org/all/178399140457.27810.11387684872148824707.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_fprobe.c | 2 +- kernel/trace/trace_kprobe.c | 2 +- kernel/trace/trace_probe.h | 3 ++- kernel/trace/trace_uprobe.c | 2 +- .../selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc | 2 +- .../selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc | 2 +- .../selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/kernel/trace/trace_fprobe.c b/kernel/trace/trace_fprobe.c index 536781cd4c47..5638a90e61cc 100644 --- a/kernel/trace/trace_fprobe.c +++ b/kernel/trace/trace_fprobe.c @@ -238,7 +238,7 @@ static bool trace_fprobe_is_busy(struct dyn_event *ev) static bool trace_fprobe_match_command_head(struct trace_fprobe *tf, int argc, const char **argv) { - char buf[MAX_ARGSTR_LEN + 1]; + char buf[MAX_COMMON_HEAD_LEN + 1]; if (!argc) return true; diff --git a/kernel/trace/trace_kprobe.c b/kernel/trace/trace_kprobe.c index cfa807d8e760..cc24e992732c 100644 --- a/kernel/trace/trace_kprobe.c +++ b/kernel/trace/trace_kprobe.c @@ -149,7 +149,7 @@ static bool trace_kprobe_is_busy(struct dyn_event *ev) static bool trace_kprobe_match_command_head(struct trace_kprobe *tk, int argc, const char **argv) { - char buf[MAX_ARGSTR_LEN + 1]; + char buf[MAX_COMMON_HEAD_LEN + 1]; if (!argc) return true; diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index e64e323244a5..e6d427910c4f 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -32,7 +32,8 @@ #include "trace_output.h" #define MAX_TRACE_ARGS 128 -#define MAX_ARGSTR_LEN 63 +#define MAX_ARGSTR_LEN 255 +#define MAX_COMMON_HEAD_LEN 63 #define MAX_ARRAY_LEN 64 #define MAX_ARG_NAME_LEN 32 #define MAX_BTF_ARGS_LEN 128 diff --git a/kernel/trace/trace_uprobe.c b/kernel/trace/trace_uprobe.c index b2e264a4b96c..67bd8fd91e3e 100644 --- a/kernel/trace/trace_uprobe.c +++ b/kernel/trace/trace_uprobe.c @@ -281,7 +281,7 @@ static bool trace_uprobe_is_busy(struct dyn_event *ev) static bool trace_uprobe_match_command_head(struct trace_uprobe *tu, int argc, const char **argv) { - char buf[MAX_ARGSTR_LEN + 1]; + char buf[MAX_COMMON_HEAD_LEN + 1]; int len; if (!argc) diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc index e9d7e6919c7f..984ab94df213 100644 --- a/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc @@ -75,7 +75,7 @@ check_error 'f vfs_read ^arg123456789012345678901234567890=@11' # ARG_NAME_TOO_L check_error 'f vfs_read ^=@11' # NO_ARG_NAME check_error 'f vfs_read ^var.1=@11' # BAD_ARG_NAME check_error 'f vfs_read var1=@11 ^var1=@12' # USED_ARG_NAME -check_error 'f vfs_read ^+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(@1234))))))' # ARG_TOO_LONG +check_error 'f vfs_read ^+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(@1234))))))))))))))))))))))))))' # ARG_TOO_LONG check_error 'f vfs_read arg1=^' # NO_ARG_BODY diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc index ffe8ffef4027..2d0905b2c8b7 100644 --- a/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc @@ -61,7 +61,7 @@ check_error 't kfree ^arg123456789012345678901234567890=@11' # ARG_NAME_TOO_LOG check_error 't kfree ^=@11' # NO_ARG_NAME check_error 't kfree ^var.1=@11' # BAD_ARG_NAME check_error 't kfree var1=@11 ^var1=@12' # USED_ARG_NAME -check_error 't kfree ^+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(@1234))))))' # ARG_TOO_LONG +check_error 't kfree ^+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(@1234))))))))))))))))))))))))))' # ARG_TOO_LONG check_error 't kfree arg1=^' # NO_ARG_BODY diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc index 21ce8414459f..d28f63b7e8a9 100644 --- a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc @@ -71,7 +71,7 @@ check_error 'p vfs_read ^arg123456789012345678901234567890=@11' # ARG_NAME_TOO_L check_error 'p vfs_read ^=@11' # NO_ARG_NAME check_error 'p vfs_read ^var.1=@11' # BAD_ARG_NAME check_error 'p vfs_read var1=@11 ^var1=@12' # USED_ARG_NAME -check_error 'p vfs_read ^+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(@1234))))))' # ARG_TOO_LONG +check_error 'p vfs_read ^+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(+1234567(@1234))))))))))))))))))))))))))' # ARG_TOO_LONG check_error 'p vfs_read arg1=^' # NO_ARG_BODY # instruction boundary check is valid on x86 (at this moment) From 1a0ffe10bfb91efc730f2d34c6875c1084b0a462 Mon Sep 17 00:00:00 2001 From: Masami Hiramatsu Date: Tue, 14 Jul 2026 10:10:14 +0900 Subject: [PATCH 21/37] tracing/probes: Eliminate recursion in parse_probe_arg() To avoid potential stack overflows on limited kernel stacks, convert parse_probe_arg() from a recursive function into a loop-based implementation with a simple local state stack. Since recursion is eliminated using a loop with a fixed-size stack in the context, this restricts the dereference nesting depth. The maximum nesting depth of dereferences is now restricted to the same limit as typecasts (TRACEPROBE_MAX_NESTED_LEVEL, which is 8) and reports the same TOO_MANY_NESTED error. Update ftrace selftests to reflect this restriction and simplify the checks. Note that this change slightly alters the behavior of nested dereferencing in fetcharg. Previously, dereferencing without BTF allowed for up to 14 levels of nesting, whereas dereferencing with BTF was limited to 3 levels. With this change, the nesting depth is now limited to 8 levels in both cases. Link: https://lore.kernel.org/all/178399141396.27810.5390060618628718661.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 340 +++++++++++------- kernel/trace/trace_probe.h | 33 +- .../test.d/dynevent/fprobe_syntax_errors.tc | 4 +- .../test.d/dynevent/tprobe_syntax_errors.tc | 2 +- .../test.d/kprobe/kprobe_syntax_errors.tc | 4 +- 5 files changed, 243 insertions(+), 140 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index eb9f7bd13c19..7568f5e68de7 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -356,61 +356,10 @@ parse_probe_arg(char *arg, const struct fetch_type *type, struct fetch_insn **pcode, struct fetch_insn *end, struct traceprobe_parse_context *ctx); -/* handle dereference nested call */ -static inline int handle_dereference(char *arg, struct fetch_insn **pcode, - struct fetch_insn *end, struct traceprobe_parse_context *ctx, - int deref, long offset) +static int parse_this_cpu(char *arg, struct traceprobe_parse_context *ctx) { - const struct fetch_type *type = find_fetch_type(NULL, ctx->flags); - struct fetch_insn *code = *pcode; - int cur_offs = ctx->offset; + bool is_read = false; char *tmp; - int ret; - - tmp = strrchr(arg, ')'); - if (!tmp) { - trace_probe_log_err(ctx->offset + strlen(arg), - DEREF_OPEN_BRACE); - return -EINVAL; - } - - *tmp = '\0'; - ret = parse_probe_arg(arg, type, &code, end, ctx); - if (ret) - return ret; - ctx->offset = cur_offs; - if (code->op == FETCH_OP_COMM || code->op == FETCH_OP_IMMSTR) { - trace_probe_log_err(ctx->offset, COMM_CANT_DEREF); - return -EINVAL; - } - - /* - * this_cpu_ptr(@SYM) does not use SYM value, but use SYM address. - * So we overwrite the last FETCH_OP_DEREF with FETCH_OP_CPU_PTR. - */ - if (!(deref == FETCH_OP_CPU_PTR && *arg == '@')) { - code++; - if (code == end) { - trace_probe_log_err(ctx->offset, TOO_MANY_OPS); - return -EINVAL; - } - } - *pcode = code; - - code->op = deref; - code->offset = offset; - /* Reset the last type if used */ - ctx->last_type = NULL; - return 0; -} - -static int parse_this_cpu(char *arg, struct fetch_insn **pcode, - struct fetch_insn *end, - struct traceprobe_parse_context *ctx) -{ - struct fetch_insn *code; - bool is_ptr = false; - int ret; /* * This is only for kernel probes, excluding eprobe, because per-cpu @@ -424,27 +373,29 @@ static int parse_this_cpu(char *arg, struct fetch_insn **pcode, if (str_has_prefix(arg, THIS_CPU_PTR_PREFIX)) { arg += THIS_CPU_PTR_LEN; ctx->offset += THIS_CPU_PTR_LEN; - is_ptr = true; } else if (str_has_prefix(arg, THIS_CPU_READ_PREFIX)) { arg += THIS_CPU_READ_LEN; ctx->offset += THIS_CPU_READ_LEN; - } else - return -EINVAL; - - ret = handle_dereference(arg, pcode, end, ctx, FETCH_OP_CPU_PTR, 0); - if (ret || is_ptr) - return ret; - - /* this_cpu_read(VAR) -> +0(this_cpu_ptr(VAR)) */ - code = *pcode; - code++; - if (code == end) { - trace_probe_log_err(ctx->offset, TOO_MANY_OPS); + is_read = true; + } else { + trace_probe_log_err(ctx->offset, BAD_FETCH_ARG); return -EINVAL; } - code->op = FETCH_OP_DEREF; - code->offset = 0; - *pcode = code; + + tmp = strrchr(arg, ')'); + if (!tmp) { + trace_probe_log_err(ctx->offset + strlen(arg), + DEREF_OPEN_BRACE); + return -EINVAL; + } + *tmp = '\0'; + + ctx->stack[ctx->depth].type = STATE_DEREF; + ctx->stack[ctx->depth].deref.deref = FETCH_OP_CPU_PTR; + ctx->stack[ctx->depth].deref.offset = 0; + ctx->stack[ctx->depth].deref.cur_offs = ctx->offset; + ctx->stack[ctx->depth].deref.inner_arg = arg; + ctx->stack[ctx->depth].deref.is_cpu_read = is_read; return 0; } @@ -1007,14 +958,12 @@ static char *find_matched_close_paren(char *s) return NULL; } -static int handle_typecast(char *arg, struct fetch_insn **pcode, - struct fetch_insn *end, - struct traceprobe_parse_context *ctx) +static int handle_typecast(char *arg, struct traceprobe_parse_context *ctx) { int orig_offset = ctx->offset; char *close; char *tmp; - int ret; + char *fieldname; if (!(tparg_is_event_probe(ctx->flags) || tparg_is_function_entry(ctx->flags) || @@ -1028,12 +977,6 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, * For example: (STRUCT)VAR->FIELD and (STRUCT)(VAR)->FIELD are same. * VAR is solved in the nested call. */ - ctx->nested_level++; - if (ctx->nested_level > TRACEPROBE_MAX_NESTED_LEVEL) { - trace_probe_log_err(ctx->offset, TOO_MANY_NESTED); - return -E2BIG; - } - tmp = strchr(arg, ')'); if (!tmp) { trace_probe_log_err(ctx->offset + strlen(arg), @@ -1103,30 +1046,19 @@ static int handle_typecast(char *arg, struct fetch_insn **pcode, } *close = '\0'; - /* We need to parse the nested one */ - ret = parse_probe_arg(tmp, find_fetch_type(NULL, ctx->flags), - pcode, end, ctx); - if (ret < 0) - return ret; - ctx->nested_level--; - clear_struct_btf(ctx); - - /* Let tmp point the field name. */ + /* Let fieldname point the field name. */ if (close[1] == '-') - tmp = close + 3; /* Skip "->" after closing parenthesis */ + fieldname = close + 3; /* Skip "->" after closing parenthesis */ else - tmp = close + 2; /* Skip ">" after inner variable name */ + fieldname = close + 2; /* Skip ">" after inner variable name */ - /* resolve the typecast struct name */ - ctx->offset = orig_offset + 1; /* for the '(' */ - ret = parse_btf_casttype(arg + 1, ctx); - if (ret < 0) - return ret; - - ctx->offset = orig_offset + tmp - arg; - ret = parse_btf_field(tmp, ctx->last_struct, pcode, end, ctx); - ctx->prefix_byteoffs = 0; - return ret; + ctx->stack[ctx->depth].type = STATE_TYPECAST; + ctx->stack[ctx->depth].typecast.casttype = arg + 1; + ctx->stack[ctx->depth].typecast.fieldname = fieldname; + ctx->stack[ctx->depth].typecast.orig_offset = orig_offset; + ctx->stack[ctx->depth].typecast.field_offset_diff = fieldname - arg; + ctx->stack[ctx->depth].typecast.inner_arg = tmp; + return 0; } #else /* !CONFIG_PROBE_EVENTS_BTF_ARGS */ @@ -1171,9 +1103,20 @@ static int check_prepare_btf_string_fetch(char *typename, return 0; } -static int handle_typecast(char *arg, struct fetch_insn **pcode, - struct fetch_insn *end, +static int parse_btf_casttype(char *casttype, + struct traceprobe_parse_context *ctx) +{ + return -EOPNOTSUPP; +} + +static int parse_btf_field(char *fieldname, const struct btf_type *type, + struct fetch_insn **pcode, struct fetch_insn *end, struct traceprobe_parse_context *ctx) +{ + return -EOPNOTSUPP; +} + +static int handle_typecast(char *arg, struct traceprobe_parse_context *ctx) { trace_probe_log_err(ctx->offset, NOSUP_BTFARG); return -EOPNOTSUPP; @@ -1598,9 +1541,7 @@ static int parse_probe_arg_mem_symbol(char *arg, struct fetch_insn **pcode, return 0; } -static int parse_probe_arg_deref(char *arg, struct fetch_insn **pcode, - struct fetch_insn *end, - struct traceprobe_parse_context *ctx) +static int parse_probe_arg_deref(char *arg, struct traceprobe_parse_context *ctx) { int deref = FETCH_OP_DEREF; long offset = 0; @@ -1627,7 +1568,22 @@ static int parse_probe_arg_deref(char *arg, struct fetch_insn **pcode, } ctx->offset += (tmp + 1 - arg) + (arg[0] != '-' ? 1 : 0); arg = tmp + 1; - return handle_dereference(arg, pcode, end, ctx, deref, offset); + + tmp = strrchr(arg, ')'); + if (!tmp) { + trace_probe_log_err(ctx->offset + strlen(arg), + DEREF_OPEN_BRACE); + return -EINVAL; + } + *tmp = '\0'; + + ctx->stack[ctx->depth].type = STATE_DEREF; + ctx->stack[ctx->depth].deref.deref = deref; + ctx->stack[ctx->depth].deref.offset = offset; + ctx->stack[ctx->depth].deref.cur_offs = ctx->offset; + ctx->stack[ctx->depth].deref.inner_arg = arg; + ctx->stack[ctx->depth].deref.is_cpu_read = false; + return 0; } static int parse_probe_arg_imm(char *arg, struct fetch_insn *code, @@ -1659,11 +1615,6 @@ static int parse_probe_arg_default(char *arg, struct fetch_insn **pcode, { int ret; - if (str_has_prefix(arg, THIS_CPU_PTR_PREFIX) || - str_has_prefix(arg, THIS_CPU_READ_PREFIX)) { - return parse_this_cpu(arg, pcode, end, ctx); - } - if (isalpha(arg[0]) || arg[0] == '_') { /* BTF variable or event field */ if (ctx->flags & TPARG_FL_TEVENT) { @@ -1685,11 +1636,56 @@ static int parse_probe_arg_default(char *arg, struct fetch_insn **pcode, return 0; } -/* Recursive argument parser */ -static int -parse_probe_arg(char *arg, const struct fetch_type *type, - struct fetch_insn **pcode, struct fetch_insn *end, - struct traceprobe_parse_context *ctx) +static int parse_probe_arg_nested(char **parg, struct traceprobe_parse_context *ctx) +{ + char *arg = *parg; + int ret; + + while (true) { + /* Determine if this is a nested argument */ + if (arg[0] != '+' && arg[0] != '-' && arg[0] != '(' && + !str_has_prefix(arg, THIS_CPU_PTR_PREFIX) && + !str_has_prefix(arg, THIS_CPU_READ_PREFIX)) + break; + + /* If nested, check the maximum depth limit */ + if (ctx->depth >= TRACEPROBE_MAX_NESTED_LEVEL) { + trace_probe_log_err(ctx->offset, TOO_MANY_NESTED); + return -E2BIG; + } + + /* Perform the actual parsing subroutine calls */ + switch (arg[0]) { + case '+': + case '-': + ret = parse_probe_arg_deref(arg, ctx); + if (ret) + return ret; + arg = ctx->stack[ctx->depth].deref.inner_arg; + break; + case '(': + ret = handle_typecast(arg, ctx); + if (ret) + return ret; + arg = ctx->stack[ctx->depth].typecast.inner_arg; + break; + default: + ret = parse_this_cpu(arg, ctx); + if (ret) + return ret; + arg = ctx->stack[ctx->depth].deref.inner_arg; + break; + } + ctx->depth++; + } + + *parg = arg; + return 0; +} + +static int parse_probe_arg_leaf(char *arg, const struct fetch_type *type, + struct fetch_insn **pcode, struct fetch_insn *end, + struct traceprobe_parse_context *ctx) { struct fetch_insn *code = *pcode; int ret; @@ -1704,26 +1700,112 @@ parse_probe_arg(char *arg, const struct fetch_type *type, case '@': /* memory, file-offset or symbol */ ret = parse_probe_arg_mem_symbol(arg, pcode, end, ctx); break; - case '+': /* deref memory */ - case '-': - ret = parse_probe_arg_deref(arg, pcode, end, ctx); - break; case '\\': /* Immediate value */ ret = parse_probe_arg_imm(arg, code, ctx); break; - case '(': - ret = handle_typecast(arg, pcode, end, ctx); - break; default: ret = parse_probe_arg_default(arg, pcode, end, ctx); break; } - if (!ret && code->op == FETCH_OP_NOP) { + + if (ret) + return ret; + + if (code->op == FETCH_OP_NOP) { /* Parsed, but do not find fetch method */ trace_probe_log_err(ctx->offset, BAD_FETCH_ARG); - ret = -EINVAL; + return -EINVAL; } - return ret; + + return 0; +} + +static int unwind_parse_states(struct fetch_insn **pcode, struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + struct parse_state *state; + struct fetch_insn *code; + int ret; + + while (ctx->depth > 0) { + ctx->depth--; + state = &ctx->stack[ctx->depth]; + + if (state->type == STATE_DEREF) { + code = *pcode; + ctx->offset = state->deref.cur_offs; + if (code->op == FETCH_OP_COMM || code->op == FETCH_OP_IMMSTR) { + trace_probe_log_err(ctx->offset, COMM_CANT_DEREF); + return -EINVAL; + } + + if (!(state->deref.deref == FETCH_OP_CPU_PTR && + *state->deref.inner_arg == '@')) { + code++; + if (code == end) { + trace_probe_log_err(ctx->offset, TOO_MANY_OPS); + return -EINVAL; + } + } + *pcode = code; + + code->op = state->deref.deref; + code->offset = state->deref.offset; + ctx->last_type = NULL; + + if (state->deref.is_cpu_read) { + code = *pcode; + code++; + if (code == end) { + trace_probe_log_err(ctx->offset, TOO_MANY_OPS); + return -EINVAL; + } + code->op = FETCH_OP_DEREF; + code->offset = 0; + *pcode = code; + } + } else if (state->type == STATE_TYPECAST) { + clear_struct_btf(ctx); + + /* resolve the typecast struct name */ + ctx->offset = state->typecast.orig_offset + 1; /* for the '(' */ + ret = parse_btf_casttype(state->typecast.casttype, ctx); + if (ret < 0) + return ret; + + ctx->offset = state->typecast.orig_offset + + state->typecast.field_offset_diff; + ret = parse_btf_field(state->typecast.fieldname, + ctx->last_struct, pcode, + end, ctx); + ctx->prefix_byteoffs = 0; + if (ret < 0) + return ret; + } + } + + return 0; +} + +/* Loop-based (non-recursive) argument parser */ +static int +parse_probe_arg(char *arg, const struct fetch_type *type, + struct fetch_insn **pcode, struct fetch_insn *end, + struct traceprobe_parse_context *ctx) +{ + int ret; + + ctx->depth = 0; + + ret = parse_probe_arg_nested(&arg, ctx); + if (ret) + return ret; + + ret = parse_probe_arg_leaf(arg, type, pcode, end, ctx); + if (ret) + return ret; + + return unwind_parse_states(pcode, end, ctx); } /* Bitfield type needs to be parsed into a fetch function */ @@ -1984,12 +2066,6 @@ static int traceprobe_parse_probe_arg_body(const char *argv, ssize_t *size, ctx); if (ret < 0) goto fail; - /* nested_level must be 0 here, otherwise there is a bug. */ - if (WARN_ON_ONCE(ctx->nested_level)) { - ret = -EINVAL; - goto fail; - } - /* Update storing type if BTF is available */ if (IS_ENABLED(CONFIG_PROBE_EVENTS_BTF_ARGS) && ctx->last_type) { diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index e6d427910c4f..ebdc706e7cb6 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -437,6 +437,34 @@ static inline bool tparg_is_event_probe(unsigned int flags) return !!(flags & TPARG_FL_TEVENT); } +/* Each typecast consumes nested level. So the max number of typecast is 8. */ +#define TRACEPROBE_MAX_NESTED_LEVEL 8 + +enum parse_state_type { + STATE_DEREF, + STATE_TYPECAST, +}; + +struct parse_state { + int type; + union { + struct { + int deref; + long offset; + int cur_offs; + char *inner_arg; + bool is_cpu_read; + } deref; + struct { + char *casttype; + char *fieldname; + int orig_offset; + int field_offset_diff; + char *inner_arg; + } typecast; + }; +}; + struct traceprobe_parse_context { struct trace_event_call *event; /* BTF related parameters */ @@ -453,12 +481,11 @@ struct traceprobe_parse_context { struct trace_probe *tp; unsigned int flags; int offset; - int nested_level; int prefix_byteoffs; /* The byte offset of the prefix field of typecast */ + struct parse_state stack[TRACEPROBE_MAX_NESTED_LEVEL + 1]; + int depth; }; -/* Each typecast consumes nested level. So the max number of typecast is 3. */ -#define TRACEPROBE_MAX_NESTED_LEVEL 3 extern int traceprobe_parse_probe_arg(struct trace_probe *tp, int i, const char *argv, diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc index 984ab94df213..384209968325 100644 --- a/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/dynevent/fprobe_syntax_errors.tc @@ -60,7 +60,7 @@ check_error 'f vfs_read ^&1' # BAD_FETCH_ARG # We've introduced this limitation with array support if grep -q ' \\\[\\\]' README; then -check_error 'f vfs_read +0(^+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(@0))))))))))))))' # TOO_MANY_OPS? +check_error 'f vfs_read +0(+0(+0(+0(+0(+0(+0(+0(^+0(@0)))))))))' # TOO_MANY_NESTED check_error 'f vfs_read +0(@11):u8[10^' # ARRAY_NO_CLOSE check_error 'f vfs_read +0(@11):u8[10]^a' # BAD_ARRAY_SUFFIX check_error 'f vfs_read +0(@11):u8[^10a]' # BAD_ARRAY_NUM @@ -114,7 +114,7 @@ check_error 'f vfs_read file^-.foo' # BAD_HYPHEN check_error 'f vfs_read ^file:string' # BAD_TYPE4STR if grep -qF "[(structname" README ; then check_error 'f vfs_read arg1=(task_struct)file^' # TYPECAST_REQ_FIELD -check_error 'f vfs_read arg1=(a)((b)((c)(^(d)file->d)->c)->b)->a' # TOO_MANY_NESTED +check_error 'f vfs_read arg1=(a)((b)((c)((d)((e)((f)((g)((h)(^(i)file->i)->h)->g)->f)->e)->d)->c)->b)->a' # TOO_MANY_NESTED check_error 'f vfs_read arg1=(task_struct,^in_execve)file->comm' # TYPECAST_NOT_ALIGNED check_error 'f vfs_read arg1=(task_struct,^foo_bar)file->pid' # NO_BTF_FIELD check_error 'f vfs_read arg1=(^task_struct1234)file->pid' # NO_PTR_STRCT diff --git a/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc index 2d0905b2c8b7..72b8652df9ba 100644 --- a/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/dynevent/tprobe_syntax_errors.tc @@ -46,7 +46,7 @@ check_error 't kfree ^&1' # BAD_FETCH_ARG # We've introduced this limitation with array support if grep -q ' \\\[\\\]' README; then -check_error 't kfree +0(^+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(@0))))))))))))))' # TOO_MANY_OPS? +check_error 't kfree +0(+0(+0(+0(+0(+0(+0(+0(^+0(@0)))))))))' # TOO_MANY_NESTED check_error 't kfree +0(@11):u8[10^' # ARRAY_NO_CLOSE check_error 't kfree +0(@11):u8[10]^a' # BAD_ARRAY_SUFFIX check_error 't kfree +0(@11):u8[^10a]' # BAD_ARRAY_NUM diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc index d28f63b7e8a9..b0e6b80ccb01 100644 --- a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc +++ b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_syntax_errors.tc @@ -56,7 +56,7 @@ check_error 'p vfs_read ^&1' # BAD_FETCH_ARG # We've introduced this limitation with array support if grep -q ' \\\[\\\]' README; then -check_error 'p vfs_read +0(^+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(+0(@0))))))))))))))' # TOO_MANY_OPS? +check_error 'p vfs_read +0(+0(+0(+0(+0(+0(+0(+0(^+0(@0)))))))))' # TOO_MANY_NESTED check_error 'p vfs_read +0(@11):u8[10^' # ARRAY_NO_CLOSE check_error 'p vfs_read +0(@11):u8[10]^a' # BAD_ARRAY_SUFFIX check_error 'p vfs_read +0(@11):u8[^10a]' # BAD_ARRAY_NUM @@ -117,7 +117,7 @@ check_error 'p kfree ^$arg10' # NO_BTFARG (exceed the number of parameters) check_error 'r kfree ^$retval' # NO_RETVAL if grep -qF "[(structname" README ; then check_error 'p vfs_read arg1=(task_struct)file^' # TYPECAST_REQ_FIELD -check_error 'p vfs_read arg1=(a)((b)((c)(^(d)file->d)->c)->b)->a' # TOO_MANY_NESTED +check_error 'p vfs_read arg1=(a)((b)((c)((d)((e)((f)((g)((h)(^(i)file->i)->h)->g)->f)->e)->d)->c)->b)->a' # TOO_MANY_NESTED check_error 'p vfs_read arg1=(task_struct,^in_execve)file->comm' # TYPECAST_NOT_ALIGNED check_error 'p vfs_read arg1=(task_struct,^foo_bar)file->pid' # NO_BTF_FIELD check_error 'p vfs_read arg1=(^task_struct1234)file->pid' # NO_PTR_STRCT From 1a416ae446afa42d2d8500ce25bd61c564508721 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Wed, 15 Jul 2026 11:06:18 +0900 Subject: [PATCH 22/37] selftests/ftrace: Add test case for a symbol in a module without module name Add a test case for probing on a symbol in a module without module name. When probing on a symbol in a module, ftrace accepts both the syntax that : and . Current test case only checks the former syntax. This adds a test for the latter one. Link: https://lore.kernel.org/all/169846405196.88147.17766692778800222203.stgit@devnote2/ Signed-off-by: Masami Hiramatsu (Google) --- .../testing/selftests/ftrace/test.d/kprobe/kprobe_module.tc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_module.tc b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_module.tc index 7e74ee11edf9..4b32e1b9a8d3 100644 --- a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_module.tc +++ b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_module.tc @@ -13,6 +13,12 @@ fi MOD=trace_printk FUNC=trace_printk_irq_work +:;: "Add an event on a module function without module name" ;: + +echo "p:event0 $FUNC" > kprobe_events +test -d events/kprobes/event0 || exit_failure +echo "-:kprobes/event0" >> kprobe_events + :;: "Add an event on a module function without specifying event name" ;: echo "p $MOD:$FUNC" > kprobe_events From 323853e2d125ec1a99df61893fb546728f4d738e Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:43:39 +0900 Subject: [PATCH 23/37] tracing/probes: Remove duplicate MAX_ARRAY_LEN macro definition MAX_ARRAY_LEN is defined twice in trace_probe.h. Remove the redundant definition. Link: https://lore.kernel.org/all/178454421926.296567.6649983666070787570.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.h | 1 - 1 file changed, 1 deletion(-) diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index ebdc706e7cb6..b3571c85abb1 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -220,7 +220,6 @@ DECLARE_BASIC_PRINT_TYPE_FUNC(symbol); _ASSIGN_FETCH_TYPE(#ptype, ptype, ftype, sizeof(ftype), sign, atype) #define ASSIGN_FETCH_TYPE_END {} -#define MAX_ARRAY_LEN 64 #ifdef CONFIG_KPROBE_EVENTS bool trace_kprobe_on_func_entry(struct trace_event_call *call); From 794b5640aa7b295623c1d6b6fc2fe229a30aad0d Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:43:48 +0900 Subject: [PATCH 24/37] tracing/probes: Remove redundant boolean conversion in trace_probe_has_single_file() list_is_singular() returns a boolean value, so the double negation (!!) in trace_probe_has_single_file() is redundant. Remove it. Link: https://lore.kernel.org/all/178454422826.296567.4382363406595169533.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index b3571c85abb1..c0f05763811c 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -360,7 +360,7 @@ static inline int trace_probe_unregister_event_call(struct trace_probe *tp) static inline bool trace_probe_has_single_file(struct trace_probe *tp) { - return !!list_is_singular(&tp->event->files); + return list_is_singular(&tp->event->files); } int trace_probe_init(struct trace_probe *tp, const char *event, From 6e9328ed75afc2f2564514df53ff66259f805a2a Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:43:57 +0900 Subject: [PATCH 25/37] tracing/probes: Remove redundant bounds check in trace_probe_compare_arg_type() In trace_probe_compare_arg_type(), prior to entering the comparison loop, a->nr_args and b->nr_args are checked for equality. Since the loop condition is i < a->nr_args, i is guaranteed to be less than b->nr_args inside the loop. Remove the redundant (b->nr_args <= i) check. Link: https://lore.kernel.org/all/178454423769.296567.6694636865644203423.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 7568f5e68de7..91ba4490937b 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -2770,10 +2770,9 @@ int trace_probe_compare_arg_type(struct trace_probe *a, struct trace_probe *b) return b->nr_args + 1; for (i = 0; i < a->nr_args; i++) { - if ((b->nr_args <= i) || - ((a->args[i].type != b->args[i].type) || - (a->args[i].count != b->args[i].count) || - strcmp(a->args[i].name, b->args[i].name))) + if ((a->args[i].type != b->args[i].type) || + (a->args[i].count != b->args[i].count) || + strcmp(a->args[i].name, b->args[i].name)) return i + 1; } From 5428fca0cd542d4fb7beb781c1b44292aedb7915 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:44:07 +0900 Subject: [PATCH 26/37] tracing/probes: Remove unused parameter from parse_probe_var_retval() The parameter 'arg' in parse_probe_var_retval() is unused. Remove it and update its caller accordingly. Link: https://lore.kernel.org/all/178454424711.296567.15352344258939682112.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 91ba4490937b..eb7be3725e85 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -1262,7 +1262,7 @@ NOKPROBE_SYMBOL(store_trace_entry_data) #define PARAM_MAX_STACK (THREAD_SIZE / sizeof(unsigned long)) -static int parse_probe_var_retval(char *orig_arg, char *arg, +static int parse_probe_var_retval(char *orig_arg, struct fetch_insn **pcode, struct fetch_insn *end, struct traceprobe_parse_context *ctx) @@ -1415,7 +1415,7 @@ static int parse_probe_vars(char *orig_arg, const struct fetch_type *t, goto inval; if (str_has_prefix(arg, "retval")) - return parse_probe_var_retval(orig_arg, arg, pcode, end, ctx); + return parse_probe_var_retval(orig_arg, pcode, end, ctx); len = str_has_prefix(arg, "stack"); if (len) From 78d252c935c9c54742443e871b7b39204ecb7a01 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:44:16 +0900 Subject: [PATCH 27/37] tracing/probes: Cleanup pointer arithmetic in store_trace_entry_data() In store_trace_entry_data(), edata is cast to unsigned long for pointer offset arithmetic before being cast back to unsigned long *. Cast edata to u8 * instead. Link: https://lore.kernel.org/all/178454425622.296567.16952341966130280432.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index eb7be3725e85..4899aa5f9230 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -1246,7 +1246,7 @@ void store_trace_entry_data(void *edata, struct trace_probe *tp, struct pt_regs val = regs_get_kernel_argument(regs, code->param); break; case FETCH_OP_ST_EDATA: - *(unsigned long *)((unsigned long)edata + code->offset) = val; + *(unsigned long *)((u8 *)edata + code->offset) = val; break; case FETCH_OP_END: goto end; From 0fb6fd1eb3e7c8649473bd0ea6d897c835740157 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:44:25 +0900 Subject: [PATCH 28/37] tracing/probes: Simplify BTF_KIND_PTR case in fetch_type_from_btf_type() Use a ternary operator for checking IS_ENABLED(CONFIG_64BIT) in the BTF_KIND_PTR case of fetch_type_from_btf_type() to simplify the code. Link: https://lore.kernel.org/all/178454426554.296567.17700307233923830044.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c index 4899aa5f9230..fc62d09ea535 100644 --- a/kernel/trace/trace_probe.c +++ b/kernel/trace/trace_probe.c @@ -495,10 +495,7 @@ static const char *fetch_type_from_btf_type(struct btf *btf, return "s64"; case BTF_KIND_PTR: /* pointer will be converted to "x??" */ - if (IS_ENABLED(CONFIG_64BIT)) - return "x64"; - else - return "x32"; + return IS_ENABLED(CONFIG_64BIT) ? "x64" : "x32"; case BTF_KIND_INT: intdata = btf_type_int(type); if (BTF_INT_ENCODING(intdata) & BTF_INT_SIGNED) { From 36db65d0e5de923909e4d2b13524375b139568ac Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:44:34 +0900 Subject: [PATCH 29/37] tracing/fprobe: Remove redundant snprintf in trace_fprobe_match_command_head() trace_fprobe_match_command_head() copies trace_fprobe_symbol(tf) into a local buffer 'buf' of size MAX_COMMON_HEAD_LEN + 1 using snprintf before comparing with argv[0]. Since trace_fprobe_symbol(tf) already returns a null-terminated string, comparing it directly with argv[0] via strcmp() avoids stack buffer usage and potential symbol truncation at MAX_COMMON_HEAD_LEN. Link: https://lore.kernel.org/all/178454427449.296567.12336315661120939938.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_fprobe.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/kernel/trace/trace_fprobe.c b/kernel/trace/trace_fprobe.c index 5638a90e61cc..566b64853c61 100644 --- a/kernel/trace/trace_fprobe.c +++ b/kernel/trace/trace_fprobe.c @@ -238,13 +238,10 @@ static bool trace_fprobe_is_busy(struct dyn_event *ev) static bool trace_fprobe_match_command_head(struct trace_fprobe *tf, int argc, const char **argv) { - char buf[MAX_COMMON_HEAD_LEN + 1]; - if (!argc) return true; - snprintf(buf, sizeof(buf), "%s", trace_fprobe_symbol(tf)); - if (strcmp(buf, argv[0])) + if (strcmp(trace_fprobe_symbol(tf), argv[0])) return false; argc--; argv++; From 3d785646cc488d3f743e0cb67491e80b69200cfb Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:44:43 +0900 Subject: [PATCH 30/37] tracing/fprobe: Remove redundant memset in fentry_perf_func() fentry_perf_func() calls memset(&entry[1], 0, dsize) prior to calling store_trace_args(). store_trace_args() populates the entry buffer and handles dynamic data fields. Furthermore, passing dsize (the dynamic data byte length) to memset at &entry[1] (the start of fixed trace arguments) is inaccurate as it zeroes from the fixed args area rather than the dynamic data region. Remove this redundant memset call to align with fexit_perf_func() and other probe perf functions. Link: https://lore.kernel.org/all/178454428366.296567.16465331441301815980.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_fprobe.c | 1 - 1 file changed, 1 deletion(-) diff --git a/kernel/trace/trace_fprobe.c b/kernel/trace/trace_fprobe.c index 566b64853c61..ec9105fca4a6 100644 --- a/kernel/trace/trace_fprobe.c +++ b/kernel/trace/trace_fprobe.c @@ -471,7 +471,6 @@ static int fentry_perf_func(struct trace_fprobe *tf, unsigned long entry_ip, regs = ftrace_fill_perf_regs(fregs, regs); entry->ip = entry_ip; - memset(&entry[1], 0, dsize); store_trace_args(&entry[1], &tf->tp, fregs, NULL, sizeof(*entry), dsize); perf_trace_buf_submit(entry, size, rctx, call->event.type, 1, regs, head, NULL); From 5b385fd434c75b6288a36722d606f04444db34eb Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:44:53 +0900 Subject: [PATCH 31/37] tracing/kprobe: Remove redundant memset in kprobe_perf_func() kprobe_perf_func() calls memset(&entry[1], 0, dsize) prior to calling store_trace_args(). store_trace_args() populates the entry buffer and handles dynamic data fields. Remove this redundant memset call to align with kretprobe_perf_func() and other probe perf functions. Link: https://lore.kernel.org/all/178454429309.296567.17379087988750549247.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_kprobe.c | 1 - 1 file changed, 1 deletion(-) diff --git a/kernel/trace/trace_kprobe.c b/kernel/trace/trace_kprobe.c index cc24e992732c..c25f902aa1a6 100644 --- a/kernel/trace/trace_kprobe.c +++ b/kernel/trace/trace_kprobe.c @@ -1721,7 +1721,6 @@ kprobe_perf_func(struct trace_kprobe *tk, struct pt_regs *regs) return 0; entry->ip = (unsigned long)tk->rp.kp.addr; - memset(&entry[1], 0, dsize); store_trace_args(&entry[1], &tk->tp, regs, NULL, sizeof(*entry), dsize); perf_trace_buf_submit(entry, size, rctx, call->event.type, 1, regs, head, NULL); From 5be3a9db600853578559681b2dd20a9bc7dd4fc5 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Mon, 20 Jul 2026 19:45:02 +0900 Subject: [PATCH 32/37] tracing/probes: Fix extra whitespace in trace_probe_kernel.h Clean up extra space after '=' in fetch_store_strlen_user(). Link: https://lore.kernel.org/all/178454430235.296567.9486503882045148585.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.5-flash Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_probe_kernel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kernel/trace/trace_probe_kernel.h b/kernel/trace/trace_probe_kernel.h index bb723eefd7b7..4eb4acfa09ba 100644 --- a/kernel/trace/trace_probe_kernel.h +++ b/kernel/trace/trace_probe_kernel.h @@ -12,7 +12,7 @@ static nokprobe_inline int fetch_store_strlen_user(unsigned long addr) { - const void __user *uaddr = (__force const void __user *)addr; + const void __user *uaddr = (__force const void __user *)addr; return strnlen_user_nofault(uaddr, MAX_STRING_SIZE); } From d0d7dc1a70da6d65bef565f647c41febf3d881dd Mon Sep 17 00:00:00 2001 From: "shaikh.kamal" Date: Sun, 26 Jul 2026 12:26:33 +0530 Subject: [PATCH 33/37] docs: trace: fprobe: fix 'thos' spelling Fix spelling error reported by codespell: thos -> those. No functional change. Link: https://lore.kernel.org/all/20260726065633.20897-1-shaikhkamal2012@gmail.com/ Signed-off-by: shaikh.kamal Signed-off-by: Masami Hiramatsu (Google) --- Documentation/trace/fprobe.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/trace/fprobe.rst b/Documentation/trace/fprobe.rst index 95998b189ae3..46dd46a97c1f 100644 --- a/Documentation/trace/fprobe.rst +++ b/Documentation/trace/fprobe.rst @@ -118,7 +118,7 @@ will be cancelled. @fregs This is the `ftrace_regs` data structure at the entry and exit. This includes the function parameters, or the return values. So user can - access thos values via appropriate `ftrace_regs_*` APIs. + access those values via appropriate `ftrace_regs_*` APIs. @entry_data This is a local storage to share the data between entry and exit handlers. From c00e735410d7269c0c655219cf42846da6b435c7 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Tue, 28 Jul 2026 13:40:11 +0900 Subject: [PATCH 34/37] tracing/probes: Treating longer symbol name on event comparation MAX_COMMON_HEAD_LEN (63) was used to allocate a temporary buffer for formatting command heads in trace_kprobe_match_command_head() and trace_uprobe_match_command_head(). However, the buffer size is too short for some longer symbols. Especially, with rust code, the symbol can be mangled and become very long. Refactor trace_kprobe_match_command_head() to perform direct string comparisons using strcmp() and strncmp(), eliminating the need for a temporary buffer and removing the MAX_COMMON_HEAD_LEN string length restriction on probe symbol names. For trace_uprobe_match_command_head(), since tu->filename is already matched via strncmp(), use a fixed 64-byte stack buffer solely for formatting offset and ref_ctr_offset (which requires at most 39 bytes). With all users converted, remove the MAX_COMMON_HEAD_LEN definition from trace_probe.h. Link: https://lore.kernel.org/all/178521361102.34226.9650586522488974115.stgit@devnote2/ Reported-by: Zhan Xusheng Link: https://lore.kernel.org/all/20260724023317.624074-1-zhanxusheng@xiaomi.com/ Signed-off-by: Masami Hiramatsu (Google) --- kernel/trace/trace_kprobe.c | 24 ++++++++++++++++-------- kernel/trace/trace_probe.h | 1 - kernel/trace/trace_uprobe.c | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/kernel/trace/trace_kprobe.c b/kernel/trace/trace_kprobe.c index c25f902aa1a6..e41a7c113646 100644 --- a/kernel/trace/trace_kprobe.c +++ b/kernel/trace/trace_kprobe.c @@ -149,20 +149,28 @@ static bool trace_kprobe_is_busy(struct dyn_event *ev) static bool trace_kprobe_match_command_head(struct trace_kprobe *tk, int argc, const char **argv) { - char buf[MAX_COMMON_HEAD_LEN + 1]; + char buf[32]; + int len; if (!argc) return true; - if (!tk->symbol) + if (!tk->symbol) { snprintf(buf, sizeof(buf), "0x%p", tk->rp.kp.addr); - else if (tk->rp.kp.offset) - snprintf(buf, sizeof(buf), "%s+%u", - trace_kprobe_symbol(tk), tk->rp.kp.offset); - else - snprintf(buf, sizeof(buf), "%s", trace_kprobe_symbol(tk)); - if (strcmp(buf, argv[0])) + if (strcmp(buf, argv[0])) + return false; + } else if (tk->rp.kp.offset) { + len = strlen(trace_kprobe_symbol(tk)); + if (strncmp(trace_kprobe_symbol(tk), argv[0], len) || + argv[0][len] != '+') + return false; + + snprintf(buf, sizeof(buf), "%u", tk->rp.kp.offset); + if (strcmp(buf, &argv[0][len + 1])) + return false; + } else if (strcmp(trace_kprobe_symbol(tk), argv[0])) return false; + argc--; argv++; return trace_probe_match_command_args(&tk->tp, argc, argv); diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h index c0f05763811c..fba1af092a9b 100644 --- a/kernel/trace/trace_probe.h +++ b/kernel/trace/trace_probe.h @@ -33,7 +33,6 @@ #define MAX_TRACE_ARGS 128 #define MAX_ARGSTR_LEN 255 -#define MAX_COMMON_HEAD_LEN 63 #define MAX_ARRAY_LEN 64 #define MAX_ARG_NAME_LEN 32 #define MAX_BTF_ARGS_LEN 128 diff --git a/kernel/trace/trace_uprobe.c b/kernel/trace/trace_uprobe.c index 67bd8fd91e3e..861d857adadb 100644 --- a/kernel/trace/trace_uprobe.c +++ b/kernel/trace/trace_uprobe.c @@ -281,7 +281,7 @@ static bool trace_uprobe_is_busy(struct dyn_event *ev) static bool trace_uprobe_match_command_head(struct trace_uprobe *tu, int argc, const char **argv) { - char buf[MAX_COMMON_HEAD_LEN + 1]; + char buf[64]; int len; if (!argc) From c13fcd3951dcc19524ce0b3e852844b16cd32b22 Mon Sep 17 00:00:00 2001 From: Rui Qi Date: Fri, 7 Aug 2026 16:15:11 +0800 Subject: [PATCH 35/37] selftests/ftrace: Force C locale in ftracetest Some ftracetest test cases parse command output by matching English field names. Tools such as readelf may localize their output via gettext, while ftracetest currently inherits the user locale from the environment. If a translated field name is printed, parsing can fail even though the tested kernel behavior is unchanged. For example, add_remove_uprobe can fail to find the ELF entry point and then write a uprobe event with a missing offset. Export LC_ALL=C in the top-level ftracetest runner so every test case gets stable command output by default. Link: https://lore.kernel.org/all/20260807081512.2974757-2-qirui.001@bytedance.com/ Signed-off-by: Rui Qi Signed-off-by: Masami Hiramatsu (Google) --- tools/testing/selftests/ftrace/ftracetest | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/testing/selftests/ftrace/ftracetest b/tools/testing/selftests/ftrace/ftracetest index 0a56bf209f6c..a610097794ae 100755 --- a/tools/testing/selftests/ftrace/ftracetest +++ b/tools/testing/selftests/ftrace/ftracetest @@ -7,6 +7,9 @@ # Written by Masami Hiramatsu # +# Keep command output parsing stable regardless of the user's locale. +export LC_ALL=C + usage() { # errno [message] [ ! -z "$2" ] && echo $2 echo "Usage: ftracetest [options] [testcase(s)] [testcase-directory(s)]" From d093d4524bb12eda49c3654cb04ad4663c2471f8 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Sat, 8 Aug 2026 06:44:11 +0900 Subject: [PATCH 36/37] tracing/boot: Add support for eprobe, fprobe, and tprobe events Boot-time tracing currently supports kprobe-events and synthetic-events under per-event configuration options. Extend boot-time tracing to support newly added dynamic probe types: - event probes (eprobe) under the "eprobes" event group - function probes (fprobe) under the "fprobes" event group - tracepoint probes (tprobe) under the "tracepoints" or "tprobes" event group To support this cleanly, update dyn_event_create() in trace_dynevent.c so that passing NULL as the type parameter delegates to create_dyn_event(), allowing generic creation of any registered dynamic event type from a raw command string. Update Documentation/trace/boottime-trace.rst accordingly to describe the new per-event bootconfig options. Link: https://lore.kernel.org/all/178613905149.259829.18185480460810689421.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.6-flash Signed-off-by: Masami Hiramatsu (Google) Changes in v3: - Check return values of strscpy() and snprintf() in trace_boot_add_probe_event() to prevent silent buffer truncation when constructing probe event strings. Changes in v2: - Fix raw command detection logic for eprobes, fprobes, and tprobes by requiring ':' or isspace() after type prefix. - Consolidate duplicate loop logic into trace_boot_add_probe_event() helper function. --- Documentation/trace/boottime-trace.rst | 18 ++++- kernel/trace/trace_boot.c | 97 ++++++++++++++++++++++++++ kernel/trace/trace_dynevent.c | 5 ++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/Documentation/trace/boottime-trace.rst b/Documentation/trace/boottime-trace.rst index 651f3a2c01de..2c3c1fedb92c 100644 --- a/Documentation/trace/boottime-trace.rst +++ b/Documentation/trace/boottime-trace.rst @@ -121,9 +121,21 @@ ftrace.[instance.INSTANCE.]event.synthetic.EVENT.fields = FIELD[, FIELD2[...]] Defines new synthetic event with FIELDs. Each field should be "type varname". -Note that kprobe and synthetic event definitions can be written under -instance node, but those are also visible from other instances. So please -take care for event name conflict. +ftrace.[instance.INSTANCE.]event.eprobes.EVENT.probes = PROBE[, PROBE2[...]] + Defines new event probe based on PROBEs. This option is available only + for the event which group name is "eprobes". + +ftrace.[instance.INSTANCE.]event.fprobes.EVENT.probes = PROBE[, PROBE2[...]] + Defines new fprobe event based on PROBEs. This option is available only + for the event which group name is "fprobes". + +ftrace.[instance.INSTANCE.]event.tracepoints.EVENT.probes = PROBE[, PROBE2[...]] + Defines new tracepoint probe based on PROBEs. This option is available only + for the event which group name is "tracepoints" or "tprobes". + +Note that dynamic event definitions can be written under instance node, but +those are also visible from other instances. So please take care for event +name conflict. Ftrace Histogram Options ------------------------ diff --git a/kernel/trace/trace_boot.c b/kernel/trace/trace_boot.c index 2ca2541c8a58..5da0a8bbb110 100644 --- a/kernel/trace/trace_boot.c +++ b/kernel/trace/trace_boot.c @@ -18,6 +18,7 @@ #include #include "trace.h" +#include "trace_dynevent.h" #define MAX_BUF_LEN 256 @@ -172,6 +173,93 @@ trace_boot_add_synth_event(struct xbc_node *node, const char *event) } #endif +#if defined(CONFIG_EPROBE_EVENTS) || defined(CONFIG_FPROBE_EVENTS) +static int __init +trace_boot_add_probe_event(struct xbc_node *node, const char *group, + const char *event, char type, const char *type_name) +{ + struct xbc_node *anode; + char buf[MAX_BUF_LEN]; + const char *val; + int ret = 0; + + xbc_node_for_each_array_value(node, "probes", anode, val) { + if (val[0] == type && (val[1] == ':' || isspace(val[1]))) { + ret = strscpy(buf, val, MAX_BUF_LEN); + if (ret < 0) { + pr_err("%s command is too long: %s\n", type_name, val); + break; + } + } else { + ret = snprintf(buf, MAX_BUF_LEN, "%c:%s/%s %s", type, group, event, val); + if (ret >= MAX_BUF_LEN || ret < 0) { + pr_err("%s command is too long: %c:%s/%s %s\n", + type_name, type, group, event, val); + ret = -E2BIG; + break; + } + } + + ret = dyn_event_create(buf, NULL); + if (ret) { + pr_err("Failed to add %s: %s\n", type_name, buf); + break; + } + } + + return ret; +} +#endif + +#ifdef CONFIG_EPROBE_EVENTS +static inline int __init +trace_boot_add_eprobe_event(struct xbc_node *node, const char *group, + const char *event) +{ + return trace_boot_add_probe_event(node, group, event, 'e', "eprobe"); +} +#else +static inline int __init +trace_boot_add_eprobe_event(struct xbc_node *node, const char *group, + const char *event) +{ + pr_err("Event probe is not supported.\n"); + return -EOPNOTSUPP; +} +#endif + +#ifdef CONFIG_FPROBE_EVENTS +static inline int __init +trace_boot_add_fprobe_event(struct xbc_node *node, const char *group, + const char *event) +{ + return trace_boot_add_probe_event(node, group, event, 'f', "fprobe"); +} + +static inline int __init +trace_boot_add_tprobe_event(struct xbc_node *node, const char *group, + const char *event) +{ + return trace_boot_add_probe_event(node, group, event, 't', "tprobe"); +} +#else +static inline int __init +trace_boot_add_fprobe_event(struct xbc_node *node, const char *group, + const char *event) +{ + pr_err("Fprobe event is not supported.\n"); + return -EOPNOTSUPP; +} + +static inline int __init +trace_boot_add_tprobe_event(struct xbc_node *node, const char *group, + const char *event) +{ + pr_err("Tracepoint probe is not supported.\n"); + return -EOPNOTSUPP; +} +#endif + #ifdef CONFIG_HIST_TRIGGERS static int __init __printf(3, 4) append_printf(char **bufp, char *end, const char *fmt, ...) @@ -477,6 +565,15 @@ trace_boot_init_one_event(struct trace_array *tr, struct xbc_node *gnode, if (!strcmp(group, "synthetic")) if (trace_boot_add_synth_event(enode, event) < 0) return; + if (!strcmp(group, "eprobes")) + if (trace_boot_add_eprobe_event(enode, group, event) < 0) + return; + if (!strcmp(group, "fprobes")) + if (trace_boot_add_fprobe_event(enode, group, event) < 0) + return; + if (!strcmp(group, "tracepoints") || !strcmp(group, "tprobes")) + if (trace_boot_add_tprobe_event(enode, group, event) < 0) + return; mutex_lock(&event_mutex); file = find_event_file(tr, group, event); diff --git a/kernel/trace/trace_dynevent.c b/kernel/trace/trace_dynevent.c index c4dfbc293bae..6f2e39b797e5 100644 --- a/kernel/trace/trace_dynevent.c +++ b/kernel/trace/trace_dynevent.c @@ -116,6 +116,8 @@ int dyn_event_release(const char *raw_command, struct dyn_event_operations *type return ret; } +static int create_dyn_event(const char *raw_command); + /* * Locked version of event creation. The event creation must be protected by * dyn_event_ops_mutex because of protecting trace_probe_log. @@ -124,6 +126,9 @@ int dyn_event_create(const char *raw_command, struct dyn_event_operations *type) { int ret; + if (!type) + return create_dyn_event(raw_command); + mutex_lock(&dyn_event_ops_mutex); ret = type->create(raw_command); mutex_unlock(&dyn_event_ops_mutex); From 29a86c5e6361caffa7e75e891169e813f82ff688 Mon Sep 17 00:00:00 2001 From: "Masami Hiramatsu (Google)" Date: Wed, 29 Jul 2026 08:29:58 +0900 Subject: [PATCH 37/37] fprobe: Simplify fprobe_remove_ips() by reusing existing helpers fprobe_remove_ips() manually duplicates the unregister and filter-removal logic for both graph and ftrace ops. Simplify it by delegating to the existing fprobe_graph_remove_ips() and fprobe_ftrace_remove_ips() helpers. Link: https://lore.kernel.org/all/178528139798.102586.5349128066643420018.stgit@devnote2/ Assisted-by: Antigravity:gemini-3.6-flash Signed-off-by: Masami Hiramatsu (Google) Reviewed-by: Steven Rostedt --- kernel/trace/fprobe.c | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/kernel/trace/fprobe.c b/kernel/trace/fprobe.c index 2727f8861fa3..8c50b0645e96 100644 --- a/kernel/trace/fprobe.c +++ b/kernel/trace/fprobe.c @@ -464,15 +464,8 @@ static bool fprobe_exists_on_hash(unsigned long ip, bool ftrace) #ifdef CONFIG_MODULES static void fprobe_remove_ips(unsigned long *ips, unsigned int cnt) { - if (!nr_fgraph_fprobes) - __fprobe_graph_unregister(); - else if (cnt) - ftrace_set_filter_ips(&fprobe_graph_ops.ops, ips, cnt, 1, 0); - - if (!nr_ftrace_fprobes) - __fprobe_ftrace_unregister(); - else if (cnt) - ftrace_set_filter_ips(&fprobe_ftrace_ops, ips, cnt, 1, 0); + fprobe_graph_remove_ips(ips, cnt); + fprobe_ftrace_remove_ips(ips, cnt); } #endif #else