From: Masami Hiramatsu (Google) <[email protected]>

Allow set_wprobe trigger to use BTF struct offset resolution to specify
the target address field.

Link: 
https://lore.kernel.org/all/59637b96946653393a7ad3c7de094094796b39c2.1785067572.git.wangjinchao...@gmail.com/

Signed-off-by: Masami Hiramatsu (Google) <[email protected]>
---
Changes in v12:
 - Refactor field parsing logic into wprobe_trigger_field_parse().
 - Remove unused variable count_str in wprobe_trigger_cmd_parse().
---
 kernel/trace/trace_wprobe.c                        |  228 +++++++++++++++++---
 .../test.d/trigger/trigger-wprobe-btf-offset.tc    |   74 ++++++
 2 files changed, 267 insertions(+), 35 deletions(-)
 create mode 100644 
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-offset.tc

diff --git a/kernel/trace/trace_wprobe.c b/kernel/trace/trace_wprobe.c
index c64bbdc90a40..565c106a9380 100644
--- a/kernel/trace/trace_wprobe.c
+++ b/kernel/trace/trace_wprobe.c
@@ -27,6 +27,7 @@
 #include <asm/ptrace.h>
 
 #include "trace.h"
+#include "trace_btf.h"
 #include "trace_dynevent.h"
 #include "trace_probe.h"
 #include "trace_probe_kernel.h"
@@ -962,9 +963,10 @@ static int wprobe_trigger_print(struct seq_file *m,
                seq_printf(m, ":count=%ld", data->count);
 
        if (data->filter_str)
-               seq_printf(m, " if %s\n", data->filter_str);
-       else
-               seq_putc(m, '\n');
+               seq_printf(m, " if %s", data->filter_str);
+
+       seq_printf(m, " # offset:%d adjust:%ld\n",
+                  wprobe_data->offset, wprobe_data->adjust);
 
        return 0;
 }
@@ -1002,6 +1004,181 @@ static void wprobe_trigger_free(struct 
event_trigger_data *data)
        }
 }
 
