Skip to contentSkip to conversation

tracing: wprobe: x86: Add wprobe for watchpoint

PATCH Superseded Masami Hiramatsu (Google) 2 participants 24 messages Jul 15 – Jul 16

Subsystems (5)

v7 of 19 Superseded Latest is v19 →

lore
Share & tools
lore

Masami Hiramatsu (Google)

author maintainer
#0 lore raw

v7 cover tracing: wprobe: x86: Add wprobe for watchpoint

Hi,

Here is the 7th version of the series for adding new wprobe (watch probe) which provides memory access tracing event. Moreover, this can be used via event trigger. Thus it can trace memory access on a dynamically allocated objects too.
The previous version is here:

https://lore.kernel.org/all/176960933881.182525.11984731584313026309.stgit@devnote2/

This version is rebased on top of probes/for-next branch, fold IS_ERR_PCPU fix and fix some checkpatch.pl related issue (not all, some of them seems wrong warnings).
Other updates:
- Fix a race on wprobe handler using bp->attr.bp_addr instead of tw->addr.
- Fix modify_wide_hw_breakpoint_local() to update bp->attr.bp_addr.
- Return -EOPNOTSUPP instead of -ENOSYS. (that is not a syscall function)
- Update test script to use dentry_kill instead of __dentry_kill.

To support arm64, we need to avoid major pagefault on single-stepping issue. I think we can solve it with checking whether the address is the kernel (which must not cause a major page fault), or not.

Usage
-----

The basic usage of this wprobe is similar to other probes;

w:[GRP/][EVENT] [r|w|rw]@<ADDRESS|SYMBOL[+OFFS]> [FETCHARGS]

This defines a new wprobe event. For example, to trace jiffies update, you can do;

echo 'w:my_jiffies w@jiffies:8 value=+0($addr)' >> dynamic_events echo 1 > events/wprobes/my_jiffies/enable

Moreover, this can be combined with event trigger to trace the memory accecss on slab objects. The trigger syntax is;

set_wprobe:WPROBE_EVENT:FIELD[+OFFSET] [if FILTER] clear_wprobe:WPROBE_EVENT[:FIELD[+OFFSET]] [if FILTER]

set_wprobe sets WPROBE_EVENT's watch address on FIELD[+OFFSET]. clear_wprobe clears WPROBE_EVENT's watch address if it is set to FIELD[+OFFSET]. If FIELD is omitted, forcibly clear the watch address when trigger event is hit.

For example, trace the first 8 byte of the dentry data structure passed to do_truncate() until it is deleted by __dentry_kill(). (Note: all tracefs setup uses '>>' so that it does not kick do_truncate())

  # echo 'w:watch rw@0:8 address=$addr value=+0($addr)' > dynamic_events
  # echo 'f:truncate do_truncate dentry=$arg2' >> dynamic_events
  # echo 'set_wprobe:watch:dentry' >> events/fprobes/truncate/trigger
  # echo 'f:dentry_kill __dentry_kill dentry=$arg1' >> dynamic_events
  # echo 'clear_wprobe:watch:dentry' >> events/fprobes/dentry_kill/trigger
  # echo 1 >> events/fprobes/truncate/enable
  # echo 1 >> events/fprobes/dentry_kill/enable
  # echo aaa > /tmp/hoge
  # echo bbb > /tmp/hoge
  # echo ccc > /tmp/hoge
  # rm /tmp/hoge

Then, the trace data will show;

# tracer: nop
#
# entries-in-buffer/entries-written: 16/16 #P:8
#
# _-----=> irqs-off/BH-disabled
# / _----=> need-resched

#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| / _-=> migrate-disable
#                              |||| /     delay
#           TASK-PID     CPU#  |||||  TIMESTAMP  FUNCTION
#              | |         |   |||||     |         |
              sh-113     [004] .....     6.467444: truncate: (do_truncate+0x4/0x120) dentry=0xffff8880044f0fd8
              sh-113     [004] ..Zff     6.468534: watch: (lookup_fast+0xaa/0x150) address=0xffff8880044f0fd8 value=0x200080
              sh-113     [004] ..Zff     6.468542: watch: (step_into+0x82/0x360) address=0xffff8880044f0fd8 value=0x200080
              sh-113     [004] ..Zff     6.468547: watch: (step_into+0x9f/0x360) address=0xffff8880044f0fd8 value=0x200080
              sh-113     [004] ..Zff     6.468553: watch: (path_openat+0xb3a/0xe70) address=0xffff8880044f0fd8 value=0x200080
              sh-113     [004] ..Zff     6.468557: watch: (path_openat+0xb9a/0xe70) address=0xffff8880044f0fd8 value=0x200080
              sh-113     [004] .....     6.468563: truncate: (do_truncate+0x4/0x120) dentry=0xffff8880044f0fd8
              sh-113     [004] ...1.     6.469826: dentry_kill: (__dentry_kill+0x0/0x220) dentry=0xffff8880044f0ea0
              sh-113     [004] ...1.     6.469859: dentry_kill: (__dentry_kill+0x0/0x220) dentry=0xffff8880044f0d68
              rm-118     [001] ..Zff     6.472360: watch: (lookup_fast+0xaa/0x150) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472366: watch: (step_into+0x82/0x360) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472370: watch: (step_into+0x9f/0x360) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472386: watch: (lookup_fast+0xaa/0x150) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472390: watch: (step_into+0x82/0x360) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472394: watch: (step_into+0x9f/0x360) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472415: watch: (lookup_one_qstr_excl+0x2c/0x150) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472419: watch: (lookup_one_qstr_excl+0xd5/0x150) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472424: watch: (may_delete+0x18/0x200) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472428: watch: (may_delete+0x194/0x200) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] ..Zff     6.472446: watch: (vfs_unlink+0x63/0x1c0) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] d.Z..     6.472528: watch: (dont_mount+0x19/0x30) address=0xffff8880044f0fd8 value=0x200180
              rm-118     [001] ..Zff     6.472533: watch: (vfs_unlink+0x11a/0x1c0) address=0xffff8880044f0fd8 value=0x200180
              rm-118     [001] ..Zff     6.472538: watch: (vfs_unlink+0x12e/0x1c0) address=0xffff8880044f0fd8 value=0x200180
              rm-118     [001] d.Z1.     6.472543: watch: (d_delete+0x61/0xa0) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] d.Z1.     6.472547: watch: (dentry_unlink_inode+0x14/0x110) address=0xffff8880044f0fd8 value=0x200080
              rm-118     [001] d.Z1.     6.472551: watch: (dentry_unlink_inode+0x1e/0x110) address=0xffff8880044f0fd8 value=0x80
              rm-118     [001] d.Z..     6.472563: watch: (fast_dput+0x8d/0x120) address=0xffff8880044f0fd8 value=0x80
              rm-118     [001] ...1.     6.472567: dentry_kill: (__dentry_kill+0x0/0x220) dentry=0xffff8880044f0fd8
              sh-113     [004] ...2.     6.473049: dentry_kill: (__dentry_kill+0x0/0x220) dentry=0xffff888006e383a8

Thank you,

---

Jinchao Wang (2):
x86/hw_breakpoint: Unify breakpoint install/uninstall x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint

Masami Hiramatsu (Google) (8):
tracing: wprobe: Add watchpoint probe event based on hardware breakpoint
x86: hw_breakpoint: Add a kconfig to clarify when a breakpoint fires
selftests: tracing: Add a basic testcase for wprobe
selftests: tracing: Add syntax testcase for wprobe
tracing: wprobe: Use a new seq_print_ip_sym_offset() wrapper
HWBP: Add modify_wide_hw_breakpoint_local() API
tracing: wprobe: Add wprobe event trigger
selftests: ftrace: Add wprobe trigger testcase

 Documentation/trace/index.rst                      |    1 
 Documentation/trace/wprobetrace.rst                |  158 +++
 arch/Kconfig                                       |   20 
 arch/x86/Kconfig                                   |    2 
 arch/x86/include/asm/hw_breakpoint.h               |    8 
 arch/x86/kernel/hw_breakpoint.c                    |  148 ++-
 include/linux/hw_breakpoint.h                      |    6 
 include/linux/trace_events.h                       |    3 
 kernel/events/hw_breakpoint.c                      |   39 +
 kernel/trace/Kconfig                               |   24 
 kernel/trace/Makefile                              |    1 
 kernel/trace/trace.c                               |    9 
 kernel/trace/trace.h                               |    5 
 kernel/trace/trace_probe.c                         |   22 
 kernel/trace/trace_probe.h                         |    8 
 kernel/trace/trace_wprobe.c                        | 1108 ++++++++++++++++++++
 tools/testing/selftests/ftrace/config              |    2 
 .../ftrace/test.d/dynevent/add_remove_wprobe.tc    |   68 +
 .../test.d/dynevent/wprobes_syntax_errors.tc       |   20 
 .../ftrace/test.d/trigger/trigger-wprobe.tc        |   48 +
 20 files changed, 1635 insertions(+), 65 deletions(-)
 create mode 100644 Documentation/trace/wprobetrace.rst
 create mode 100644 kernel/trace/trace_wprobe.c
 create mode 100644 tools/testing/selftests/ftrace/test.d/dynevent/add_remove_wprobe.tc
 create mode 100644 tools/testing/selftests/ftrace/test.d/dynevent/wprobes_syntax_errors.tc
 create mode 100644 tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc

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

author maintainer
#1 lore raw

PATCH v7 1/10 tracing: wprobe: Add watchpoint probe event based on hardware breakpoint

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

Add a new probe event for the hardware breakpoint called wprobe-event. This wprobe allows user to trace (watch) the memory access at the specified memory address.
The new syntax is;

w[:[GROUP/]EVENT] [r|w|rw]@[ADDR|SYM][:SIZE] [FETCH_ARGs]

User also can use $addr to fetch the accessed address. But no other variables are supported. To record updated value, use '+0($addr)'.

For example, tracing updates of the jiffies;

/sys/kernel/tracing # echo 'w:my_jiffies w@jiffies' >> dynamic_events /sys/kernel/tracing # cat dynamic_events w:wprobes/my_jiffies w@jiffies:4
/sys/kernel/tracing # echo 1 > events/wprobes/my_jiffies/enable /sys/kernel/tracing # head -n 20 trace | tail -n 5

 #           TASK-PID     CPU#  |||||  TIMESTAMP  FUNCTION
 #              | |         |   |||||     |         |
          <idle>-0       [000] d.Z1.   206.547317: my_jiffies: (tick_do_update_jiffies64+0xbe/0x130)
          <idle>-0       [000] d.Z1.   206.548341: my_jiffies: (tick_do_update_jiffies64+0xbe/0x130)
          <idle>-0       [000] d.Z1.   206.549346: my_jiffies: (tick_do_update_jiffies64+0xbe/0x130)

---
Changes in v7:
- Include IS_ERR_PCPU fix.
- use seq_print_ip_sym_offset().
- fix checkpatch warning on DEFINE_FREE()
- Use bp->attr.bp_addr instead of tw->addr because it can be updated from another CPU.

---
 Documentation/trace/index.rst       |    1 
 Documentation/trace/wprobetrace.rst |   69 +++
 include/linux/trace_events.h        |    2 
 kernel/trace/Kconfig                |   13 +
 kernel/trace/Makefile               |    1 
 kernel/trace/trace.c                |    9 
 kernel/trace/trace.h                |    5 
 kernel/trace/trace_probe.c          |   22 +
 kernel/trace/trace_probe.h          |    8 
 kernel/trace/trace_wprobe.c         |  692 +++++++++++++++++++++++++++++++++++
 10 files changed, 819 insertions(+), 3 deletions(-)
 create mode 100644 Documentation/trace/wprobetrace.rst
 create mode 100644 kernel/trace/trace_wprobe.c
Documentation/trace/index.rst +1 -0
index 5d9bf4694d5d..2f04f32001ed 100644
@@ -36,6 +36,7 @@ the Linux kernel.
    kprobes
    kprobetrace
    fprobetrace
+   wprobetrace
    eprobetrace
    fprobe
    ring-buffer-design
Documentation/trace/wprobetrace.rst new file +69 -0

69 lines not on this page · raw message

include/linux/trace_events.h +2 -0
index 308c76b57d13..d1e5ab71d928 100644
@@ -328,6 +328,7 @@ enum {
 	TRACE_EVENT_FL_UPROBE_BIT,
 	TRACE_EVENT_FL_EPROBE_BIT,
 	TRACE_EVENT_FL_FPROBE_BIT,
+	TRACE_EVENT_FL_WPROBE_BIT,
 	TRACE_EVENT_FL_CUSTOM_BIT,
 	TRACE_EVENT_FL_TEST_STR_BIT,
 };
@@ -358,6 +359,7 @@ enum {
 	TRACE_EVENT_FL_UPROBE		= (1 << TRACE_EVENT_FL_UPROBE_BIT),
 	TRACE_EVENT_FL_EPROBE		= (1 << TRACE_EVENT_FL_EPROBE_BIT),
 	TRACE_EVENT_FL_FPROBE		= (1 << TRACE_EVENT_FL_FPROBE_BIT),
+	TRACE_EVENT_FL_WPROBE		= (1 << TRACE_EVENT_FL_WPROBE_BIT),
 	TRACE_EVENT_FL_CUSTOM		= (1 << TRACE_EVENT_FL_CUSTOM_BIT),
 	TRACE_EVENT_FL_TEST_STR		= (1 << TRACE_EVENT_FL_TEST_STR_BIT),
 };
