On Wed, 26 Aug 2026 11:00:09 +0800
Henry Martin <[email protected]> wrote:

> The fields of a probe-based dynamic event (kprobe, uprobe, eprobe and
> fprobe events) are created in traceprobe_define_arg_fields() by handing
> the probe_arg name/type strings to trace_define_field(), which only
> stores the pointers without copying. Those strings are owned by the
> trace_probe and are freed when that probe is removed.
> 
> An event can have several probes attached. The field list is defined
> only once, by the first probe that registers the event, but it is kept
> alive by any surviving sibling probe. Deleting just that first probe by
> symbol -
> 
>   # primary A: fields are defined from A's args
>   echo 'p:kprobes/ev vfs_read  a1=$arg1' >  kprobe_events
>   # append B: shares A's event call
>   echo 'p:kprobes/ev vfs_write a1=$arg1' >> kprobe_events
>   # delete only A (matched by symbol), B survives
>   echo '-:kprobes/ev vfs_read'           >> kprobe_events
> 
> frees A's args (trace_probe_cleanup() -> traceprobe_free_probe_arg()),
> but trace_probe_unlink() keeps the trace_probe_event because the probe
> list is not empty. The event call stays registered via B while its
> fields now reference freed memory. Any field lookup then reads it, e.g.
> 
>   echo 'a1 == 1' > events/kprobes/ev/filter
> 
>   BUG: KASAN: slab-use-after-free in strcmp+0xa7/0xb0
>   Call Trace:
>    strcmp
>    trace_find_event_field
>    parse_pred
>    process_preds
>    create_filter
>    apply_event_filter
>    event_filter_write
> 
> field->name references parg->name (kstrdup'd, freed with the probe) and,
> for array arguments, field->type references parg->fmt (kmalloc'd, freed
> with the probe) - the scalar type otherwise points at the static
> fmttype rodata, which is safe.
> 
> Have traceprobe_define_arg_fields() duplicate the name and type strings
> and anchor the copies on the trace_probe_event, which embeds the event
> call and outlives every individual probe; trace_probe_event_free()
> releases them.
> 
> The reproducer above triggers reliably; the field lookup and the delete
> both run under event_mutex, so this is a dangling reference after
> removal rather than a race.
> 
> The issue was found by the autokbug dynamic kernel fuzzer at Tencent
> Yunding Lab.
> 
> Fixes: ca89bc071d5e4 ("tracing/kprobe: Add multi-probe per event support")
> Signed-off-by: Henry Martin <[email protected]>

Thanks! this looks good to me.

Hmm, maybe I need to update kselftest to check this issue.
Since the current multiprobe test case does not set any argument, this
was not caught. I have checked this patch with below update.

Let me pick this fix.

Thank you,

diff --git a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_multiprobe.tc 
b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_multiprobe.tc
index f0d5b7777ed7..10633d54130c 100644
--- a/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_multiprobe.tc
+++ b/tools/testing/selftests/ftrace/test.d/kprobe/kprobe_multiprobe.tc
@@ -30,3 +30,27 @@ cat kprobe_events | grep "$DEF2"
 
 :;: "Appending different type must fail" ;:
 ! echo "$DEF1 \$stack" >> kprobe_events
+
+:;: "Remove remaining probe" ;:
+echo "-:$EVENT_NAME" >> kprobe_events
+
+:;: "Define multiprobe with arguments and verify format and filter after 
primary removal" ;:
+DEF1_ARG="p:$EVENT_NAME $SYM1 a1=\$stack"
+DEF2_ARG="p:$EVENT_NAME $SYM2 a1=\$stack"
+echo $DEF1_ARG >> kprobe_events
+echo $DEF2_ARG >> kprobe_events
+
+# Remove primary probe that defined the fields
+echo "-:$EVENT_NAME $SYM1" >> kprobe_events
+grep -q "$DEF2_ARG" kprobe_events
+! grep -q "$DEF1_ARG" kprobe_events
+
+# Verify format and filter on remaining event (reads field->name and 
field->type)
+cat events/$EVENT_NAME/format > /dev/null
+echo 'a1 == 0' > events/$EVENT_NAME/filter
+echo 0 > events/$EVENT_NAME/filter
+
+# Clean up
+echo "-:$EVENT_NAME" >> kprobe_events
+test `cat kprobe_events | wc -l` -eq 0
+



Thanks,