+#ifdef CONFIG_PROBE_EVENTS_BTF_ARGS
+
+static int get_offset_of_field(struct btf *btf, const struct btf_type *type, 
char *field_name)
+{
+       const struct btf_member *field;
+       int bitoffs = 0;
+       u32 anon_offs;
+       char *next;
+
+       do {
+               next = strchr(field_name, '.');
+               if (next)
+                       *next++ = '\0';
+
+               field = btf_find_struct_member(btf, type, field_name, 
&anon_offs);
+               if (IS_ERR_OR_NULL(field))
+                       return -ENOENT;
+
+               if (btf_type_kflag(type)) {
+                       /* Reject bitfield member access */
+                       if (BTF_MEMBER_BITFIELD_SIZE(field->offset))
+                               return -EINVAL;
+                       bitoffs += anon_offs + 
BTF_MEMBER_BIT_OFFSET(field->offset);
+               } else {
+                       bitoffs += anon_offs + field->offset;
+               }
+
+               field_name = next;
+               if (next) {
+                       type = btf_type_skip_modifiers(btf, field->type, NULL);
+                       if (!type)
+                               return -ENOENT;
+               }
+       } while (next);
+       return bitoffs / BITS_PER_BYTE;
+}
+
+/* btf_put(NULL) is acceptable. */
+DEFINE_FREE(btf_put, struct btf *, btf_put(_T))
+
+/* parse typecast: (TYPE[,ASGN])EVENT_FIELD->FIELD[.SUBFIELD...] and set 
adjust. */
+static int wprobe_trigger_typecast_parse(char **field_str_ptr,
+                                        struct trace_event_file *file,
+                                        struct wprobe_trigger_data 
*wprobe_data,
+                                        const char *glob)
+{
+       struct btf *btf __free(btf_put) = NULL;
+       const struct btf_type *type;
+       char *assign_field;
+       char *event_field;
+       char *type_field;
+       char *type_name;
+       char *offs;
+       long val = 0;
+       int id;
+       int adjust;
+
+       type_name = *field_str_ptr + 1;
+       event_field = strchr(type_name, ')');
+       if (!event_field) {
+               wprobe_trigger_log_err(file, glob, type_name - glob, 
DEREF_OPEN_BRACE);
+               return -EINVAL;
+       }
+       *event_field++ = '\0';
+
+       /* Check the optional assign field. */
+       assign_field = strchr(type_name, ',');
+       if (assign_field)
+               *assign_field++ = '\0';
+
+       /* Get the type field name. */
+       type_field = strstr(event_field, "->");
+       if (!type_field) {
+               wprobe_trigger_log_err(file, glob, event_field - glob, 
TYPECAST_REQ_FIELD);
+               return -EINVAL;
+       }
+       *type_field = '\0';
+       type_field += 2;
+
+       offs = strpbrk(type_field, "+-");
+       if (offs) {
+               if (kstrtol(offs, 0, &val) < 0) {
+                       wprobe_trigger_log_err(file, glob, offs - glob, 
BAD_DEREF_OFFS);
+                       return -EINVAL;
+               }
+               *offs = '\0';
+       }
+
+       /* find type from BTF */
+       id = bpf_find_btf_id(type_name, BTF_KIND_STRUCT, &btf);
+       if (id < 0) {
+               wprobe_trigger_log_err(file, glob, type_name - glob, 
BAD_BTF_TID);
+               return id;
+       }
+
+       type = btf_type_by_id(btf, id);
+       if (!type) {
+               wprobe_trigger_log_err(file, glob, type_name - glob, 
BAD_BTF_TID);
+               return -EINVAL;
+       }
+
+       adjust = get_offset_of_field(btf, type, type_field);
+       if (adjust < 0) {
+               wprobe_trigger_log_err(file, glob, type_field - glob, 
NO_BTF_FIELD);
+               return adjust;
+       }
+       wprobe_data->adjust = adjust + val;
+
+       if (assign_field) {
+               /* assign_field should be a struct field */
+               adjust = get_offset_of_field(btf, type, assign_field);
+               if (adjust < 0) {
+                       wprobe_trigger_log_err(file, glob, assign_field - glob, 
NO_BTF_FIELD);
+                       return adjust;
+               }
+               wprobe_data->adjust -= adjust;
+       }
+
+       *field_str_ptr = event_field;
+       return 0;
+}
+#else
+static int wprobe_trigger_typecast_parse(char **field_str_ptr,
+                                        struct trace_event_file *file,
+                                        struct wprobe_trigger_data 
*wprobe_data,
+                                        const char *glob)
+{
+       wprobe_trigger_log_err(file, glob, *field_str_ptr - glob, NOSUP_BTFARG);
+       return -EOPNOTSUPP;
+}
+#endif /* CONFIG_PROBE_EVENTS_BTF_ARGS */
+
+static int wprobe_trigger_field_parse(char *field_str, struct trace_event_file 
*file,
+                                       struct wprobe_trigger_data *wprobe_data,
+                                       const char *glob)
+{
+       struct ftrace_event_field *field;
+       char *offs;
+
+       if (field_str[0] == '(') {
+               int ret = wprobe_trigger_typecast_parse(&field_str, file, 
wprobe_data, glob);
+
+               if (ret < 0)
+                       return ret;
+       } else {
+               offs = strpbrk(field_str, "+-");
+               if (offs) {
+                       long val;
+
+                       if (kstrtol(offs, 0, &val) < 0) {
+                               wprobe_trigger_log_err(file, glob, offs - glob, 
BAD_DEREF_OFFS);
+                               return -EINVAL;
+                       }
+                       wprobe_data->adjust = val;
+                       *offs = '\0';
+               }
+       }
+
+       field = trace_find_event_field(file->event_call, field_str);
+       if (!field) {
+               wprobe_trigger_log_err(file, glob, field_str - glob, 
NO_EVENT_FIELD);
+               return -ENOENT;
+       }
+       if (field->size != sizeof(void *)) {
+               wprobe_trigger_log_err(file, glob, field_str - glob, 
WPROBE_BAD_FIELD);
+               return -ENOEXEC;
+       }
+       wprobe_data->offset = field->offset;
+       wprobe_data->field = kstrdup(field_str, GFP_KERNEL);
+       if (!wprobe_data->field)
+               return -ENOMEM;
+
+       return 0;
+}
+
 static int wprobe_trigger_cmd_parse(struct event_command *cmd_ops,
                                    struct trace_event_file *file,
                                    char *glob, char *cmd,
@@ -1013,10 +1190,9 @@ static int wprobe_trigger_cmd_parse(struct event_command 
*cmd_ops,
         */
        struct wprobe_trigger_data *wprobe_data 
__free(free_wprobe_trigger_data) = NULL;
        struct event_trigger_data *trigger_data __free(kfree) = NULL;
-       char *event_str, *field_str, *count_str, *comment;
+       char *event_str, *comment;
        struct trace_event_file *wprobe_file;
        struct trace_array *tr = file->tr;
-       struct trace_event_call *event;
        bool remove, clear = false;
        struct trace_wprobe *tw;
        char *param, *filter;
@@ -1075,42 +1251,24 @@ static int wprobe_trigger_cmd_parse(struct 
event_command *cmd_ops,
 
        /* clear_wprobe does not need field. */
        if (!clear) {
-               char *offs;
+               char *field_str = strsep(&param, ":");
 
-               /* Find target field, which must be equivarent to "void *" */
-               field_str = strsep(&param, ":");
                if (!field_str) {
                        wprobe_trigger_log_err(file, glob, strlen(glob), 
WPROBE_NEED_FIELD);
                        return -EINVAL;
                }
-
-               offs = strpbrk(field_str, "+-");
-               if (offs) {
-                       long val;
-
-                       if (kstrtol(offs, 0, &val) < 0) {
-                               wprobe_trigger_log_err(file, glob, offs - glob, 
BAD_DEREF_OFFS);
-                               return -EINVAL;
-                       }
-                       wprobe_data->adjust = val;
-                       *offs = '\0';
-               }
-
-               event = file->event_call;
-               field = trace_find_event_field(event, field_str);
-               if (!field) {
-                       wprobe_trigger_log_err(file, glob, field_str - glob, 
NO_EVENT_FIELD);
-                       return -ENOENT;
-               }
-
-               if (field->size != sizeof(void *)) {
-                       wprobe_trigger_log_err(file, glob, field_str - glob, 
WPROBE_BAD_FIELD);
-                       return -ENOEXEC;
+               ret = wprobe_trigger_field_parse(field_str, file, wprobe_data, 
glob);
+               if (ret < 0)
+                       return ret;
+       } else if (param) {
+               char *orig_param = param;
+               char *field_str = strsep(&param, ":");
+
+               ret = wprobe_trigger_field_parse(field_str, file, wprobe_data, 
glob);
+               if (ret < 0) {
+                       /* field_str was not a field, so it must be count_str */
+                       param = orig_param;
                }
-               wprobe_data->offset = field->offset;
-               wprobe_data->field = kstrdup(field_str, GFP_KERNEL);
-               if (!wprobe_data->field)
-                       return -ENOMEM;
        }
 
        trigger_data = trigger_data_alloc(cmd_ops, cmd, param, wprobe_data);
diff --git 
a/tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-offset.tc 
b/tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-offset.tc
new file mode 100644
index 000000000000..dda179a23282
--- /dev/null
+++ b/tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-offset.tc
@@ -0,0 +1,74 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# description: event trigger - test set_wprobe trigger with BTF struct offset
+# requires: dynamic_events "w[:[<group>/][<event>]] 
[r|w|rw]@<addr>[:<len>]":README events/sched/sched_process_fork/trigger 
"[(structname[,field])]<argname>[->field[->field|.field...]]":README
+
+rmmod trace-events-sample ||:
+if ! modprobe trace-events-sample ; then
+  echo "No trace-events sample module - please make 
CONFIG_SAMPLE_TRACE_EVENTS=m"
+  exit_unresolved
+fi
+
+cleanup_wprobe_triggers() {
+  if [ -f events/fprobes/testevent/trigger ]; then
+    reset_trigger_file events/fprobes/testevent/trigger || true
+  fi
+  echo 0 > events/enable 2>/dev/null || true
+  echo > dynamic_events 2>/dev/null || true
+  sleep 1
+  rmmod trace-events-sample 2>/dev/null || true
+  return 0
+}
+
+trap cleanup_wprobe_triggers EXIT
+
+echo 0 > tracing_on
+
+# we will skip this test if fprobe is not supported.
+if ! grep -Fq "f[:[<group>/][<event>]] <func-name>[%return] [<args>]" README; 
then
+    echo "UNRESOLVED: fprobe is not supported"
+    exit_unresolved
+fi
+
+# we will skip this test if the target function does not exist.
+if ! grep -wq "sample_timer_cb" /proc/kallsyms; then
+    echo "UNRESOLVED: sample_timer_cb not found"
+    exit_unresolved
+fi
+
+:;: "Add a wprobe event watching 8 bytes" ;:
+echo 'w:watch rw@0:8 address=$addr value=$value' >> dynamic_events
+
+:;: "Add fprobe event for sample_timer_cb" ;:
+# sample_timer_cb(struct timer_list *t)
+# container_of(t, struct foo_timer_data, timer)
+echo 'f:fprobes/testevent sample_timer_cb timer=t' >> dynamic_events
+
+:;: "Enable all events before setting triggers" ;:
+echo 1 > tracing_on
+echo 1 >> events/fprobes/testevent/enable
+
+:;: "Set set_wprobe trigger using BTF struct offset resolution" ;:
+# Syntax: set_wprobe:WPEVENT:(STRUCT,FIELD)EVENT_FIELD->MEMBER
+# (foo_timer_data,timer) is the BTF struct type and field name
+# timer->expires is the struct member whose offset is resolved automatically 
via BTF
+echo 'set_wprobe:watch:(foo_timer_data,timer)timer->timer.expires' >> 
events/fprobes/testevent/trigger
+cat events/fprobes/testevent/trigger | grep ^set_wprobe
+
+# Wait for sample_timer_cb to fire and set_wprobe trigger to activate
+sleep 3
+
+:;: "Check set_wprobe trigger activated the watchpoint" ;:
+cat trace | grep watch
+
+:;: "Remove wprobe triggers" ;:
+# Since we don't know actual offset of timer->expires in foo_timer_data, we 
use reset_trigger_file
+reset_trigger_file events/fprobes/testevent/trigger
+! grep ^set_wprobe events/fprobes/testevent/trigger
+
+:;: "Disable events and remove dynamic events" ;:
+echo 0 > events/enable
+echo > dynamic_events
+clear_trace
+
+exit 0


Reply via email to