kernel/trace/Kconfig +13 -0
index 0ab5916575a9..b58c2565024f 100644
@@ -862,6 +862,19 @@ config EPROBE_EVENTS
 	  convert the type of an event field. For example, turn an
 	  address into a string.
 
+config WPROBE_EVENTS
+	bool "Enable wprobe-based dynamic events"
+	depends on TRACING
+	depends on HAVE_HW_BREAKPOINT
+	select PROBE_EVENTS
+	select DYNAMIC_EVENTS
+	help
+	  This allows the user to add watchpoint tracing events based on
+	  hardware breakpoints on the fly via the ftrace interface.
+
+	  Those events can be inserted wherever hardware breakpoints can be
+	  set, and record accessed memory address and values.
+
 config BPF_EVENTS
 	depends on BPF_SYSCALL
 	depends on (KPROBE_EVENTS || UPROBE_EVENTS) && PERF_EVENTS
kernel/trace/Makefile +1 -0
index f934ff586bd4..141c8323de20 100644
@@ -126,6 +126,7 @@ obj-$(CONFIG_FTRACE_RECORD_RECURSION) += trace_recursion_record.o
 obj-$(CONFIG_FPROBE) += fprobe.o
 obj-$(CONFIG_RETHOOK) += rethook.o
 obj-$(CONFIG_FPROBE_EVENTS) += trace_fprobe.o
+obj-$(CONFIG_WPROBE_EVENTS) += trace_wprobe.o
 
 obj-$(CONFIG_TRACEPOINT_BENCHMARK) += trace_benchmark.o
 obj-$(CONFIG_RV) += rv/
kernel/trace/trace.c +8 -1
index c9e182d40059..1bc27c0ad029 100644
@@ -4294,8 +4294,12 @@ static const char readme_msg[] =
 	"  uprobe_events\t\t- Create/append/remove/show the userspace dynamic events\n"
 	"\t\t\t  Write into this file to define/undefine new trace events.\n"
 #endif
+#ifdef CONFIG_WPROBE_EVENTS
+	"  wprobe_events\t\t- Create/append/remove/show the hardware breakpoint dynamic events\n"
+	"\t\t\t  Write into this file to define/undefine new trace events.\n"
+#endif
 #if defined(CONFIG_KPROBE_EVENTS) || defined(CONFIG_UPROBE_EVENTS) || \
-    defined(CONFIG_FPROBE_EVENTS)
+    defined(CONFIG_FPROBE_EVENTS) || defined(CONFIG_WPROBE_EVENTS)
 	"\t  accepts: event-definitions (one definition per line)\n"
 #if defined(CONFIG_KPROBE_EVENTS) || defined(CONFIG_UPROBE_EVENTS)
 	"\t   Format: p[:[<group>/][<event>]] <place> [<args>]\n"
@@ -4305,6 +4309,9 @@ static const char readme_msg[] =
 	"\t           f[:[<group>/][<event>]] <func-name>[%return] [<args>]\n"
 	"\t           t[:[<group>/][<event>]] <tracepoint> [<args>]\n"
 #endif
+#ifdef CONFIG_WPROBE_EVENTS
+	"\t           w[:[<group>/][<event>]] [r|w|rw]@<addr>[:<len>]\n"
+#endif
 #ifdef CONFIG_HIST_TRIGGERS
 	"\t           s:[synthetic/]<event> <field> [<field>]\n"
 #endif
kernel/trace/trace.h +5 -0
index 80fe152af1dd..2f07c5c4ffc8 100644
@@ -179,6 +179,11 @@ struct fexit_trace_entry_head {
 	unsigned long		ret_ip;
 };
 
+struct wprobe_trace_entry_head {
+	struct trace_entry	ent;
+	unsigned long		ip;
+};
+
 #define TRACE_BUF_SIZE		1024
 
 struct trace_array;
kernel/trace/trace_probe.c +21 -1
index 18c212122344..2600d9699bd8 100644
@@ -1344,6 +1344,24 @@ static int parse_probe_vars(char *orig_arg, const struct fetch_type *t,
 		return 0;
 	}
 