> ---
> v4:
>  - Drop the redundant "added by the Fixes: commit below" and reword the
>    code comment ("an event with multiple probes attached"; clearer last
>    sentence), per Steve's review.
>  - Steve: drop the sentence explaining the move from the previous
>    version from the changelog.
>  - sashiko-bot (ack'd by Steve): traceprobe_define_arg_fields() may be
>    called again after a failed first attempt, since event_define_fields()
>    ignores this hook's return value. Freeing and resetting the leftover
>    duplicates at entry avoids leaking the previous array and writing
>    past the new one.
> v3:
>  - Move the fix out of trace_events.c into the probe layer
>    (traceprobe_define_arg_fields()/trace_probe_event_free()). Ownership
>    lives on trace_probe_event, whose lifetime matches the field list.
>  - Clarify this is kprobe multi-probe-per-event, not eprobes, and add a
>    shell reproducer.
> v2:
>  - Changelog wording (superseded by v3).
> 
>  kernel/trace/trace_probe.c | 48 ++++++++++++++++++++++++++++++++++++-
>  kernel/trace/trace_probe.h |  2 ++
>  2 files changed, 49 insertions(+), 1 deletion(-)
> 
> diff --git a/kernel/trace/trace_probe.c b/kernel/trace/trace_probe.c
> index c4163904ba747..0dfeb6d5eec07 100644
> --- a/kernel/trace/trace_probe.c
> +++ b/kernel/trace/trace_probe.c
> @@ -2552,19 +2552,60 @@ int traceprobe_set_print_fmt(struct trace_probe *tp, 
> enum probe_print_type ptype
>  int traceprobe_define_arg_fields(struct trace_event_call *event_call,
>                                size_t offset, struct trace_probe *tp)
>  {
> +     struct trace_probe_event *tpe = trace_probe_event_from_call(event_call);
>       int ret, i;
>  
> +     /*
> +      * A field created by trace_define_field() only stores the name and
> +      * type pointers, it does not copy the strings. Here they point into
> +      * the probe_arg of @tp, which is freed when @tp is removed. For an
> +      * event with multiple probes attached, the field list is defined
> +      * once by the first probe but kept alive by the surviving siblings,
> +      * so removing that first probe would leave the fields referencing
> +      * freed memory. Duplicate the strings and anchor the copies on the
> +      * trace_probe_event, which lives as long as the field list itself.
> +      *
> +      * event_define_fields() ignores the return value of this hook, so
> +      * if a previous attempt failed before creating any field, it may
> +      * call here again. Release duplicates left behind by such an
> +      * attempt before starting over.
> +      */
> +     for (i = 0; i < tpe->nr_field_strings; i++)
> +             kfree(tpe->field_strings[i]);
> +     kfree(tpe->field_strings);
> +     tpe->field_strings = NULL;
> +     tpe->nr_field_strings = 0;
> +
> +     if (tp->nr_args) {
> +             tpe->field_strings = kcalloc(tp->nr_args * 2, sizeof(char *),
> +                                          GFP_KERNEL);
> +             if (!tpe->field_strings)
> +                     return -ENOMEM;
> +     }
> +
>       /* Set argument names as fields */
>       for (i = 0; i < tp->nr_args; i++) {
>               struct probe_arg *parg = &tp->args[i];
>               const char *fmt = parg->type->fmttype;
>               int size = parg->type->size;
> +             char *name, *type;
>  
>               if (parg->fmt)
>                       fmt = parg->fmt;
>               if (parg->count)
>                       size *= parg->count;
> -             ret = trace_define_field(event_call, fmt, parg->name,
> +
> +             name = kstrdup(parg->name, GFP_KERNEL);
> +             type = kstrdup(fmt, GFP_KERNEL);
> +             if (!name || !type) {
> +                     kfree(name);
> +                     kfree(type);
> +                     return -ENOMEM;
> +             }
> +             tpe->field_strings[tpe->nr_field_strings++] = name;
> +             tpe->field_strings[tpe->nr_field_strings++] = type;
> +
> +             ret = trace_define_field(event_call, type, name,
>                                        offset + parg->offset, size,
>                                        parg->type->is_signed,
>                                        FILTER_OTHER);
> @@ -2576,6 +2617,11 @@ int traceprobe_define_arg_fields(struct 
> trace_event_call *event_call,
>  
>  static void trace_probe_event_free(struct trace_probe_event *tpe)
>  {
> +     int i;
> +
> +     for (i = 0; i < tpe->nr_field_strings; i++)
> +             kfree(tpe->field_strings[i]);
> +     kfree(tpe->field_strings);
>       kfree(tpe->class.system);
>       kfree(tpe->call.name);
>       kfree(tpe->call.print_fmt);
> diff --git a/kernel/trace/trace_probe.h b/kernel/trace/trace_probe.h
> index fba1af092a9bd..d1fb3520700fb 100644
> --- a/kernel/trace/trace_probe.h
> +++ b/kernel/trace/trace_probe.h
> @@ -264,6 +264,8 @@ struct trace_probe_event {
>       struct trace_event_call         call;
>       struct list_head                files;
>       struct list_head                probes;
> +     char                            **field_strings;
> +     int                             nr_field_strings;
>       struct trace_uprobe_filter      filter[];
>  };
>  
> -- 
> 2.43.0


-- 
Masami Hiramatsu (Google) <[email protected]>

Reply via email to