+	/* wprobe only support "$addr" and "$value" variable */
+	if (ctx->flags & TPARG_FL_WPROBE) {
+		if (!strcmp(arg, "addr")) {
+			code->op = FETCH_OP_BADDR;
+			return 0;
+		}
+		if (!strcmp(arg, "value")) {
+			code->op = FETCH_OP_BADDR;
+			code++;
+			code->op = FETCH_OP_DEREF;
+			code->offset = 0;
+			*pcode = code;
+			return 0;
+		}
+		err = TP_ERR_BAD_VAR;
+		goto inval;
+	}
+
 	if (str_has_prefix(arg, "retval")) {
 		if (!(ctx->flags & TPARG_FL_RETURN)) {
 			err = TP_ERR_RETVAL_ON_PROBE;
@@ -1491,8 +1509,9 @@ parse_probe_arg(char *arg, const struct fetch_type *type,
 		ret = parse_probe_vars(arg, type, pcode, end, ctx);
 		break;
 
+#ifdef CONFIG_HAVE_FUNCTION_ARG_ACCESS_API
 	case '%':	/* named register */
-		if (ctx->flags & (TPARG_FL_TEVENT | TPARG_FL_FPROBE)) {
+		if (ctx->flags & (TPARG_FL_TEVENT | TPARG_FL_FPROBE | TPARG_FL_WPROBE)) {
 			/* eprobe and fprobe do not handle registers */
 			trace_probe_log_err(ctx->offset, BAD_VAR);
 			break;
@@ -1505,6 +1524,7 @@ parse_probe_arg(char *arg, const struct fetch_type *type,
 		} else
 			trace_probe_log_err(ctx->offset, BAD_REG_NAME);
 		break;
+#endif
 
 	case '@':	/* memory, file-offset or symbol */
 		if (isdigit(arg[1])) {
kernel/trace/trace_probe.h +7 -1
index e6268a8dc378..64c5fe9bdfc9 100644
@@ -90,6 +90,7 @@ typedef int (*print_type_func_t)(struct trace_seq *, void *, void *);
 	FETCH_OP(STACK, param),		/* Stack: .param = index */	\
 	FETCH_OP(STACKP, none),		/* Stack pointer */		\
 	FETCH_OP(RETVAL, none),		/* Return value */		\
+	FETCH_OP(BADDR, none),		/* Break address */		\
 	FETCH_OP(IMM, imm),		/* Immediate: .immediate */	\
 	FETCH_OP(COMM, none),		/* Current comm */		\
 	FETCH_OP(CURRENT, none),	/* Current task_struct address */\
@@ -419,6 +420,7 @@ static inline int traceprobe_get_entry_data_size(struct trace_probe *tp)
 #define TPARG_FL_USER   BIT(4)
 #define TPARG_FL_FPROBE BIT(5)
 #define TPARG_FL_TPOINT BIT(6)
+#define TPARG_FL_WPROBE BIT(7)
 #define TPARG_FL_LOC_MASK	GENMASK(4, 0)
 
 static inline bool tparg_is_function_entry(unsigned int flags)
@@ -600,7 +602,11 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call,
 	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(NOSUP_PERCPU,		"Per-cpu variable access is only for kernel probes"), \
+	C(BAD_ACCESS_FMT,	"Access memory address requires @"),	\
+	C(BAD_ACCESS_TYPE,	"Bad memory access type"),	\
+	C(BAD_ACCESS_LEN,	"This memory access length is not supported"), \
+	C(BAD_ACCESS_ADDR,	"Invalid access memory address"),
 
 #undef C
 #define C(a, b)		TP_ERR_##a
kernel/trace/trace_wprobe.c new file +692 -0

692 lines not on this page · raw message

#2 lore raw

Re: [PATCH v7 01/10] tracing: wprobe: Add watchpoint probe event based on hardware breakpoint

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider: - [High] Missing error rollback in enable_trace_wprobe() leaves dangling event file links and active…

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] Missing error rollback in enable_trace_wprobe() leaves dangling event file links and active hardware breakpoints, leading to Use-After-Free and resource leaks.
- [High] Format string bugs in trace_wprobe_show break the dynamic_events save and restore functionality.
- [Medium] wprobes silently drop all perf events because the hardware breakpoint handler lacks perf ring buffer delivery logic.
- [Medium] wprobe_trace_handler silently drops events if the hardware breakpoint address was updated, contradicting the stated design.
- [Medium] Unconditional memory leak of the dynamically allocated symbol string in __trace_wprobe_create().
- [Medium] Undocumented $value variable is implemented, contradicting the commit message claims.
--

--- Patch [1]: [PATCH v7 01/10] tracing: wprobe: Add watchpoint probe event based on hardware breakpoint --- commit f8a7aeb231575f15f9227ab411134bde7408e480
Author: Masami Hiramatsu (Google) <[email protected]>

tracing: wprobe: Add watchpoint probe event based on hardware breakpoint

This patch adds a new dynamic event for hardware breakpoints, called a wprobe-event. It allows users to trace memory access at specified addresses or symbols, supporting both ftrace and perf interfaces.

Quoted 2 lines

User also can use $addr to fetch the accessed address. But no other variables are supported. To record updated value, use '+0($addr)'.

[Severity: Medium]
Is this documentation accurate?

Looking at parse_probe_vars(), it explicitly parses and handles a $value variable:

parse_probe_vars() {
    ...
    if (!strcmp(arg, "value")) {
        code->op = FETCH_OP_BADDR;
        code++;
        code->op = FETCH_OP_DEREF;

...

Should the commit message be updated to reflect that $value is actually supported?

Quoted 5 lines
kernel/trace/trace_wprobe.c new file +0 -0
new file mode 100644
index 0000000000000..b52f3eac719f9

[ ... ]

Quoted 5 lines
kernel/trace/trace_wprobe.c new file +5 -0
+static void wprobe_trace_handler(struct trace_wprobe *tw,
+				 unsigned long addr,
+				 struct pt_regs *regs,
+				 struct trace_event_file *trace_file)
+{

[ ... ]

kernel/trace/trace_wprobe.c line 139

Quoted 5 lines
kernel/trace/trace_wprobe.c new file +5 -0
+	if (trace_trigger_soft_disabled(trace_file))
+		return;
+
+	if (tw->addr != addr)
+		return;

[Severity: Medium]
Does this strict address comparison inadvertently drop valid events if the hardware breakpoint address was updated dynamically?

The v7 changelog mentions using the dynamic address because the original address can be updated from another CPU, but this check causes the handler to exit immediately if the updated address differs from the statically recorded tw->addr.

[ ... ]

kernel/trace/trace_wprobe.c line 165

Quoted 11 lines
kernel/trace/trace_wprobe.c new file +11 -0
+static void wprobe_perf_handler(struct perf_event *bp,
+			      struct perf_sample_data *data,
+			      struct pt_regs *regs)
+{
+	struct trace_wprobe *tw = bp->overflow_handler_context;
+	struct event_file_link *link;
+	unsigned long addr = bp->attr.bp_addr;
+
+	trace_probe_for_each_link_rcu(link, &tw->tp)
+		wprobe_trace_handler(tw, addr, regs, link->file);
+}

[Severity: Medium]
Does this handler omit the necessary logic to generate and submit perf samples?

Since wprobe_register() accepts TRACE_REG_PERF_REGISTER, users can open these events via the perf syscalls.

However, this handler only iterates over ftrace ring buffer links and calls wprobe_trace_handler().

Without perf ring buffer delivery logic, will perf recordings of wprobe events contain zero samples?

[ ... ]

Quoted 3 lines
kernel/trace/trace_wprobe.c new file +3 -0
+static int enable_trace_wprobe(struct trace_event_call *call,
+			       struct trace_event_file *file)
+{

[ ... ]

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+	if (file) {
+		ret = trace_probe_add_file(tp, file);
+		if (ret)
+			return ret;
+	} else {
+		trace_probe_set_flag(tp, TP_FLAG_PROFILE);
+	}
+
+	if (!enabled) {
+		list_for_each_entry(tw, trace_probe_probe_list(tp), tp.list) {

kernel/trace/trace_wprobe.c line 406

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+			ret = __register_trace_wprobe(tw);
+			if (ret < 0) {
+				/* TODO: rollback */
+				return ret;
+			}
+		}
+	}
+
+	return 0;
+}

[Severity: High]
Could this missing rollback logic lead to a use-after-free or memory corruption if a later hardware breakpoint registration fails?

When __register_trace_wprobe() fails partway through the loop, the function returns immediately without unlinking the trace_event_file (via trace_probe_remove_file) or unregistering any successfully enabled sibling breakpoints.

If the user then deletes the trace instance, the trace_event_file is freed. Should one of the still-active breakpoints fire later, wprobe_perf_handler() would iterate over the corrupted file list and dereference the freed memory:

wprobe_perf_handler() {
    ...
    trace_probe_for_each_link_rcu(link, &tw->tp)
        wprobe_trace_handler(tw, addr, regs, link->file);
}

[ ... ]

Quoted 3 lines
kernel/trace/trace_wprobe.c new file +3 -0
+static int parse_address_spec(const char *spec, unsigned long *addr, int *type,
+			      int *len, char **symbol)
+{

[ ... ]

Quoted 11 lines
kernel/trace/trace_wprobe.c new file +11 -0
+	if (kstrtoul(at + 1, 0, &_addr) != 0) {
+		char *off_str = strpbrk(at + 1, "+-");
+		int offset = 0;
+
+		if (off_str) {
+			if (kstrtoint(off_str, 0, &offset) != 0) {
+				trace_probe_log_err(off_str - _spec, BAD_PROBE_ADDR);
+				return -EINVAL;
+			}
+			*off_str = '\0';
+		}

kernel/trace/trace_wprobe.c line 549

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+		_addr = kallsyms_lookup_name(at + 1);
+		if (!_addr) {
+			trace_probe_log_err(at + 1 - _spec, BAD_ACCESS_ADDR);
+			return -ENOENT;
+		}
+		_addr += offset;
+		*symbol = kstrdup(at + 1, GFP_KERNEL);
+		if (!*symbol)
+			return -ENOMEM;
+	}

[Severity: High]
Could the null terminator injection here break dynamic_events save and restore functionality for events with symbol offsets?

When the event is created with a symbol offset (like symbol+8), *off_str is overwritten with a null byte. This truncates the string before it gets duplicated into *symbol.

Later, trace_wprobe_show() outputs the truncated base symbol without the offset, meaning the restored event will monitor the wrong memory address.

[ ... ]

Quoted 2 lines
kernel/trace/trace_wprobe.c new file +2 -0
+static int __trace_wprobe_create(int argc, const char *argv[])
+{

[ ... ]

Quoted 5 lines
kernel/trace/trace_wprobe.c new file +5 -0
+	char *symbol = NULL;
+	unsigned long addr;
+	int len, type, i;
+	int ret = 0;
+

[ ... ]

kernel/trace/trace_wprobe.c line 604

Quoted 12 lines
kernel/trace/trace_wprobe.c new file +12 -0
+	trace_probe_log_set_index(1);
+	ret = parse_address_spec(argv[1], &addr, &type, &len, &symbol);
+	if (ret < 0)
+		return ret;
+
+	if (!event)
+		event = symbol ? symbol : "wprobe";
+
+	argc -= 2; argv += 2;
+	tw = alloc_trace_wprobe(group, event, symbol, addr, len, type, argc);
+	if (IS_ERR(tw))
+		return PTR_ERR(tw);

[Severity: Medium]
Does this leak the dynamically allocated symbol string?

The symbol variable is populated with an allocated string from parse_address_spec(). When passed to alloc_trace_wprobe(), that function duplicates the string again:

alloc_trace_wprobe() {
    ...
    if (symbol) {
        tw->symbol = kstrdup(symbol, GFP_KERNEL);
    ...
}

However, __trace_wprobe_create() never frees the original symbol string on either the success or error paths.

[ ... ]

Quoted 6 lines
kernel/trace/trace_wprobe.c new file +6 -0
+static int trace_wprobe_show(struct seq_file *m, struct dyn_event *ev)
+{
+	struct trace_wprobe *tw = to_trace_wprobe(ev);
+	int i;
+
+	seq_printf(m, "w:%s/%s", trace_probe_group_name(&tw->tp),

kernel/trace/trace_wprobe.c line 662

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+		   trace_probe_name(&tw->tp));
+
+	char type_char;
+
+	if (tw->type == HW_BREAKPOINT_R)
+		type_char = 'r';
+	else if (tw->type == HW_BREAKPOINT_W)
+		type_char = 'w';
+	else
+		type_char = 'x'; /* Should be rw */

[Severity: High]
Will outputting 'x' for HW_BREAKPOINT_RW cause parse_address_spec() to reject the configuration upon restore?

If type_char is set to 'x', the formatted string saved to dynamic_events will be invalid. When the system attempts to restore the event across reboots, parse_address_spec() checks strictly for 'r', 'w', or 'rw', and will fail with -EINVAL for 'x'.

Should this be adjusted to output a string like "rw" instead?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=1
author maintainer
#3 lore raw

Re: [PATCH v7 01/10] tracing: wprobe: Add watchpoint probe event based on hardware breakpoint

Quoted 7 lines · On Wed, 15 Jul 2026 02:01:39 +0000 [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [High] Missing error rollback in enable_trace_wprobe() leaves dangling event file links and active hardware breakpoints, leading to Use-After-Free and resource leaks.
- [High] Format string bugs in trace_wprobe_show break the dynamic_events save and restore functionality.
- [Medium] wprobes silently drop all perf events because the hardware breakpoint handler lacks perf ring buffer delivery logic.
- [Medium] wprobe_trace_handler silently drops events if the hardware breakpoint address was updated, contradicting the stated design.
- [Medium] Unconditional memory leak of the dynamically allocated symbol string in __trace_wprobe_create().
- [Medium] Undocumented $value variable is implemented, contradicting the commit message claims.

Thanks for the comments.

Quoted 29 lines

--

--- Patch [1]: [PATCH v7 01/10] tracing: wprobe: Add watchpoint probe event based on hardware breakpoint --- commit f8a7aeb231575f15f9227ab411134bde7408e480
Author: Masami Hiramatsu (Google) <[email protected]>

tracing: wprobe: Add watchpoint probe event based on hardware breakpoint

This patch adds a new dynamic event for hardware breakpoints, called a wprobe-event. It allows users to trace memory access at specified addresses or symbols, supporting both ftrace and perf interfaces.

Quoted 2 lines

User also can use $addr to fetch the accessed address. But no other variables are supported. To record updated value, use '+0($addr)'.

[Severity: Medium]
Is this documentation accurate?

Looking at parse_probe_vars(), it explicitly parses and handles a $value variable:

parse_probe_vars() {
    ...
    if (!strcmp(arg, "value")) {
        code->op = FETCH_OP_BADDR;
        code++;
        code->op = FETCH_OP_DEREF;
Quoted 4 lines

...

Should the commit message be updated to reflect that $value is actually supported?

Good catch! Yeah $value was added, so we don't need to use +0($addr). The $addr is used for ensuring watching address. That will be useful when wprobe trigger is introduced.

Quoted 17 lines
Quoted 6 lines
kernel/trace/trace_wprobe.c new file +0 -0
new file mode 100644
index 0000000000000..b52f3eac719f9

[ ... ]

Quoted 5 lines
kernel/trace/trace_wprobe.c new file +5 -0
+static void wprobe_trace_handler(struct trace_wprobe *tw,
+				 unsigned long addr,
+				 struct pt_regs *regs,
+				 struct trace_event_file *trace_file)
+{

[ ... ]

Quoted 9 lines

kernel/trace/trace_wprobe.c line 139

Quoted 5 lines
kernel/trace/trace_wprobe.c new file +5 -0
+	if (trace_trigger_soft_disabled(trace_file))
+		return;
+
+	if (tw->addr != addr)
+		return;

[Severity: Medium]
Does this strict address comparison inadvertently drop valid events if the hardware breakpoint address was updated dynamically?

Yes, to avoid confusion, if the watchpoint is changed on another CPU, it is better to be skipped.

Quoted 5 lines

The v7 changelog mentions using the dynamic address because the original address can be updated from another CPU, but this check causes the handler to exit immediately if the updated address differs from the statically recorded tw->addr.

Yeah, this is something for wprobe trigger, which will dynamically update the tw->addr. So maybe we can move this in wprobe event trigger patch.

Quoted 18 lines

[ ... ]

kernel/trace/trace_wprobe.c line 165

Quoted 11 lines
kernel/trace/trace_wprobe.c new file +11 -0
+static void wprobe_perf_handler(struct perf_event *bp,
+			      struct perf_sample_data *data,
+			      struct pt_regs *regs)
+{
+	struct trace_wprobe *tw = bp->overflow_handler_context;
+	struct event_file_link *link;
+	unsigned long addr = bp->attr.bp_addr;
+
+	trace_probe_for_each_link_rcu(link, &tw->tp)
+		wprobe_trace_handler(tw, addr, regs, link->file);
+}

[Severity: Medium]
Does this handler omit the necessary logic to generate and submit perf samples?

Ah, OK. This should not be used from perf. I think this should be explicitly prohibited at this moment.

Quoted 25 lines

[ ... ]

Quoted 3 lines
kernel/trace/trace_wprobe.c new file +3 -0
+static int enable_trace_wprobe(struct trace_event_call *call,
+			       struct trace_event_file *file)
+{

[ ... ]

Quoted 16 lines
kernel/trace/trace_wprobe.c new file +16 -0
+	if (file) {
+		ret = trace_probe_add_file(tp, file);
+		if (ret)
+			return ret;
+	} else {
+		trace_probe_set_flag(tp, TP_FLAG_PROFILE);
+	}
+
+	if (!enabled) {
+		list_for_each_entry(tw, trace_probe_probe_list(tp), tp.list) {
+			ret = __register_trace_wprobe(tw);
+			if (ret < 0) {
+				/* TODO: rollback */
+				return ret;
+			}
+		}
Quoted 8 lines

kernel/trace/trace_wprobe.c line 188

Quoted 4 lines
kernel/trace/trace_wprobe.c new file +4 -0
+	}
+
+	return 0;
+}

[Severity: High]
Could this missing rollback logic lead to a use-after-free or memory corruption if a later hardware breakpoint registration fails?

OK, to avoid memory corruption add a rollback logic here.

Quoted 34 lines

[ ... ]

Quoted 3 lines
kernel/trace/trace_wprobe.c new file +3 -0
+static int parse_address_spec(const char *spec, unsigned long *addr, int *type,
+			      int *len, char **symbol)
+{

[ ... ]

Quoted 11 lines
kernel/trace/trace_wprobe.c new file +11 -0
+	if (kstrtoul(at + 1, 0, &_addr) != 0) {
+		char *off_str = strpbrk(at + 1, "+-");
+		int offset = 0;
+
+		if (off_str) {
+			if (kstrtoint(off_str, 0, &offset) != 0) {
+				trace_probe_log_err(off_str - _spec, BAD_PROBE_ADDR);
+				return -EINVAL;
+			}
+			*off_str = '\0';
+		}

kernel/trace/trace_wprobe.c line 549

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+		_addr = kallsyms_lookup_name(at + 1);
+		if (!_addr) {
+			trace_probe_log_err(at + 1 - _spec, BAD_ACCESS_ADDR);
+			return -ENOENT;
+		}
+		_addr += offset;
+		*symbol = kstrdup(at + 1, GFP_KERNEL);
+		if (!*symbol)
+			return -ENOMEM;
+	}

[Severity: High]
Could the null terminator injection here break dynamic_events save and restore functionality for events with symbol offsets?

Quoted 6 lines

When the event is created with a symbol offset (like symbol+8), *off_str is overwritten with a null byte. This truncates the string before it gets duplicated into *symbol.

Later, trace_wprobe_show() outputs the truncated base symbol without the offset, meaning the restored event will monitor the wrong memory address.

Since the symbol is used for making the event name, it should not have offset. However, in trace_wprobe_show(), the symbol is directly used to show the target address. Thus, instead of changing parse_address_spec(), we need to update trace_wprobe_show() to use kallsyms_lookup_name() to find the symbol address and calculate the offset from tw->addr and symbol address, and print out the "symbol+offset".

Quoted 43 lines

[ ... ]

Quoted 2 lines
kernel/trace/trace_wprobe.c new file +2 -0
+static int __trace_wprobe_create(int argc, const char *argv[])
+{

[ ... ]

Quoted 5 lines
kernel/trace/trace_wprobe.c new file +5 -0
+	char *symbol = NULL;
+	unsigned long addr;
+	int len, type, i;
+	int ret = 0;
+

[ ... ]

kernel/trace/trace_wprobe.c line 604

Quoted 12 lines
kernel/trace/trace_wprobe.c new file +12 -0
+	trace_probe_log_set_index(1);
+	ret = parse_address_spec(argv[1], &addr, &type, &len, &symbol);
+	if (ret < 0)
+		return ret;
+
+	if (!event)
+		event = symbol ? symbol : "wprobe";
+
+	argc -= 2; argv += 2;
+	tw = alloc_trace_wprobe(group, event, symbol, addr, len, type, argc);
+	if (IS_ERR(tw))
+		return PTR_ERR(tw);

[Severity: Medium]
Does this leak the dynamically allocated symbol string?

The symbol variable is populated with an allocated string from parse_address_spec(). When passed to alloc_trace_wprobe(), that function duplicates the string again:

alloc_trace_wprobe() {
    ...
    if (symbol) {
        tw->symbol = kstrdup(symbol, GFP_KERNEL);
    ...
}
Quoted 2 lines

However, __trace_wprobe_create() never frees the original symbol string on either the success or error paths.

Yes, let me use __free() macro to free it.

Quoted 24 lines

[ ... ]

Quoted 6 lines
kernel/trace/trace_wprobe.c new file +6 -0
+static int trace_wprobe_show(struct seq_file *m, struct dyn_event *ev)
+{
+	struct trace_wprobe *tw = to_trace_wprobe(ev);
+	int i;
+
+	seq_printf(m, "w:%s/%s", trace_probe_group_name(&tw->tp),

kernel/trace/trace_wprobe.c line 662

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+		   trace_probe_name(&tw->tp));
+
+	char type_char;
+
+	if (tw->type == HW_BREAKPOINT_R)
+		type_char = 'r';
+	else if (tw->type == HW_BREAKPOINT_W)
+		type_char = 'w';
+	else
+		type_char = 'x'; /* Should be rw */

[Severity: High]
Will outputting 'x' for HW_BREAKPOINT_RW cause parse_address_spec() to reject the configuration upon restore?

Quoted 6 lines

If type_char is set to 'x', the formatted string saved to dynamic_events will be invalid. When the system attempts to restore the event across reboots, parse_address_spec() checks strictly for 'r', 'w', or 'rw', and will fail with -EINVAL for 'x'.

Should this be adjusted to output a string like "rw" instead?

Ah, good catch. Let me use rw instead.

Thank you,

Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
author maintainer
#4 lore raw

PATCH v7 2/10 x86: hw_breakpoint: Add a kconfig to clarify when a breakpoint fires

In reply to Masami Hiramatsu (Google)

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

Add CONFIG_HAVE_POST_BREAKPOINT_HOOK which indicates the hw_breakpoint on that architecture fires after the target memory has been modified. This is currently x86 only behavior.

---
 arch/Kconfig         |   10 ++++++++++
 arch/x86/Kconfig     |    1 +
 kernel/trace/Kconfig |    1 +
 3 files changed, 12 insertions(+)
arch/Kconfig +10 -0
index fa7507ac8e13..959aee9568ff 100644
@@ -457,6 +457,16 @@ config HAVE_MIXED_BREAKPOINTS_REGS
 	  Select this option if your arch implements breakpoints under the
 	  latter fashion.
 
+config HAVE_POST_BREAKPOINT_HOOK
+	bool
+	depends on HAVE_HW_BREAKPOINT
+	help
+	  Depending on the arch implementation of hardware breakpoints,
+	  some of them provide breakpoint hook after the target memory
+	  is modified.
+	  Select this option if your arch implements breakpoints overflow
+	  handler hooks after the target memory is modified.
+
 config HAVE_USER_RETURN_NOTIFIER
 	bool
 
arch/x86/Kconfig +1 -0
index bdad90f210e4..6b7e14ef8cfb 100644
@@ -246,6 +246,7 @@ config X86
 	select HAVE_FUNCTION_TRACER
 	select HAVE_GCC_PLUGINS
 	select HAVE_HW_BREAKPOINT
+	select HAVE_POST_BREAKPOINT_HOOK
 	select HAVE_IOREMAP_PROT
 	select HAVE_IRQ_EXIT_ON_IRQ_STACK	if X86_64
 	select HAVE_IRQ_TIME_ACCOUNTING
kernel/trace/Kconfig +1 -0
index b58c2565024f..d9b6fa5c35d9 100644
@@ -866,6 +866,7 @@ config WPROBE_EVENTS
 	bool "Enable wprobe-based dynamic events"
 	depends on TRACING
 	depends on HAVE_HW_BREAKPOINT
+	depends on HAVE_POST_BREAKPOINT_HOOK
 	select PROBE_EVENTS
 	select DYNAMIC_EVENTS
 	help
#5 lore raw

Re: [PATCH v7 02/10] x86: hw_breakpoint: Add a kconfig to clarify when a breakpoint fires

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider: - [Medium] Memory leak of the `symbol` string in `__trace_wprobe_create()`. - [Medium]…

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] Memory leak of the symbol string in __trace_wprobe_create().
- [Medium] enable_trace_wprobe() fails to rollback trace probe state on hardware breakpoint registration error, leading to state inconsistency and silent tracing failures. --

--- Patch [2]: [PATCH v7 02/10] x86: hw_breakpoint: Add a kconfig to clarify when a breakpoint fires --- commit f8a7aeb231575f15f9227ab411134bde7408e480
Author: Masami Hiramatsu (Google) <[email protected]>

tracing: wprobe: Add watchpoint probe event based on hardware breakpoint

Add a new probe event for the hardware breakpoint called wprobe-event. This wprobe allows user to trace (watch) the memory access at the specified memory address. The events can be defined via the dynamic_events interface file.

Quoted 6 lines
kernel/trace/trace_wprobe.c new file +0 -0
new file mode 100644
index 0000000000000..b52f3eac719f9
@@ -0,0 +1,692 @@

[ ... ]

Quoted 17 lines
kernel/trace/trace_wprobe.c new file +17 -0
+static int enable_trace_wprobe(struct trace_event_call *call,
+			       struct trace_event_file *file)
+{
+	struct trace_probe *tp;
+	struct trace_wprobe *tw;
+	bool enabled;
+	int ret = 0;
+
+	tp = trace_probe_primary_from_call(call);
+	if (WARN_ON_ONCE(!tp))
+		return -ENODEV;
+	enabled = trace_probe_is_enabled(tp);
+
+	if (file) {
+		ret = trace_probe_add_file(tp, file);
+		if (ret)
+			return ret;

kernel/trace/trace_wprobe.c

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+	} else {
+		trace_probe_set_flag(tp, TP_FLAG_PROFILE);
+	}
+
+	if (!enabled) {
+		list_for_each_entry(tw, trace_probe_probe_list(tp), tp.list) {
+			ret = __register_trace_wprobe(tw);
+			if (ret < 0) {
+				/* TODO: rollback */
+				return ret;

[Severity: Medium]
Will this leave the trace probe state inconsistent if hardware breakpoint registration fails?

Earlier in enable_trace_wprobe(), trace_probe_add_file() or trace_probe_set_flag() modify the probe state. If __register_trace_wprobe() returns an error, the function returns immediately without rolling back those changes.

Could this cause subsequent enable attempts to skip registration entirely and silently fail to trace?

Quoted 6 lines
kernel/trace/trace_wprobe.c new file +6 -0
+			}
+		}
+	}
+
+	return 0;
+}

[ ... ]

Quoted 38 lines
kernel/trace/trace_wprobe.c new file +38 -0
+static int __trace_wprobe_create(int argc, const char *argv[])
+{
+	/*
+	 * Argument syntax:
+	 *  b[:[GRP/][EVENT]] SPEC
+	 *
+	 * SPEC:
+	 *  [r|w|rw]@[ADDR|SYMBOL[+OFFS]][:LEN]
+	 */
+	struct traceprobe_parse_context *ctx __free(traceprobe_parse_context) = NULL;
+	struct trace_wprobe *tw __free(free_trace_wprobe) = NULL;
+	const char *event = NULL, *group = WPROBE_EVENT_SYSTEM;
+	const char *tplog __free(trace_probe_log_clear) = NULL;
+	char *symbol = NULL;
+	unsigned long addr;
+	int len, type, i;
+	int ret = 0;
+
+	if (argv[0][0] != 'w')
+		return -ECANCELED;
+
+	if (argc < 2)
+		return -EINVAL;
+
+	tplog = trace_probe_log_init("wprobe", argc, argv);
+
+	if (argv[0][1] != '\0') {
+		if (argv[0][1] != ':') {
+			trace_probe_log_set_index(0);
+			trace_probe_log_err(1, BAD_MAXACT_TYPE);
+			/* Invalid format */
+			return -EINVAL;
+		}
+		event = &argv[0][2];
+	}
+
+	trace_probe_log_set_index(1);
+	ret = parse_address_spec(argv[1], &addr, &type, &len, &symbol);

kernel/trace/trace_wprobe.c

Quoted 10 lines
kernel/trace/trace_wprobe.c new file +10 -0
+	if (ret < 0)
+		return ret;
+
+	if (!event)
+		event = symbol ? symbol : "wprobe";
+
+	argc -= 2; argv += 2;
+	tw = alloc_trace_wprobe(group, event, symbol, addr, len, type, argc);
+	if (IS_ERR(tw))
+		return PTR_ERR(tw);

[Severity: Medium]
Does this code leak the symbol string?

When a wprobe event is created using a symbol name, parse_address_spec() allocates a string for symbol. This string is then passed to alloc_trace_wprobe() which duplicates it again using kstrdup().

However, the original symbol pointer does not appear to be freed before __trace_wprobe_create() returns. Should symbol be annotated with __free(kfree) or explicitly freed?

Quoted 4 lines
kernel/trace/trace_wprobe.c new file +4 -0
+
+	ctx = kzalloc_obj(*ctx);
+	if (!ctx)
+		return -ENOMEM;
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=2
author maintainer
#6 lore raw

PATCH v7 3/10 selftests: tracing: Add a basic testcase for wprobe

In reply to Masami Hiramatsu (Google)

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

Add 'add_remove_wprobe.tc' testcase for testing wprobe event that tests adding and removing operations of the wprobe event.

---
 tools/testing/selftests/ftrace/config              |    1 
 .../ftrace/test.d/dynevent/add_remove_wprobe.tc    |   68 ++++++++++++++++++++
 2 files changed, 69 insertions(+)
 create mode 100644 tools/testing/selftests/ftrace/test.d/dynevent/add_remove_wprobe.tc
tools/testing/selftests/ftrace/config +1 -0
index 544de0db5f58..d2f503722020 100644
@@ -27,3 +27,4 @@ CONFIG_STACK_TRACER=y
 CONFIG_TRACER_SNAPSHOT=y
 CONFIG_UPROBES=y
 CONFIG_UPROBE_EVENTS=y
+CONFIG_WPROBE_EVENTS=y
tools/testing/selftests/ftrace/test.d/dynevent/add_remove_wprobe.tc new file +68 -0

68 lines not on this page · raw message

#7 lore raw

Re: [PATCH v7 03/10] selftests: tracing: Add a basic testcase for wprobe

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] The test script uses '!grep' without a space, causing a shell error that silently bypasses trace…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The test script uses '!grep' without a space, causing a shell error that silently bypasses trace validation. Additionally, the check lacks an 'exit_fail' call. --

--- Patch [3]: [PATCH v7 03/10] selftests: tracing: Add a basic testcase for wprobe --- commit 430a459576b6f13049e560d4a90b2c6a987f1ad3
Author: Masami Hiramatsu (Google) <[email protected]>
selftests: tracing: Add a basic testcase for wprobe

This commit adds a basic selftest testcase to verify adding and removing wprobe dynamic events.

Quoted 4 lines
tools/testing/selftests/ftrace/test.d/dynevent/add_remove_wprobe.tc new file +0 -0
new file mode 100644

[ ... ]

tools/testing/selftests/ftrace/test.d/dynevent/add_remove_wprobe.tc line 42

Quoted 8 lines
tools/testing/selftests/ftrace/test.d/dynevent/add_remove_wprobe.tc new file +8 -0
+# Let some time pass to trigger the breakpoint
+sleep 1
+
+# Check if we got any trace output
+if !grep -q my_wprobe trace; then
+    echo "wprobe event was not triggered"
+fi
+

[Severity: Medium]
Will this check silently bypass trace validation? When the shell evaluates "!grep", it treats it as a single command name rather than negating the grep command, which typically results in a 'command not found' error.

Because the shell error results in a non-zero exit code, the if condition sees this as false and entirely skips the block. Furthermore, even if written correctly as "! grep", the block misses an exit_fail call, so the test would still pass if the event failed to trigger.

Could this be updated to add a space after the exclamation mark and an exit_fail call?

[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=3
author maintainer
#8 lore raw

PATCH v7 4/10 selftests: tracing: Add syntax testcase for wprobe

In reply to Masami Hiramatsu (Google)

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

Add "wprobe_syntax_errors.tc" testcase for testing syntax errors of the watch probe events.

---
 .../test.d/dynevent/wprobes_syntax_errors.tc       |   20 ++++++++++++++++++++
 1 file changed, 20 insertions(+)
 create mode 100644 tools/testing/selftests/ftrace/test.d/dynevent/wprobes_syntax_errors.tc
tools/testing/selftests/ftrace/test.d/dynevent/wprobes_syntax_errors.tc new file +20 -0
new file mode 100644
index 000000000000..56ac579d60ae
@@ -0,0 +1,20 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# description: Watch probe event parser error log check
+# requires: dynamic_events "w[:[<group>/][<event>]] [r|w|rw]@<addr>[:<len>]":README
+
+check_error() { # command-with-error-pos-by-^
+    ftrace_errlog_check 'wprobe' "$1" 'dynamic_events'
+}
+
+check_error 'w ^symbol'			# BAD_ACCESS_FMT
+check_error 'w ^a@symbol'		# BAD_ACCESS_TYPE
+check_error 'w w@^symbol'		# BAD_ACCESS_ADDR
+check_error 'w w@jiffies^+offset'	# BAD_ACCESS_ADDR
+check_error 'w w@jiffies:^100'		# BAD_ACCESS_LEN
+check_error 'w w@jiffies ^$arg1'	# BAD_VAR
+check_error 'w w@jiffies ^$retval'	# BAD_VAR
+check_error 'w w@jiffies ^$stack'	# BAD_VAR
+check_error 'w w@jiffies ^%ax'		# BAD_VAR
+
+exit 0
author maintainer
#9 lore raw

PATCH v7 5/10 tracing: wprobe: Use a new seq_print_ip_sym_offset() wrapper

In reply to Masami Hiramatsu (Google)

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

Use a new seq_print_ip_sym_offset() wrapper function instead of using TRACE_ITER(SYM_OFFSET) mask directly.

---
 kernel/trace/trace_wprobe.c |    1 +
 1 file changed, 1 insertion(+)
kernel/trace/trace_wprobe.c +1 -0
index b52f3eac719f..dd310a87b333 100644
@@ -20,6 +20,7 @@
 #include <asm/ptrace.h>
 
 #include "trace_dynevent.h"
+#include "trace_output.h"
 #include "trace_probe.h"
 #include "trace_probe_kernel.h"
 #include "trace_probe_tmpl.h"
#10 lore raw

Re: [PATCH v7 05/10] tracing: wprobe: Use a new seq_print_ip_sym_offset() wrapper

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: - [High] Symbol offsets are discarded in wprobe definitions when displayed via tracefs, preventing…

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Symbol offsets are discarded in wprobe definitions when displayed via tracefs, preventing round-tripping of event specifications.
- [High] Failure to register hardware breakpoints leaves the trace_probe in a corrupted, pseudo-enabled state, causing future enablement attempts to silently fail.
- [High] The dynamically allocated symbol string is permanently leaked every time a wprobe with a symbol is created.
- [Medium] Meaningful code and commit message mismatch regarding the use of a new wrapper function.
--

--- Patch [5]: [PATCH v7 05/10] tracing: wprobe: Use a new seq_print_ip_sym_offset() wrapper --- commit f3a4507e611a20fcf1578dc82ce81258e7452d39
Author: Masami Hiramatsu (Google) <[email protected]>

tracing: wprobe: Use a new seq_print_ip_sym_offset() wrapper

The commit message states it refactors code to use a new seq_print_ip_sym_offset() wrapper. However, the commit only adds an include statement, missing the expected implementation changes.

Quoted 4 lines

tracing: wprobe: Use a new seq_print_ip_sym_offset() wrapper

Use a new seq_print_ip_sym_offset() wrapper function instead of using TRACE_ITER(SYM_OFFSET) mask directly.

[Severity: Medium]
Is this commit missing the actual implementation changes? The commit message claims to refactor code to use a new wrapper function, but the diff only adds an include directive.

kernel/trace/trace_wprobe.c line 26

Quoted 12 lines
kernel/trace/trace_wprobe.c +1 -0
index b52f3eac719f9..dd310a87b3333 100644
@@ -20,6 +20,7 @@
 #include <asm/ptrace.h>
 
 #include "trace_dynevent.h"
+#include "trace_output.h"
 #include "trace_probe.h"
 #include "trace_probe_kernel.h"
 #include "trace_probe_tmpl.h"

I also noticed a few other concerns in this file:

[Severity: High]
Does parse_address_spec() drop the offset from the base symbol?

kernel/trace/trace_wprobe.c:parse_address_spec() {
    ...
		if (off_str) {
			if (kstrtoint(off_str, 0, &offset) != 0) {
				trace_probe_log_err(off_str - _spec, BAD_PROBE_ADDR);
				return -EINVAL;
			}
			*off_str = '\0';
    ...
}

If the offset string is stripped and discarded here, it causes the offset to be dropped from the probe's symbol representation.

When trace_wprobe_show() emits the symbol, it is truncated:

kernel/trace/trace_wprobe.c:trace_wprobe_show() {
    ...
	if (tw->symbol)
		seq_printf(m, " %c@%s:%d", type_char, tw->symbol, len);
    ...
}

If a user reads the event definition via tracefs, it will display the probe as simply w@jiffies instead of w@jiffies+10. If this definition is saved and loaded back, it attaches to the base symbol instead of the offset address, leading to incorrect behavior.

[Severity: High]
Does enable_trace_wprobe() fail to handle rollback properly on error?

kernel/trace/trace_wprobe.c:enable_trace_wprobe() {
    ...
	if (!enabled) {
		list_for_each_entry(tw, trace_probe_probe_list(tp), tp.list) {
			ret = __register_trace_wprobe(tw);
			if (ret < 0) {
				/* TODO: rollback */
				return ret;
			}
		}
	}
    ...
}

If __register_trace_wprobe() fails, it returns an error immediately without clearing the flags set earlier (TP_FLAG_PROFILE or file links).

Could this missing rollback leave the trace_probe in an incorrectly enabled state, causing future enablement attempts to silently fail because trace_probe_is_enabled() will return true?

[Severity: High]
Does this code leak the symbol string?

kernel/trace/trace_wprobe.c:parse_address_spec() {
    ...
		_addr += offset;
		*symbol = kstrdup(at + 1, GFP_KERNEL);
		if (!*symbol)
			return -ENOMEM;
    ...
}

The string pointed to by symbol is allocated here. Then __trace_wprobe_create() passes it to alloc_trace_wprobe():

kernel/trace/trace_wprobe.c:__trace_wprobe_create() {
    ...
	ret = parse_address_spec(argv[1], &addr, &type, &len, &symbol);
	if (ret < 0)
		return ret;
    ...
	tw = alloc_trace_wprobe(group, event, symbol, addr, len, type, argc);
    ...
}

But alloc_trace_wprobe() duplicates the string again:

kernel/trace/trace_wprobe.c:alloc_trace_wprobe() {
    ...
	if (symbol) {
		tw->symbol = kstrdup(symbol, GFP_KERNEL);
		if (!tw->symbol)
			return ERR_PTR(-ENOMEM);
	}
    ...
}

The original symbol pointer in __trace_wprobe_create() is never freed before the function returns on either success or error paths, which might leak the string memory on every wprobe creation.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=5
author maintainer
#11 lore raw

Re: [PATCH v7 05/10] tracing: wprobe: Use a new seq_print_ip_sym_offset() wrapper

In reply to Masami Hiramatsu (Google)

Oops, I missed to fold this in the 1st patch... (the seq_print_ip_sym_offset() part is already folded)

Thanks,

Quoted 25 lines · On Wed, 15 Jul 2026 10:44:57 +0900 "Masami Hiramatsu (Google)"…

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

Use a new seq_print_ip_sym_offset() wrapper function instead of using TRACE_ITER(SYM_OFFSET) mask directly.

---
 kernel/trace/trace_wprobe.c |    1 +
 1 file changed, 1 insertion(+)
kernel/trace/trace_wprobe.c +1 -0
index b52f3eac719f..dd310a87b333 100644
@@ -20,6 +20,7 @@
 #include <asm/ptrace.h>
 
 #include "trace_dynevent.h"
+#include "trace_output.h"
 #include "trace_probe.h"
 #include "trace_probe_kernel.h"
 #include "trace_probe_tmpl.h"
Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
author maintainer
#12 lore raw

PATCH v7 6/10 x86/hw_breakpoint: Unify breakpoint install/uninstall

In reply to Masami Hiramatsu (Google)

From: Jinchao Wang <[email protected]>

Consolidate breakpoint management to reduce code duplication. The diffstat was misleading, so the stripped code size is compared instead. After refactoring, it is reduced from 11976 bytes to 11448 bytes on my x86_64 system built with clang.

This also makes it easier to introduce arch_reinstall_hw_breakpoint().

In addition, including linux/types.h to fix a missing build dependency.

---
 arch/x86/include/asm/hw_breakpoint.h |    6 +
 arch/x86/kernel/hw_breakpoint.c      |  141 +++++++++++++++++++---------------
 2 files changed, 84 insertions(+), 63 deletions(-)
arch/x86/include/asm/hw_breakpoint.h +6 -0
index 0bc931cd0698..aa6adac6c3a2 100644
@@ -5,6 +5,7 @@
 #include <uapi/asm/hw_breakpoint.h>
 
 #define	__ARCH_HW_BREAKPOINT_H
+#include <linux/types.h>
 
 /*
  * The name should probably be something dealt in
@@ -18,6 +19,11 @@ struct arch_hw_breakpoint {
 	u8		type;
 };
 
+enum bp_slot_action {
+	BP_SLOT_ACTION_INSTALL,
+	BP_SLOT_ACTION_UNINSTALL,
+};
+
 #include <linux/kdebug.h>
 #include <linux/percpu.h>
 #include <linux/list.h>
arch/x86/kernel/hw_breakpoint.c +78 -63

181 lines not on this page · raw message

#13 lore raw

Re: [PATCH v7 06/10] x86/hw_breakpoint: Unify breakpoint install/uninstall

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] Missing compiler memory barrier during hardware breakpoint disable allows reordering that breaks…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Missing compiler memory barrier during hardware breakpoint disable allows reordering that breaks NMI safety. --

--- Patch [6]: [PATCH v7 06/10] x86/hw_breakpoint: Unify breakpoint install/uninstall ---
Note: The format of this review has been altered to a free-form summary due to restrictions on quoting the original patch text.

commit 16356ea39e4e8d100873d56e9298c633a58cb692
Author: Jinchao Wang <[email protected]> x86/hw_breakpoint: Unify breakpoint install/uninstall

This patch consolidates breakpoint management to reduce code duplication and binary size. It introduces manage_bp_slot() and setup_hwbp() to handle the logic previously duplicated in install and uninstall functions.

File: arch/x86/kernel/hw_breakpoint.c
Function: setup_hwbp()

[Severity: High]
Could the repositioning of the barrier() in the disable path introduce a race condition with NMI handlers?

In setup_hwbp(), when enable is false, the code writes to the per-CPU cpu_dr7 cache after updating the hardware DR7 register via set_debugreg(). However, the barrier() call was placed before set_debugreg().

Since neither the native set_debugreg() nor this_cpu_write() provides a compiler memory clobber by default, is the compiler permitted to reorder this_cpu_write(cpu_dr7, dr7) to occur before the hardware set_debugreg() update?

If an NMI interrupts exactly between the reordered memory write to cpu_dr7 and the hardware DR7 register update, the NMI entry code (local_db_save) would observe cpu_dr7 as zero and skip clearing the hardware DR7 register. If the NMI handler then touches the watched address, would this trigger an infinite #DB exception recursion and crash the system?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=6
author maintainer
#14 lore raw

PATCH v7 7/10 x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint

In reply to Masami Hiramatsu (Google)

From: Jinchao Wang <[email protected]>

The new arch_reinstall_hw_breakpoint() function can be used in an atomic context, unlike the more expensive free and re-allocation path. This allows callers to efficiently re-establish an existing breakpoint.

---
 arch/x86/include/asm/hw_breakpoint.h |    2 ++
 arch/x86/kernel/hw_breakpoint.c      |    9 +++++++++
 2 files changed, 11 insertions(+)
arch/x86/include/asm/hw_breakpoint.h +2 -0
index aa6adac6c3a2..c22cc4e87fc5 100644
@@ -21,6 +21,7 @@ struct arch_hw_breakpoint {
 
 enum bp_slot_action {
 	BP_SLOT_ACTION_INSTALL,
+	BP_SLOT_ACTION_REINSTALL,
 	BP_SLOT_ACTION_UNINSTALL,
 };
 
@@ -65,6 +66,7 @@ extern int hw_breakpoint_exceptions_notify(struct notifier_block *unused,
 
 
 int arch_install_hw_breakpoint(struct perf_event *bp);
+int arch_reinstall_hw_breakpoint(struct perf_event *bp);
 void arch_uninstall_hw_breakpoint(struct perf_event *bp);
 void hw_breakpoint_pmu_read(struct perf_event *bp);
 void hw_breakpoint_pmu_unthrottle(struct perf_event *bp);
arch/x86/kernel/hw_breakpoint.c +9 -0
index 877509539300..9af8d81075db 100644
@@ -100,6 +100,10 @@ static int manage_bp_slot(struct perf_event *bp, enum bp_slot_action action)
 		old_bp = NULL;
 		new_bp = bp;
 		break;
+	case BP_SLOT_ACTION_REINSTALL:
+		old_bp = bp;
+		new_bp = bp;
+		break;
 	case BP_SLOT_ACTION_UNINSTALL:
 		old_bp = bp;
 		new_bp = NULL;
@@ -188,6 +192,11 @@ int arch_install_hw_breakpoint(struct perf_event *bp)
 	return arch_manage_bp(bp, BP_SLOT_ACTION_INSTALL);
 }
 
+int arch_reinstall_hw_breakpoint(struct perf_event *bp)
+{
+	return arch_manage_bp(bp, BP_SLOT_ACTION_REINSTALL);
+}
+
 void arch_uninstall_hw_breakpoint(struct perf_event *bp)
 {
 	arch_manage_bp(bp, BP_SLOT_ACTION_UNINSTALL);
#15 lore raw

Re: [PATCH v7 07/10] x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] arch_reinstall_hw_breakpoint() fails to clear old DR7 length and type bits before applying new…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] arch_reinstall_hw_breakpoint() fails to clear old DR7 length and type bits before applying new attributes, causing hardware state corruption if breakpoint parameters are modified. --

--- Patch [7]: [PATCH v7 07/10] x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint --- commit 6bcf7b8e474f5683afc4564b0a92754b65b43406
Author: Jinchao Wang <[email protected]>

x86/hw_breakpoint: Add arch_reinstall_hw_breakpoint

This commit introduces arch_reinstall_hw_breakpoint() to allow callers to efficiently re-establish existing hardware breakpoints in an atomic context without needing the full free and re-allocation path.

Quoted 4 lines
arch/x86/kernel/hw_breakpoint.c +0 -0
index 877509539300c..9af8d81075db7 100644

[ ... ]

arch/x86/kernel/hw_breakpoint.c int arch_install_hw_breakpoint(struct perf_event *bp) · line 198

Quoted 8 lines
arch/x86/kernel/hw_breakpoint.c +4 -0
@@ -188,6 +192,11 @@ int arch_install_hw_breakpoint(struct perf_event *bp)
 	return arch_manage_bp(bp, BP_SLOT_ACTION_INSTALL);
 }
 
+int arch_reinstall_hw_breakpoint(struct perf_event *bp)
+{
+	return arch_manage_bp(bp, BP_SLOT_ACTION_REINSTALL);
+}

[Severity: Medium]
When callers use this new function to update an existing breakpoint's length or type attributes, will this cause hardware state corruption in the DR7 control register?

The execution path goes through arch_manage_bp() and calls setup_hwbp() with enable = true. Looking at setup_hwbp(), it appears the new control bits are bitwise OR'ed with the existing mask:

arch/x86/kernel/hw_breakpoint.c:setup_hwbp() {
    ...
    if (enable)
        dr7 |= encode_dr7(slot, info->len, info->type);
    ...
}

Because the old DR7 configuration bits for this slot are never cleared first, could the combination of old and new bits create an invalid breakpoint configuration mask?

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=7
author maintainer
#16 lore raw

PATCH v7 8/10 HWBP: Add modify_wide_hw_breakpoint_local() API

In reply to Masami Hiramatsu (Google)

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

Add modify_wide_hw_breakpoint_local() arch-wide interface which allows hwbp users to update watch address on-line. This is available if the arch supports CONFIG_HAVE_REINSTALL_HW_BREAKPOINT.
Note that this allows to change the type only for compatible types, because it does not release and reserve the hwbp slot based on type. For instance, you can not change HW_BREAKPOINT_W to HW_BREAKPOINT_X.

---
Changes in v7:
- Update bp->attr.bp_attr so that we can correctly check the address on it.
- Use -EOPNOTSUPP instead of -ENOSYS.
Changes in v4:
- Update kerneldoc comment about modify_wide_hw_breakpoint_local according to Randy's comment.
Changes in v2:
- Check type compatibility by checking slot. (Thanks Jinchao!)

---
 arch/Kconfig                  |   10 ++++++++++
 arch/x86/Kconfig              |    1 +
 include/linux/hw_breakpoint.h |    6 ++++++
 kernel/events/hw_breakpoint.c |   39 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 56 insertions(+)
arch/Kconfig +10 -0
index 959aee9568ff..4a87e843bb4c 100644
@@ -467,6 +467,16 @@ config HAVE_POST_BREAKPOINT_HOOK
 	  Select this option if your arch implements breakpoints overflow
 	  handler hooks after the target memory is modified.
 
+config HAVE_REINSTALL_HW_BREAKPOINT
+	bool
+	depends on HAVE_HW_BREAKPOINT
+	help
+	  Depending on the arch implementation of hardware breakpoints,
+	  some of them are able to update the breakpoint configuration
+	  without release and reserve the hardware breakpoint register.
+	  What configuration is able to update depends on hardware and
+	  software implementation.
+
 config HAVE_USER_RETURN_NOTIFIER
 	bool
 
arch/x86/Kconfig +1 -0
index 6b7e14ef8cfb..588218da8f41 100644
@@ -247,6 +247,7 @@ config X86
 	select HAVE_GCC_PLUGINS
 	select HAVE_HW_BREAKPOINT
 	select HAVE_POST_BREAKPOINT_HOOK
+	select HAVE_REINSTALL_HW_BREAKPOINT
 	select HAVE_IOREMAP_PROT
 	select HAVE_IRQ_EXIT_ON_IRQ_STACK	if X86_64
 	select HAVE_IRQ_TIME_ACCOUNTING
include/linux/hw_breakpoint.h +6 -0
index db199d653dd1..6754ffbee9ed 100644
@@ -81,6 +81,9 @@ register_wide_hw_breakpoint(struct perf_event_attr *attr,
 			    perf_overflow_handler_t triggered,
 			    void *context);
 
+extern int modify_wide_hw_breakpoint_local(struct perf_event *bp,
+					   struct perf_event_attr *attr);
+
 extern int register_perf_hw_breakpoint(struct perf_event *bp);
 extern void unregister_hw_breakpoint(struct perf_event *bp);
 extern void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events);
@@ -124,6 +127,9 @@ register_wide_hw_breakpoint(struct perf_event_attr *attr,
 			    perf_overflow_handler_t triggered,
 			    void *context)		{ return NULL; }
 static inline int
+modify_wide_hw_breakpoint_local(struct perf_event *bp,
+				struct perf_event_attr *attr) { return -EOPNOTSUPP; }
+static inline int
 register_perf_hw_breakpoint(struct perf_event *bp)	{ return -ENOSYS; }
 static inline void unregister_hw_breakpoint(struct perf_event *bp)	{ }
 static inline void
kernel/events/hw_breakpoint.c +39 -0
index 789add0c185a..4337688da397 100644
@@ -888,6 +888,45 @@ void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events)
 }
 EXPORT_SYMBOL_GPL(unregister_wide_hw_breakpoint);
 
+/**
+ * modify_wide_hw_breakpoint_local - update breakpoint config for local CPU
+ * @bp: the hwbp perf event for this CPU
+ * @attr: the new attribute for @bp
+ *
+ * This does not release and reserve the slot of a HWBP; it just reuses the
+ * current slot on local CPU. So the users must update the other CPUs by
+ * themselves.
+ * Also, since this does not release/reserve the slot, this can not change the
+ * type to incompatible type of the HWBP.
+ * Return err if attr is invalid or the CPU fails to update debug register
+ * for new @attr.
+ */
+#ifdef CONFIG_HAVE_REINSTALL_HW_BREAKPOINT
+int modify_wide_hw_breakpoint_local(struct perf_event *bp,
+				    struct perf_event_attr *attr)
+{
+	int ret;
+
+	if (find_slot_idx(bp->attr.bp_type) != find_slot_idx(attr->bp_type))
+		return -EINVAL;
+
+	ret = hw_breakpoint_arch_parse(bp, attr, counter_arch_bp(bp));
+	if (ret)
+		return ret;
+
+	bp->attr.bp_addr = attr->bp_addr;
+
+	return arch_reinstall_hw_breakpoint(bp);
+}
+#else
+int modify_wide_hw_breakpoint_local(struct perf_event *bp,
+				    struct perf_event_attr *attr)
+{
+	return -EOPNOTSUPP;
+}
+#endif
+EXPORT_SYMBOL_GPL(modify_wide_hw_breakpoint_local);
+
 /**
  * hw_breakpoint_is_used - check if breakpoints are currently used
  *
#17 lore raw

Re: [PATCH v7 08/10] HWBP: Add modify_wide_hw_breakpoint_local() API

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: - [High] State Corruption on Error Path during breakpoint modification - [High] Hardware Register…

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] State Corruption on Error Path during breakpoint modification
- [High] Hardware Register Corruption (DR7 missing bit clear) on Reinstall
- [Medium] Inconsistent Software State after breakpoint modification
- [Medium] Race Condition with Debug Exception Handlers during in-place modification --

--- Patch [8]: [PATCH v7 08/10] HWBP: Add modify_wide_hw_breakpoint_local() API --- commit e82221932a08cae24063fb7cb3fe49f250391aa5
Author: Masami Hiramatsu (Google) <[email protected]>

HWBP: Add modify_wide_hw_breakpoint_local() API

This commit adds the modify_wide_hw_breakpoint_local() arch-wide interface which allows hwbp users to update watch addresses on-line. It reuses the current hardware breakpoint slot without releasing and reserving it, meaning type changes are only allowed between compatible types.

Quoted 4 lines
kernel/events/hw_breakpoint.c +0 -0
index 789add0c185a0..4337688da3976 100644

[ ... ]

Quoted 17 lines
kernel/events/hw_breakpoint.c +13 -0
@@ -888,6 +888,45 @@ void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events)
 }
 EXPORT_SYMBOL_GPL(unregister_wide_hw_breakpoint);
 
+/**
+ * modify_wide_hw_breakpoint_local - update breakpoint config for local CPU
+ * @bp: the hwbp perf event for this CPU
+ * @attr: the new attribute for @bp
+ *
+ * This does not release and reserve the slot of a HWBP; it just reuses the
+ * current slot on local CPU. So the users must update the other CPUs by
+ * themselves.
+ * Also, since this does not release/reserve the slot, this can not change the
+ * type to incompatible type of the HWBP.
+ * Return err if attr is invalid or the CPU fails to update debug register
+ * for new @attr.
+ */

kernel/events/hw_breakpoint.c void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events) · line 913

Quoted 10 lines
kernel/events/hw_breakpoint.c +10 -0
+#ifdef CONFIG_HAVE_REINSTALL_HW_BREAKPOINT
+int modify_wide_hw_breakpoint_local(struct perf_event *bp,
+				    struct perf_event_attr *attr)
+{
+	int ret;
+
+	if (find_slot_idx(bp->attr.bp_type) != find_slot_idx(attr->bp_type))
+		return -EINVAL;
+
+	ret = hw_breakpoint_arch_parse(bp, attr, counter_arch_bp(bp));

[Severity: Medium]
Is it safe to mutate the active hardware state structure in-place while the hardware breakpoint is still active? If a debug exception (#DB) triggers on the same CPU during this non-atomic update, could hw_breakpoint_handler() read a partially updated bp->hw.info and generate flawed samples?

[Severity: High]
Could this corrupt the active state on error paths? By passing the active hardware configuration pointer via counter_arch_bp(bp) directly, the architecture parser mutates it in place.

For example, in arch_build_bp_info() on x86, if length or type is updated but a subsequent alignment check fails, we return an error but leave the active struct partially modified. When the breakpoint is later uninstalled, could this use the corrupted length/type to mask out DR7, leaving residual bits that permanently corrupt the slot?

kernel/events/hw_breakpoint.c void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events) · line 917

Quoted 4 lines
kernel/events/hw_breakpoint.c +4 -0
+	if (ret)
+		return ret;
+
+	bp->attr.bp_addr = attr->bp_addr;

[Severity: Medium]
Does this leave the logical perf_event attributes disconnected from the hardware configuration? The parser updates the hardware state to reflect the new type and length, but we only copy attr->bp_addr here. Should this also update bp->attr.bp_type and bp->attr.bp_len?

kernel/events/hw_breakpoint.c void unregister_wide_hw_breakpoint(struct perf_event * __percpu *cpu_events) · line 919

Quoted 2 lines
kernel/events/hw_breakpoint.c +2 -0
+
+	return arch_reinstall_hw_breakpoint(bp);

[Severity: High]
Can reinstalling the breakpoint without clearing the old hardware state corrupt the debug registers? When arch_reinstall_hw_breakpoint() invokes setup_hwbp() on x86, it ORs the new length and type bits into DR7:

arch/x86/kernel/hw_breakpoint.c:setup_hwbp()

    if (enable)
        dr7 |= encode_dr7(slot, info->len, info->type);

Since we did not disable the breakpoint first, won't a bitwise OR of the old and new encoded values produce an invalid configuration?

Additionally, if the new configuration sets info->mask to 0, setup_hwbp() skips resetting the hardware mask MSR because it evaluates if (info->mask). Could this leave the old mask permanently active?

Quoted 2 lines
kernel/events/hw_breakpoint.c +2 -0
+}
+#else

[ ... ]

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=8
author maintainer
#18 lore raw

PATCH v7 9/10 tracing: wprobe: Add wprobe event trigger

In reply to Masami Hiramatsu (Google)

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

Add wprobe event trigger to set and clear the watch event dynamically. This allows us to set an watchpoint on a given local variables and a slab object instead of static objects.

The trigger syntax is below:

- set_wprobe:WPROBE:FIELD[+OFFSET] [if FILTER]
- clear_wprobe:WPROBE[:FIELD[+OFFSET]] [if FILTER]

set_wprobe sets the address pointed by FIELD[+offset] to the WPROBE event. The FIELD is the field name of trigger event. clear_wprobe clears the watch address of WPROBE event. If the FIELD option is specified, it clears only if the current watch address is same as the given FIELD[+OFFSET] value.

The set_wprobe trigger does not change the type and length, these must be set when creating a new wprobe.

Also, the WPROBE event must be disabled when setting the new trigger and it will be busy afterwards. Recommended usage is to add a new wprobe at NULL address and keep disabled.

---
Changes in v7:
- Use kzalloc_obj().
- Update sample code in document.
Changes in v6:
- Update according to the latest change of trigger ops.
Changes in v5:
- Following the suggestions, the documentation was revised to suit rst.
Changes in v3:
- Add FIELD option support for clear_wprobe and update document.
- Fix to unregister/free event_trigger_data on file correctly.
- Fix syntax comments.
Changes in v2:
- Getting local cpu perf_event from trace_wprobe directly.
- Remove trace_wprobe_local_perf() because it is conditionally unused.
- Make CONFIG_WPROBE_TRIGGERS a hidden config.

---
 Documentation/trace/wprobetrace.rst |   89 ++++++++
 include/linux/trace_events.h        |    1 
 kernel/trace/Kconfig                |   10 +
 kernel/trace/trace_wprobe.c         |  415 +++++++++++++++++++++++++++++++++++
 4 files changed, 515 insertions(+)
Documentation/trace/wprobetrace.rst +89 -0
index 025b4c39b809..6f3d2afb35e3 100644
@@ -67,3 +67,92 @@ Here is an example to add a wprobe event on a variable `jiffies`.
            <idle>-0       [000] d.Z1.  717.026373: my_jiffies: (tick_do_update_jiffies64+0xbe/0x130)
 
 You can see the code which writes to `jiffies` is `tick_do_update_jiffies64()`.
+
+Combination with trigger action
+-------------------------------
+The event trigger action can extend the utilization of this wprobe.
+
+- set_wprobe:WPEVENT:FIELD[+|-ADJUST]
+- clear_wprobe:WPEVENT[:FIELD[+|-]ADJUST]
+
+Set these triggers to the target event, then the WPROBE event will be
+setup to trace the memory access at FIELD[+|-ADJUST] address.
+When clear_wprobe is hit, if FIELD is NOT specified, the WPEVENT is
+forcibly cleared. If FIELD[[+|-]ADJUST] is set, it clears WPEVENT only
+if its watching address is the same as the FIELD[[+|-]ADJUST] value.
+
+Notes:
+The set_wprobe trigger does not change the type and length, these
+must be set when creating a new wprobe.
+
+The WPROBE event must be disabled when setting the new trigger
+and it will be busy afterwards. Recommended usage is to add a new
+wprobe at NULL address and keep disabled.
+
+
+For example, trace the first 8 bytes of the dentry data structure passed
+to do_truncate() until it is deleted by dentry_kill().
+(Note: all tracefs setup uses '>>' so that it does not kick do_truncate())
+::
+
+  # echo 'w:watch rw@0:8 address=$addr value=+0($addr)' >> dynamic_events
+  # echo 'f:truncate do_truncate dentry=$arg2' >> dynamic_events
+  # echo 'set_wprobe:watch:dentry' >> events/fprobes/truncate/trigger
+  # echo 'f:dentry_kill dentry_kill dentry=$arg1' >> dynamic_events
+  # echo 'clear_wprobe:watch:dentry' >> events/fprobes/dentry_kill/trigger
+  # echo 1 >> events/fprobes/truncate/enable
+  # echo 1 >> events/fprobes/dentry_kill/enable
+
+  # echo aaa > /tmp/hoge
+  # echo bbb > /tmp/hoge
+  # echo ccc > /tmp/hoge
+  # rm /tmp/hoge
+
+Then, the trace data will show::
+
+ # tracer: nop
+ #
+ # entries-in-buffer/entries-written: 32/32   #P:8
+ #
+ #                                _-----=> irqs-off/BH-disabled
+ #                               / _----=> need-resched
+ #                              | / _---=> hardirq/softirq
+ #                              || / _--=> preempt-depth
+ #                              ||| / _-=> migrate-disable
+ #                              |||| /     delay
+ #           TASK-PID     CPU#  |||||  TIMESTAMP  FUNCTION
+ #              | |         |   |||||     |         |
+               sh-107     [004] ...1.     9.990418: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004ad6618
+               sh-107     [004] ...1.     9.990914: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004b3de78
+               sh-107     [004] ...1.     9.993175: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff8880049ddd40
+               sh-107     [004] .....     9.995198: truncate: (do_truncate+0x4/0x120) dentry=0xffff8880048083a8
+               sh-107     [004] ...1.     9.995389: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff8880049db998
+               sh-107     [004] ..Zff     9.997503: watch: (lookup_fast+0xaa/0x150) address=0xffff8880048083a8 value=0x8200080
+               sh-107     [004] ..Zff     9.997509: watch: (path_openat+0x211/0xda0) address=0xffff8880048083a8 value=0x8200080
+               sh-107     [004] ..Zff     9.997514: watch: (path_openat+0xa56/0xda0) address=0xffff8880048083a8 value=0x8200080
+               sh-107     [004] ..Zff     9.997518: watch: (path_openat+0xae2/0xda0) address=0xffff8880048083a8 value=0x8200080
+               sh-107     [004] .....     9.997521: truncate: (do_truncate+0x4/0x120) dentry=0xffff8880048083a8
+               sh-107     [004] ...1.     9.997582: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004808270
+               sh-107     [004] ...1.     9.999365: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff8880049db728
+               sh-107     [004] ...1.     9.999388: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004b1c000
+               rm-113     [005] ..Zff    10.000965: watch: (lookup_fast+0xaa/0x150) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.000971: watch: (path_lookupat+0x97/0x1e0) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.000984: watch: (lookup_fast+0xaa/0x150) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.000988: watch: (path_lookupat+0x97/0x1e0) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.001010: watch: (lookup_one_qstr_excl+0x28/0x140) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.001014: watch: (lookup_one_qstr_excl+0xd1/0x140) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.001018: watch: (may_delete_dentry+0x1c/0x200) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.001021: watch: (may_delete_dentry+0x195/0x200) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] ..Zff    10.001031: watch: (vfs_unlink+0x5e/0x260) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] d.Z..    10.001067: watch: (d_make_discardable+0x1b/0x40) address=0xffff8880048083a8 value=0x8200080
+               rm-113     [005] d.Z..    10.001071: watch: (d_make_discardable+0x29/0x40) address=0xffff8880048083a8 value=0x200080
+               rm-113     [005] ...1.    10.001072: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff8880048083a8
+               rm-113     [005] ...1.    10.001218: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff8880048083a8
+               sh-107     [004] ...1.    10.001416: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff8880049db110
+               sh-107     [004] ...1.    10.001444: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff8880049db248
+               sh-107     [004] ...1.    10.001500: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004ad6618
+               sh-107     [004] ...1.    10.002067: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004b41e78
+               sh-107     [004] ...1.    10.904920: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004b41e78
+               sh-107     [004] ...1.    10.905129: dentry_kill: (dentry_kill+0x0/0x2c0) dentry=0xffff888004ad6618
+
+You can see the watch event is correctly configured on the dentry.
include/linux/trace_events.h +1 -0
index d1e5ab71d928..e6f3bbcbb9af 100644
@@ -729,6 +729,7 @@ enum event_trigger_type {
 	ETT_EVENT_HIST		= (1 << 4),
 	ETT_HIST_ENABLE		= (1 << 5),
 	ETT_EVENT_EPROBE	= (1 << 6),
+	ETT_EVENT_WPROBE	= (1 << 7),
 };
 
 extern int filter_match_preds(struct event_filter *filter, void *rec);
kernel/trace/Kconfig +10 -0
index d9b6fa5c35d9..af570fa2a882 100644
@@ -876,6 +876,16 @@ config WPROBE_EVENTS
 	  Those events can be inserted wherever hardware breakpoints can be
 	  set, and record accessed memory address and values.
 
+config WPROBE_TRIGGERS
+	depends on WPROBE_EVENTS
+	depends on HAVE_REINSTALL_HW_BREAKPOINT
+	bool
+	default y
+	help
+	  This adds an event trigger which will set the wprobe on a specific
+	  field of an event. This allows user to trace the memory access of
+	  an address pointed by the event field.
+
 config BPF_EVENTS
 	depends on BPF_SYSCALL
 	depends on (KPROBE_EVENTS || UPROBE_EVENTS) && PERF_EVENTS
kernel/trace/trace_wprobe.c +415 -0

435 lines not on this page · raw message

#19 lore raw

Re: [PATCH v7 09/10] tracing: wprobe: Add wprobe event trigger

Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider: - [Critical] Deadlocks due to NMI-unsafe operations (schedule_work and raw_spin_lock_irqsave) in tracepoint…

Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [Critical] Deadlocks due to NMI-unsafe operations (schedule_work and raw_spin_lock_irqsave) in tracepoint context.
- [High] Missing cancel_work_sync() causes a Use-After-Free of wprobe_trigger_data via the workqueue.
- [High] Synchronous free of wprobe_data violates RCU removal ordering, risking concurrent use-after-free.
- [High] Missing trace_event_try_get_ref() and imbalanced trace_event_put_ref() cause module refcount underflow.
- [High] on_each_cpu is called with wait=false for an IPI that references wprobe_data.
- [High] Immediate kfree() of RCU-protected trigger_data and wprobe_data on the error path.
- [Medium] Permanent memory leak of wprobe trigger objects on successful registration.
- [Medium] tw->addr is read and written concurrently by different triggers without a shared lock. --

--- Patch [9]: [PATCH v7 09/10] tracing: wprobe: Add wprobe event trigger --- The format of this report is altered due to recitation restrictions. I am providing a free-form summary of the findings rather than inline quoting the patch.

Commit: 4087513c82b11b98d27cfc4452df9a1a8e41ddd5
Author: Masami Hiramatsu (Google) <[email protected]>
Subject: tracing: wprobe: Add wprobe event trigger

This patch adds a wprobe event trigger to set and clear watch events dynamically, allowing watchpoints on local variables and slab objects.

[Severity: Critical]
Is it safe to call raw_spin_lock_irqsave() and schedule_work() directly from wprobe_trigger()? Tracepoints can execute in arbitrary contexts, including NMI or with scheduler locks held. Calling schedule_work() is unsafe in these contexts. Additionally, acquiring wprobe_data->lock here can cause a hard deadlock if the tracepoint interrupts wprobe_smp_update_func() while it is holding the exact same lock in IPI context.

[Severity: High]
Does free_wprobe_trigger_data() safely handle the embedded work struct? When a trigger is unregistered, this function frees wprobe_data via kfree(). If wprobe_trigger() recently scheduled wprobe_data->work, the workqueue could execute wprobe_work_func() on the freed memory since there is no cancel_work_sync() before freeing.

[Severity: High]
Is an RCU grace period needed in wprobe_trigger_free()? wprobe_unregister_trigger() unlinks the trigger and calls wprobe_trigger_free(), which synchronously frees wprobe_data. Since tracepoints execute under RCU, could a concurrent tracepoint execution still be accessing wprobe_data when it gets freed?

[Severity: High]
Can the error path in wprobe_trigger_cmd_parse() cause a use-after-free? If event_trigger_register() successfully exposes the trigger to RCU readers but a subsequent step fails, returning an error causes the __free(kfree) cleanup to immediately free trigger_data and wprobe_data. This bypasses any RCU grace period for objects that were just briefly exposed to concurrent tracepoint readers.

[Severity: High]
Is it intentional that on_each_cpu() is called with wait=false in wprobe_work_func()? Because it doesn't wait, the work function completes immediately while IPIs are still pending on other CPUs. If the trigger is unregistered and freed right after, wprobe_smp_update_func() will access freed wprobe_data.

[Severity: High]
Does wprobe_trigger_cmd_parse() acquire the target event's module reference? The code calls trace_event_enable_disable() directly without first calling trace_event_try_get_ref(). However, wprobe_trigger_free() unconditionally calls trace_event_put_ref(), which could lead to a module refcount underflow.

[Severity: Medium]
Is there a memory leak on the success path of wprobe_trigger_cmd_parse()? The function increments the refcount via event_trigger_init(), and then event_trigger_register() increments it again. On success, trigger_data is set to NULL to bypass the cleanup macro, but the initial refcount is never dropped. When the trigger is later unregistered, the refcount only drops to 1, permanently leaking the objects.

[Severity: Medium]
How is concurrent access to tw->addr synchronized between different triggers? If a user attaches multiple triggers (e.g., set_wprobe and clear_wprobe) targeting the same wprobe event, they allocate separate wprobe_data instances with independent locks. When wprobe_trigger() executes, they will read and write the shared tw->addr simultaneously without a common lock, potentially corrupting the hardware breakpoint state.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=9
author maintainer
#20 lore raw

PATCH v7 10/10 selftests: ftrace: Add wprobe trigger testcase

In reply to Masami Hiramatsu (Google)

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

Add a testcase for checking wprobe trigger. This sets set_wprobe and clear_wprobe triggers on fprobe events to watch dentry access. So this depends on both wprobe and fprobe.

---
Changes in v7:
- Add a newline at the end of file.(style fix)
- Use dentry_kill instead of __dentry_kill.
Changes in v5:
- Enable CONFIG_WPROBE_TRIGGERS in the config for ftrace test.
Changes in v3:
- Newly added.

---
 tools/testing/selftests/ftrace/config              |    1 
 .../ftrace/test.d/trigger/trigger-wprobe.tc        |   48 ++++++++++++++++++++
 2 files changed, 49 insertions(+)
 create mode 100644 tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc
tools/testing/selftests/ftrace/config +1 -0
index d2f503722020..ecdee77f360f 100644
@@ -28,3 +28,4 @@ CONFIG_TRACER_SNAPSHOT=y
 CONFIG_UPROBES=y
 CONFIG_UPROBE_EVENTS=y
 CONFIG_WPROBE_EVENTS=y
+CONFIG_WPROBE_TRIGGERS=y
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc new file +48 -0

48 lines not on this page · raw message

#21 lore raw

Re: [PATCH v7 10/10] selftests: ftrace: Add wprobe trigger testcase

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider: - [High] Permanent leak of trigger_data and wprobe_data when unregistering a wprobe trigger. - [Medium] The…

Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [High] Permanent leak of trigger_data and wprobe_data when unregistering a wprobe trigger.
- [Medium] The selftest fails to declare its dependency on fprobe in the # requires directive, causing test failures on unsupported kernels.
- [Low] The test uses a hardcoded path /tmp/hoge for triggering file operations, leading to test brittleness.
- [Low] The test hooks a static function dentry_kill without checking its presence in available_filter_functions. --

--- Patch [10]: [PATCH v7 10/10] selftests: ftrace: Add wprobe trigger testcase --- commit 3a609968e71e94ce3399d95a2f00d2d3b0974ab0
Author: Masami Hiramatsu (Google) <[email protected]>

selftests: ftrace: Add wprobe trigger testcase

This commit adds a testcase to check the wprobe trigger functionality. It sets set_wprobe and clear_wprobe triggers on fprobe events to watch dentry access, validating features that depend on both wprobe and fprobe.

[Severity: High]
While not part of this commit's diff, there appears to be a regression associated with the wprobe trigger registration tested here.

In kernel/trace/trace_wprobe.c:wprobe_trigger_cmd_parse():

	ret = event_trigger_register(cmd_ops, file, glob, trigger_data);
	if (ret) {
		event_trigger_reset_filter(cmd_ops, trigger_data);
		trace_event_enable_disable(wprobe_file, 0, 1);
		return ret;
	}
	/* Make it NULL to avoid freeing trigger_data and wprobe_data by __free() */
	trigger_data = NULL;
	wprobe_data = NULL;

	return 0;

Does this code permanently leak the trigger_data and wprobe_data structures when unregistering a wprobe trigger?

The successful path returns without calling event_trigger_free(trigger_data), stranding the reference count at 2.

tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc line 4

Quoted 10 lines
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc new file +4 -0
new file mode 100644
index 0000000000000..cc7a0532d7ff8
@@ -0,0 +1,48 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# description: event trigger - test wprobe trigger
+# requires: dynamic_events "w[:[<group>/][<event>]] [r|w|rw]@<addr>[:<len>]":README events/sched/sched_process_fork/trigger

[Severity: Medium]
Will this script fail with an error exit code on environments without fprobe support, rather than gracefully skipping the test?

The test relies on fprobe (f:), but does not declare this dependency in the

# requires directive.

Without this requirement, the ftracetest runner will try to write f:truncate to dynamic_events on kernels configured with CONFIG_WPROBE_EVENTS=y but without CONFIG_FPROBE_EVENTS, causing the test to fail.

tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc line 13

Quoted 9 lines
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc new file +9 -0
+
+echo 0 > tracing_on
+
+:;: "Add a wprobe event used by trigger" ;:
+echo 'w:watch rw@0:8 address=$addr value=+0($addr)' > dynamic_events
+
+:;: "Add events for triggering wprobe" ;:
+echo 'f:truncate do_truncate dentry=$arg2' >> dynamic_events
+echo 'f:dentry_kill dentry_kill dentry=$arg1' >> dynamic_events

[Severity: Low]
Is it safe to assume the static function dentry_kill is always available for tracing?

The script attaches an fprobe to dentry_kill without checking if it exists in available_filter_functions.

If the compiler aggressively inlines or eliminates dentry_kill, the fprobe creation will fail and abort the test.

[ ... ]

tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc line 34

Quoted 7 lines
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc new file +7 -0
+:;: "Start test workload" ;:
+echo 1 >> tracing_on
+
+echo aaa > /tmp/hoge
+echo bbb > /tmp/hoge
+echo ccc > /tmp/hoge
+rm /tmp/hoge

[Severity: Low]
Could this hardcoded path cause unexpected failures if /tmp/hoge already exists, is owned by another user, or if tests run in parallel?

The test writes to a fixed global path instead of relying on the isolated $TMPDIR standard within ftracetest.

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178407983818.95826.12714571928538799781.stgit@devnote2?part=10
author maintainer
#22 lore raw

Re: [PATCH v7 10/10] selftests: ftrace: Add wprobe trigger testcase

Quoted 11 lines · On Wed, 15 Jul 2026 02:05:55 +0000 [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [Critical] Deadlocks due to NMI-unsafe operations (schedule_work and raw_spin_lock_irqsave) in tracepoint context.
- [High] Missing cancel_work_sync() causes a Use-After-Free of wprobe_trigger_data via the workqueue.
- [High] Synchronous free of wprobe_data violates RCU removal ordering, risking concurrent use-after-free.
- [High] Missing trace_event_try_get_ref() and imbalanced trace_event_put_ref() cause module refcount underflow.
- [High] on_each_cpu is called with wait=false for an IPI that references wprobe_data.
- [High] Immediate kfree() of RCU-protected trigger_data and wprobe_data on the error path.
- [Medium] Permanent memory leak of wprobe trigger objects on successful registration.
- [Medium] tw->addr is read and written concurrently by different triggers without a shared lock. --

Thanks for the review comments.
BTW, why this only to linux-perf-users? (Maybe I need to modify Sashiko setting)

Quoted 12 lines

--- Patch [9]: [PATCH v7 09/10] tracing: wprobe: Add wprobe event trigger --- The format of this report is altered due to recitation restrictions. I am providing a free-form summary of the findings rather than inline quoting the patch.

Commit: 4087513c82b11b98d27cfc4452df9a1a8e41ddd5
Author: Masami Hiramatsu (Google) <[email protected]>
Subject: tracing: wprobe: Add wprobe event trigger

This patch adds a wprobe event trigger to set and clear watch events dynamically, allowing watchpoints on local variables and slab objects.

Quoted 7 lines

[Severity: Critical]
Is it safe to call raw_spin_lock_irqsave() and schedule_work() directly from wprobe_trigger()? Tracepoints can execute in arbitrary contexts, including NMI or with scheduler locks held. Calling schedule_work() is unsafe in these contexts. Additionally, acquiring wprobe_data->lock here can cause a hard deadlock if the tracepoint interrupts wprobe_smp_update_func() while it is holding the exact same lock in IPI context.

Hmm, good catch!
OK, use a pair of irq_work and work to call on_each_cpu() for some interrupt context.
For NMI context, I think wprobe_trigger should check the original state is in_nmi(). This can work for normal trace events or fprobe, but not for kprobes, because kprobe handler itself is working inside NMI context. Hmm, let me check how can I fix this. Until solving this problem, this wprobe trigger may not work with kprobes.
Also, I found the lock should protect the wprobe itself, not data. Let me move the irq_work and lock to trace_wprobe.

Hmm, we may need to pass to original context (calculated by preempt_count) to trigger event so that it can evaluate the original context. (BTW, )

Quoted 6 lines

[Severity: High]
Does free_wprobe_trigger_data() safely handle the embedded work struct? When a trigger is unregistered, this function frees wprobe_data via kfree(). If wprobe_trigger() recently scheduled wprobe_data->work, the workqueue could execute wprobe_work_func() on the freed memory since there is no cancel_work_sync() before freeing.

OK, so move the worker to wprobe because anyway the worker will update the wprobe itself.

Quoted 7 lines

[Severity: High]
Is an RCU grace period needed in wprobe_trigger_free()? wprobe_unregister_trigger() unlinks the trigger and calls wprobe_trigger_free(), which synchronously frees wprobe_data. Since tracepoints execute under RCU, could a concurrent tracepoint execution still be accessing wprobe_data when it gets freed?

Indeed, let's use call_rcu().

Quoted 8 lines

[Severity: High]
Can the error path in wprobe_trigger_cmd_parse() cause a use-after-free? If event_trigger_register() successfully exposes the trigger to RCU readers but a subsequent step fails, returning an error causes the __free(kfree) cleanup to immediately free trigger_data and wprobe_data. This bypasses any RCU grace period for objects that were just briefly exposed to concurrent tracepoint readers.

Ah, good catch. OK, let me call synchronize_rcu() before returning.

Quoted 7 lines

[Severity: High]
Is it intentional that on_each_cpu() is called with wait=false in wprobe_work_func()? Because it doesn't wait, the work function completes immediately while IPIs are still pending on other CPUs. If the trigger is unregistered and freed right after, wprobe_smp_update_func() will access freed wprobe_data.

Ah, indeed. I will use a combination of irq_worker and worker and use on_each_cpu() with wait=true for safety.

Quoted 7 lines

[Severity: High]
Does wprobe_trigger_cmd_parse() acquire the target event's module reference? The code calls trace_event_enable_disable() directly without first calling trace_event_try_get_ref(). However, wprobe_trigger_free() unconditionally calls trace_event_put_ref(), which could lead to a module refcount underflow.

OK, let me fix it.

Quoted 8 lines

[Severity: Medium]
Is there a memory leak on the success path of wprobe_trigger_cmd_parse()? The function increments the refcount via event_trigger_init(), and then event_trigger_register() increments it again. On success, trigger_data is set to NULL to bypass the cleanup macro, but the initial refcount is never dropped. When the trigger is later unregistered, the refcount only drops to 1, permanently leaking the objects.

OK, call trace_event_put_ref directly in wprobe_trigger_cmd_parse().

Quoted 8 lines

[Severity: Medium]
How is concurrent access to tw->addr synchronized between different triggers? If a user attaches multiple triggers (e.g., set_wprobe and clear_wprobe) targeting the same wprobe event, they allocate separate wprobe_data instances with independent locks. When wprobe_trigger() executes, they will read and write the shared tw->addr simultaneously without a common lock, potentially corrupting the hardware breakpoint state.

I decided to use a lock in trace_wprobe side and use READ_ONCE/WRITE_ONCE() which may be enough.

Thank you,

Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
author maintainer
#23 lore raw

Re: [PATCH v7 10/10] selftests: ftrace: Add wprobe trigger testcase

Oops, sorry for confusion, this is for [9/10].

Thanks,

Quoted 133 lines · On Thu, 16 Jul 2026 10:59:13 +0900 Masami Hiramatsu (Google)…
Quoted 11 lines · On Wed, 15 Jul 2026 02:05:55 +0000 [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 8 potential issue(s) to consider:
- [Critical] Deadlocks due to NMI-unsafe operations (schedule_work and raw_spin_lock_irqsave) in tracepoint context.
- [High] Missing cancel_work_sync() causes a Use-After-Free of wprobe_trigger_data via the workqueue.
- [High] Synchronous free of wprobe_data violates RCU removal ordering, risking concurrent use-after-free.
- [High] Missing trace_event_try_get_ref() and imbalanced trace_event_put_ref() cause module refcount underflow.
- [High] on_each_cpu is called with wait=false for an IPI that references wprobe_data.
- [High] Immediate kfree() of RCU-protected trigger_data and wprobe_data on the error path.
- [Medium] Permanent memory leak of wprobe trigger objects on successful registration.
- [Medium] tw->addr is read and written concurrently by different triggers without a shared lock. --

Thanks for the review comments.
BTW, why this only to linux-perf-users? (Maybe I need to modify Sashiko setting)

Quoted 12 lines

--- Patch [9]: [PATCH v7 09/10] tracing: wprobe: Add wprobe event trigger --- The format of this report is altered due to recitation restrictions. I am providing a free-form summary of the findings rather than inline quoting the patch.

Commit: 4087513c82b11b98d27cfc4452df9a1a8e41ddd5
Author: Masami Hiramatsu (Google) <[email protected]>
Subject: tracing: wprobe: Add wprobe event trigger

This patch adds a wprobe event trigger to set and clear watch events dynamically, allowing watchpoints on local variables and slab objects.

Quoted 7 lines

[Severity: Critical]
Is it safe to call raw_spin_lock_irqsave() and schedule_work() directly from wprobe_trigger()? Tracepoints can execute in arbitrary contexts, including NMI or with scheduler locks held. Calling schedule_work() is unsafe in these contexts. Additionally, acquiring wprobe_data->lock here can cause a hard deadlock if the tracepoint interrupts wprobe_smp_update_func() while it is holding the exact same lock in IPI context.

Hmm, good catch!
OK, use a pair of irq_work and work to call on_each_cpu() for some interrupt context.
For NMI context, I think wprobe_trigger should check the original state is in_nmi(). This can work for normal trace events or fprobe, but not for kprobes, because kprobe handler itself is working inside NMI context. Hmm, let me check how can I fix this. Until solving this problem, this wprobe trigger may not work with kprobes.
Also, I found the lock should protect the wprobe itself, not data. Let me move the irq_work and lock to trace_wprobe.

Hmm, we may need to pass to original context (calculated by preempt_count) to trigger event so that it can evaluate the original context. (BTW, )

Quoted 6 lines

[Severity: High]
Does free_wprobe_trigger_data() safely handle the embedded work struct? When a trigger is unregistered, this function frees wprobe_data via kfree(). If wprobe_trigger() recently scheduled wprobe_data->work, the workqueue could execute wprobe_work_func() on the freed memory since there is no cancel_work_sync() before freeing.

OK, so move the worker to wprobe because anyway the worker will update the wprobe itself.

Quoted 7 lines

[Severity: High]
Is an RCU grace period needed in wprobe_trigger_free()? wprobe_unregister_trigger() unlinks the trigger and calls wprobe_trigger_free(), which synchronously frees wprobe_data. Since tracepoints execute under RCU, could a concurrent tracepoint execution still be accessing wprobe_data when it gets freed?

Indeed, let's use call_rcu().

Quoted 8 lines

[Severity: High]
Can the error path in wprobe_trigger_cmd_parse() cause a use-after-free? If event_trigger_register() successfully exposes the trigger to RCU readers but a subsequent step fails, returning an error causes the __free(kfree) cleanup to immediately free trigger_data and wprobe_data. This bypasses any RCU grace period for objects that were just briefly exposed to concurrent tracepoint readers.

Ah, good catch. OK, let me call synchronize_rcu() before returning.

Quoted 7 lines

[Severity: High]
Is it intentional that on_each_cpu() is called with wait=false in wprobe_work_func()? Because it doesn't wait, the work function completes immediately while IPIs are still pending on other CPUs. If the trigger is unregistered and freed right after, wprobe_smp_update_func() will access freed wprobe_data.

Ah, indeed. I will use a combination of irq_worker and worker and use on_each_cpu() with wait=true for safety.

Quoted 7 lines

[Severity: High]
Does wprobe_trigger_cmd_parse() acquire the target event's module reference? The code calls trace_event_enable_disable() directly without first calling trace_event_try_get_ref(). However, wprobe_trigger_free() unconditionally calls trace_event_put_ref(), which could lead to a module refcount underflow.

OK, let me fix it.

Quoted 8 lines

[Severity: Medium]
Is there a memory leak on the success path of wprobe_trigger_cmd_parse()? The function increments the refcount via event_trigger_init(), and then event_trigger_register() increments it again. On success, trigger_data is set to NULL to bypass the cleanup macro, but the initial refcount is never dropped. When the trigger is later unregistered, the refcount only drops to 1, permanently leaking the objects.

OK, call trace_event_put_ref directly in wprobe_trigger_cmd_parse().

Quoted 8 lines

[Severity: Medium]
How is concurrent access to tw->addr synchronized between different triggers? If a user attaches multiple triggers (e.g., set_wprobe and clear_wprobe) targeting the same wprobe event, they allocate separate wprobe_data instances with independent locks. When wprobe_trigger() executes, they will read and write the shared tw->addr simultaneously without a common lock, potentially corrupting the hardware breakpoint state.

I decided to use a lock in trace_wprobe side and use READ_ONCE/WRITE_ONCE() which may be enough.

Thank you,

Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>