Skip to contentSkip to conversation

tracing: wprobe: x86: Add wprobe for watchpoint

PATCH Superseded Masami Hiramatsu (Google) 2 participants 36 messages Aug 30 – Sep 6

Subsystems (7)

v14 of 19 Superseded Latest is v19 →

lore
Share & tools
lore

Masami Hiramatsu (Google)

author maintainer
#0 lore raw

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

Hi,

Here is the 14th 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 dynamically allocated objects too.
The previous version is here:

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

This version includes independent bug fixes for x86 user-mode MCE debug register migration and BTF anonymous struct kflag resolution (these 2 patches should be merged as bugfix), enforces valid kernel addresses (< TASK_SIZE check), allows '-1' dummy trigger addresses, fixes trigger count unregistration and error path leaks, and resolves race conditions in selftests. See below for more details.

Changes in v14:
- x86/mce: Add standalone fix for hardware debug register corruption on task migration during user MCE.
- tracing/probes: Add standalone fix for BTF member offset resolution to check the kflag of the actual containing structure/union type.
- x86/hw_breakpoints: Return early in local_db_restore() when saved DR7 is 0 to avoid overwriting guest/cleared hardware DR7 on NMI.
- tracing/wprobe: Disallow address 0 and user-space addresses (< TASK_SIZE) in parse_address_spec().
- tracing/wprobe: Format symbol and offset explicitly in trace_wprobe_show() instead of %pS to ensure clean event restoration.
- tracing/wprobe: Accept '-1' as a dummy address for trigger-only wprobes and display as '-1' on show.
- tracing/wprobe: Skip field parsing in clear_wprobe if parameter is a count keyword to allow unregistering with '!clear_wprobe:...:count=N'.
- tracing/wprobe: Set target address before soft-enabling events in set_wprobe trigger and roll back on failure.
- tracing/wprobe: Fix trigger_data/wprobe_data memory and refcount leaks on trigger registration error paths.
- selftests/ftrace: Add BTF argument dependency requirement in trigger-wprobe.tc.
- selftests/ftrace: Add delay before clear_trace in trigger-wprobe.tc to prevent race with clear_wprobe execution.
- selftests/ftrace: Update dummy wprobe event definitions across trigger tests to use '-1' instead of '0'.
- selftests/tracing: Fix error log comment to BAD_PROBE_ADDR and add testcase for address 0 rejection.

Public branch
-------------
I will push this branch as topic/wprobe-v2 to my tree so that it can be easily tested.

https://git.kernel.org/pub/scm/linux/kernel/git/mhiramat/linux.git/log/?h=topic/wprobe-v2

This is based on linux-trace tree's linux-trace/for-next.

Usage
-----

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

w:[GRP/][EVENT] [r|w|rw]@<ADDRESS|SYMBOL[+|-OFFS]>[:LEN] [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 access on slab objects. The trigger syntax is;

set_wprobe:WPROBE_EVENT:FIELD[+|-OFFSET][:COUNT] [if FILTER] clear_wprobe:WPROBE_EVENT[:FIELD[+|-OFFSET][:COUNT]] [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@-1: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

Thank you,

---
base-commit: 3df3ae98c30c249c4bbf3d74559cc6972898052c

Jinchao Wang (2):
x86/hw_breakpoints: Make DR7 updates NMI safe x86/hw_breakpoints: Add arch_modify_local_hw_breakpoint_addr() API

Masami Hiramatsu (Google) (12):
x86/mce: Fix hardware debug register corruption on task migration tracing/probes: Fix BTF kflag check for anonymous struct member access
kprobes: Protect kprobe_blacklist with RCU
HWBP: Add modify_local_hw_breakpoint_addr() API tracing/wprobe: Add wprobe (watchpoint probe) trace event support
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: Add set_wprobe and clear_wprobe event triggers
selftests: ftrace: Add wprobe trigger testcase tracing/wprobe: Support BTF typecast in fetchargs tracing/wprobe: Support BTF struct offset resolution in set_wprobe trigger

 Documentation/trace/index.rst                      |    1 
 Documentation/trace/wprobetrace.rst                |  185 +++
 arch/Kconfig                                       |   18 
 arch/x86/Kconfig                                   |    2 
 arch/x86/include/asm/debugreg.h                    |   37 -
 arch/x86/include/asm/hw_breakpoint.h               |    2 
 arch/x86/kernel/cpu/mce/core.c                     |   25 
 arch/x86/kernel/hw_breakpoint.c                    |  139 +-
 arch/x86/kernel/nmi.c                              |    7 
 arch/x86/kernel/traps.c                            |   10 
 include/linux/hw_breakpoint.h                      |    6 
 include/linux/kprobes.h                            |    1 
 include/linux/trace_events.h                       |    3 
 kernel/events/hw_breakpoint.c                      |   48 +
 kernel/kprobes.c                                   |   14 
 kernel/trace/Kconfig                               |   24 
 kernel/trace/Makefile                              |    1 
 kernel/trace/trace.c                               |    9 
 kernel/trace/trace.h                               |    6 
 kernel/trace/trace_btf.c                           |   11 
 kernel/trace/trace_btf.h                           |    3 
 kernel/trace/trace_events_trigger.c                |    2 
 kernel/trace/trace_probe.c                         |   51 +
 kernel/trace/trace_probe.h                         |   22 
 kernel/trace/trace_wprobe.c                        | 1475 ++++++++++++++++++++
 tools/testing/selftests/ftrace/config              |    3 
 .../ftrace/test.d/dynevent/add_remove_wprobe.tc    |   63 +
 .../test.d/dynevent/wprobes_syntax_errors.tc       |   23 
 .../test.d/trigger/trigger-wprobe-btf-offset.tc    |   74 +
 .../test.d/trigger/trigger-wprobe-btf-typecast.tc  |   72 +
 .../test.d/trigger/trigger-wprobe-syntax-errors.tc |   37 +
 .../ftrace/test.d/trigger/trigger-wprobe.tc        |   90 +
 32 files changed, 2364 insertions(+), 100 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-btf-offset.tc
 create mode 100644 tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-typecast.tc
 create mode 100644 tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-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 v14 1/14 x86/mce: Fix hardware debug register corruption on task migration

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

In exc_machine_check_user(), local_db_save() and local_db_restore() are invoked in the outer entry stubs (DEFINE_IDTENTRY_MCE_USER, DEFINE_FREDENTRY_MCE, and DEFINE_IDTENTRY_RAW), surrounding exc_machine_check_user().

However, exc_machine_check_user() calls irqentry_exit_to_user_mode(), which handles pending thread work and may schedule() if TIF_NEED_RESCHED is set. If the task migrates to another CPU during schedule(), local_db_restore() runs on the new CPU with the dr7 state saved from the old CPU. This corrupts the new CPU's DR7 hardware debug register and leaves the old CPU's DR7 disabled.
In short, local_db_save() and local_db_restore() pair must be run on the same CPU.

To fix this, move local_db_save() and local_db_restore() inside exc_machine_check_user() and exc_machine_check_kernel() so that DR7 is saved and restored strictly around do_machine_check().

Fixes cd840e424f27 ("x86/entry, mce: Disallow #DB during #MC")
Assisted-by Antigravity:gemini-3.7-flash

---
Changes in v14:
- Newly added.

---
 arch/x86/kernel/cpu/mce/core.c |   23 +++++++----------------
 1 file changed, 7 insertions(+), 16 deletions(-)
arch/x86/kernel/cpu/mce/core.c +7 -16
index cfb74be19994..61ade8b5c9d7 100644
@@ -2108,6 +2108,7 @@ bool filter_mce(struct mce *m)
 static __always_inline void exc_machine_check_kernel(struct pt_regs *regs)
 {
 	irqentry_state_t irq_state;
+	unsigned long dr7;
 
 	WARN_ON_ONCE(user_mode(regs));
 
@@ -2118,18 +2119,24 @@ static __always_inline void exc_machine_check_kernel(struct pt_regs *regs)
 	if (mca_cfg.initialized && mce_check_crashing_cpu())
 		return;
 
+	dr7 = local_db_save();
 	irq_state = irqentry_nmi_enter(regs);
 
 	do_machine_check(regs);
 
 	irqentry_nmi_exit(regs, irq_state);
+	local_db_restore(dr7);
 }
 
 static __always_inline void exc_machine_check_user(struct pt_regs *regs)
 {
+	unsigned long dr7;
+
 	irqentry_enter_from_user_mode(regs);
 
+	dr7 = local_db_save();
 	do_machine_check(regs);
+	local_db_restore(dr7);
 
 	irqentry_exit_to_user_mode(regs);
 }
@@ -2138,21 +2145,13 @@ static __always_inline void exc_machine_check_user(struct pt_regs *regs)
 /* MCE hit kernel mode */
 DEFINE_IDTENTRY_MCE(exc_machine_check)
 {
-	unsigned long dr7;
-
-	dr7 = local_db_save();
 	exc_machine_check_kernel(regs);
-	local_db_restore(dr7);
 }
 
 /* The user mode variant. */
 DEFINE_IDTENTRY_MCE_USER(exc_machine_check)
 {
-	unsigned long dr7;
-
-	dr7 = local_db_save();
 	exc_machine_check_user(regs);
-	local_db_restore(dr7);
 }
 
 #ifdef CONFIG_X86_FRED
@@ -2169,28 +2168,20 @@ DEFINE_IDTENTRY_MCE_USER(exc_machine_check)
  */
 DEFINE_FREDENTRY_MCE(exc_machine_check)
 {
-	unsigned long dr7;
-
-	dr7 = local_db_save();
 	if (user_mode(regs))
 		exc_machine_check_user(regs);
 	else
 		exc_machine_check_kernel(regs);
-	local_db_restore(dr7);
 }
 #endif
 #else
 /* 32bit unified entry point */
 DEFINE_IDTENTRY_RAW(exc_machine_check)
 {
-	unsigned long dr7;
-
-	dr7 = local_db_save();
 	if (user_mode(regs))
 		exc_machine_check_user(regs);
 	else
 		exc_machine_check_kernel(regs);
-	local_db_restore(dr7);
 }
 #endif
 
#2 lore raw

Re: [PATCH v14 01/14] x86/mce: Fix hardware debug register corruption on task migration

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] Moving local_db_save() inside exc_machine_check_kernel() exposes early #MC entry code to hardware…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Moving local_db_save() inside exc_machine_check_kernel() exposes early #MC entry code to hardware breakpoints, partially reverting commit cd840e424f27 and allowing nested #DB exceptions that can fatally corrupt the #DB IST stack. --

commit 22e84ef34bc3d02a04e2e3356bffad4d11749a7a
Author: Masami Hiramatsu (Google) <[email protected]>

x86/mce: Fix hardware debug register corruption on task migration

In exc_machine_check_user(), local_db_save() and local_db_restore() are invoked in the outer entry stubs, surrounding exc_machine_check_user(). However, exc_machine_check_user() handles pending thread work and may schedule(), causing the task to migrate to another CPU. This patch moves local_db_save() and local_db_restore() inside exc_machine_check_user() and exc_machine_check_kernel() to prevent hardware debug register corruption.

Quoted 13 lines
arch/x86/kernel/cpu/mce/core.c +1 -0
index cfb74be19994d..61ade8b5c9d7b 100644
@@ -2108,6 +2108,7 @@ bool filter_mce(struct mce *m)
 static __always_inline void exc_machine_check_kernel(struct pt_regs *regs)
 {
 	irqentry_state_t irq_state;
+	unsigned long dr7;
 
 	WARN_ON_ONCE(user_mode(regs));
 

arch/x86/kernel/cpu/mce/core.c static __always_inline void exc_machine_check_kernel(struct pt_regs *regs) · line 2122

Quoted 4 lines
arch/x86/kernel/cpu/mce/core.c +1 -0
@@ -2118,18 +2119,24 @@ static __always_inline void exc_machine_check_kernel(struct pt_regs *regs)
 	if (mca_cfg.initialized && mce_check_crashing_cpu())
 		return;
 
+	dr7 = local_db_save();

[Severity: High]
Does moving local_db_save() below the early checks in exc_machine_check_kernel() expose this early #MC entry code to hardware breakpoints?

If a privileged user sets a hardware data breakpoint on a kernel variable accessed here, such as mca_cfg.initialized or variables inside mce_check_crashing_cpu(), a #DB exception could fire in kernel mode.

If a Machine Check Exception (#MC) then occurs and preempts the #DB handler, it switches to the #MC IST stack. Because local_db_save() is not called until after these variable accesses, the code could trigger a nested #DB exception.

Since x86 uses a fixed-size Interrupt Stack Table (IST) for #DB exceptions that unconditionally resets the stack pointer to the top of the #DB IST stack, wouldn't the nested #DB overwrite the active stack frame of the interrupted #DB handler, leading to a fatal crash when it attempts to return?

Could local_db_save() be called at the very beginning of exc_machine_check_kernel() to prevent this IST stack corruption?

Quoted 3 lines
arch/x86/kernel/cpu/mce/core.c +0 -0
 	irq_state = irqentry_nmi_enter(regs);
 
 	do_machine_check(regs);

[ ... ]

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

Re: [PATCH v14 01/14] x86/mce: Fix hardware debug register corruption on task migration

Quoted 18 lines · On Sun, 30 Aug 2026 14:40:05 +0000 [email protected] wrote:

arch/x86/kernel/cpu/mce/core.c static __always_inline void exc_machine_check_kernel(struct pt_regs *regs) · line 2122

Quoted 12 lines

static __always_inline void exc_machine_check_kernel(struct pt_regs *regs) {

arch/x86/kernel/cpu/mce/core.c +2 -0
 	irqentry_state_t irq_state;
+	unsigned long dr7;
 
 	WARN_ON_ONCE(user_mode(regs));
@@ -2118,18 +2119,24 @@ static __always_inline void exc_machine_check_kernel(struct pt_regs *regs)
 	if (mca_cfg.initialized && mce_check_crashing_cpu())
 		return;
 
+	dr7 = local_db_save();

[Severity: High]
Does moving local_db_save() below the early checks in exc_machine_check_kernel() expose this early #MC entry code to hardware breakpoints?

Quoted 3 lines

If a privileged user sets a hardware data breakpoint on a kernel variable accessed here, such as mca_cfg.initialized or variables inside mce_check_crashing_cpu(), a #DB exception could fire in kernel mode.

Ah, good catch! This can happen if we introduce wprobe.

Quoted 6 lines

If a Machine Check Exception (#MC) then occurs and preempts the #DB handler, it switches to the #MC IST stack. Because local_db_save() is not called until after these variable accesses, the code could trigger a nested #DB exception.

Quoted 8 lines

Since x86 uses a fixed-size Interrupt Stack Table (IST) for #DB exceptions that unconditionally resets the stack pointer to the top of the #DB IST stack, wouldn't the nested #DB overwrite the active stack frame of the interrupted #DB handler, leading to a fatal crash when it attempts to return?

Could local_db_save() be called at the very beginning of exc_machine_check_kernel() to prevent this IST stack corruption?

Yes, I will fix it.

Thanks!

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

PATCH v14 2/14 tracing/probes: Fix BTF kflag check for anonymous struct member access

In reply to Masami Hiramatsu (Google)

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

btf_find_struct_member() traverses into nested anonymous structures and unions to find a struct member. However, get_bitoffset_of_field() in trace_probe.c checked btf_type_kflag(type) using the outer parent type instead of the actual anonymous structure/union that directly contains the found member.

If the parent structure and anonymous structure have mismatched kflags (e.g., the parent has kflag=0 while the anonymous structure has kflag=1 because it contains bitfields), the bitfield size encoded in the upper 8 bits of member->offset is erroneously treated as part of the byte/bit offset, corrupting the resolved offset and failing to set last_bitsize. Similarly, btf_find_struct_member() pushed anonymous member offsets onto anon_stack without masking BTF_MEMBER_BIT_OFFSET() when kflag is set.

To fix this problem, update btf_find_struct_member() to return actual containing structure/union type via member_type, and use it for btf_type_kflag() in get_bitoffset_of_field().

Fixes c440adfbe302 ("tracing/probes: Support BTF based data structure field access")
Assisted-by Antigravity:gemini-3.7-flash

---
Changes in v14:
- Newly added.

---
 kernel/trace/trace_btf.c   |   11 +++++++++--
 kernel/trace/trace_btf.h   |    3 ++-
 kernel/trace/trace_probe.c |   15 ++++++++-------
 3 files changed, 19 insertions(+), 10 deletions(-)
kernel/trace/trace_btf.c +9 -2
index 00172f301f25..7d93be2872ff 100644
@@ -70,7 +70,8 @@ struct btf_anon_stack {
 const struct btf_member *btf_find_struct_member(struct btf *btf,
 						const struct btf_type *type,
 						const char *member_name,
-						u32 *anon_offset)
+						u32 *anon_offset,
+						const struct btf_type **member_type)
 {
 	struct btf_anon_stack *anon_stack;
 	const struct btf_member *member;
@@ -91,17 +92,23 @@ const struct btf_member *btf_find_struct_member(struct btf *btf,
 	for_each_member(i, type, member) {
 		if (!member->name_off) {
 			/* Anonymous union/struct: push it for later use */
+			u32 m_off = btf_type_kflag(type) ?
+				BTF_MEMBER_BIT_OFFSET(member->offset) :
+				member->offset;
+
 			if (btf_type_skip_modifiers(btf, member->type, &tid) &&
 			    top < BTF_ANON_STACK_MAX) {
 				anon_stack[top].tid = tid;
 				anon_stack[top++].offset =
-					cur_offset + member->offset;
+					cur_offset + m_off;
 			}
 		} else {
 			name = btf_name_by_offset(btf, member->name_off);
 			if (name && !strcmp(member_name, name)) {
 				if (anon_offset)
 					*anon_offset = cur_offset;
+				if (member_type)
+					*member_type = type;
 				goto out;
 			}
 		}
kernel/trace/trace_btf.h +2 -1
index 4bc44bc261e6..4bd26bceae23 100644
@@ -8,4 +8,5 @@ const struct btf_param *btf_get_func_param(const struct btf_type *func_proto,
 const struct btf_member *btf_find_struct_member(struct btf *btf,
 						const struct btf_type *type,
 						const char *member_name,
-						u32 *anon_offset);
+						u32 *anon_offset,
+						const struct btf_type **member_type);
kernel/trace/trace_probe.c +8 -7
index c4163904ba74..908b4b6bc2df 100644
@@ -625,6 +625,7 @@ static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **pty
 {
 	const struct btf_type *type = *ptype;
 	const struct btf_member *field;
+	const struct btf_type *mtype;
 	struct btf *btf = ctx_btf(ctx);
 	char *fieldname = *pfieldname;
 	int bitoffs = 0;
@@ -640,7 +641,7 @@ static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **pty
 
 		anon_offs = 0;
 		field = btf_find_struct_member(btf, type, fieldname,
-						&anon_offs);
+						&anon_offs, &mtype);
 		if (IS_ERR(field)) {
 			trace_probe_log_err(ctx->offset, BAD_BTF_TID);
 			return PTR_ERR(field);
@@ -653,7 +654,7 @@ static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **pty
 		bitoffs += anon_offs;
 
 		/* Accumulate the bit-offsets of the dot-connected fields */
-		if (btf_type_kflag(type)) {
+		if (btf_type_kflag(mtype)) {
 			bitoffs += BTF_MEMBER_BIT_OFFSET(field->offset);
 			ctx->last_bitsize = BTF_MEMBER_BITFIELD_SIZE(field->offset);
 		} else {
@@ -661,11 +662,11 @@ static int get_bitoffset_of_field(char **pfieldname, const struct btf_type **pty
 			ctx->last_bitsize = 0;
 		}
 
-			type = btf_type_skip_modifiers(btf, field->type, NULL);
-			if (!type) {
-				trace_probe_log_err(ctx->offset, BAD_BTF_TID);
-				return -EINVAL;
-			}
+		type = btf_type_skip_modifiers(btf, field->type, NULL);
+		if (!type) {
+			trace_probe_log_err(ctx->offset, BAD_BTF_TID);
+			return -EINVAL;
+		}
 
 		if (next)
 			ctx->offset += next - fieldname;
#5 lore raw

Re: [PATCH v14 02/14] tracing/probes: Fix BTF kflag check for anonymous struct member access

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Unnamed bitfields break nested anonymous struct resolution in btf_find_struct_member(). --

commit 20a606aa6d4e155be5a531c5d52d78c34205c0b4
Author: Masami Hiramatsu (Google) <[email protected]>

tracing/probes: Fix BTF kflag check for anonymous struct member access

This commit fixes how BTF kflags are checked when traversing nested anonymous structures and unions in btf_find_struct_member() by using the actual containing type.

Quoted 3 lines
kernel/trace/trace_btf.c +0 -0

[ ... ]

Quoted 6 lines
kernel/trace/trace_btf.c +2 -0
@@ -91,17 +92,23 @@ const struct btf_member *btf_find_struct_member(struct btf *btf,
 	for_each_member(i, type, member) {
 		if (!member->name_off) {
 			/* Anonymous union/struct: push it for later use */
+			u32 m_off = btf_type_kflag(type) ?
+				BTF_MEMBER_BIT_OFFSET(member->offset) :

kernel/trace/trace_btf.c const struct btf_member *btf_find_struct_member(struct btf *btf, · line 105

Quoted 10 lines
kernel/trace/trace_btf.c +3 -1
+				member->offset;
+
 			if (btf_type_skip_modifiers(btf, member->type, &tid) &&
 			    top < BTF_ANON_STACK_MAX) {
 				anon_stack[top].tid = tid;
 				anon_stack[top++].offset =
-					cur_offset + member->offset;
+					cur_offset + m_off;
 			}
 		} else {

[Severity: High]
This isn't a bug introduced by this patch, but is it possible for unnamed bitfields to break nested anonymous struct resolution here?

Since unnamed bitfields (like int : 5) also have name_off == 0, they get pushed onto anon_stack alongside actual anonymous structs.

Because anon_stack is processed in LIFO order, an unnamed bitfield defined after an anonymous struct will be popped first. When it is popped at the top of btf_find_struct_member():

retry:

	if (!btf_type_is_struct(type)) {
		member = ERR_PTR(-EINVAL);
		goto out;
	}

Does this cause the scalar type check to fail and abort the entire search, preventing the earlier anonymous struct from ever being searched?

Quoted 4 lines
kernel/trace/trace_btf.c +0 -0
 			name = btf_name_by_offset(btf, member->name_off);
 			if (name && !strcmp(member_name, name)) {
 				if (anon_offset)
 					*anon_offset = cur_offset;
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=2
author maintainer
#6 lore raw

Re: [PATCH v14 02/14] tracing/probes: Fix BTF kflag check for anonymous struct member access

Quoted 33 lines · On Sun, 30 Aug 2026 14:38:59 +0000 [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [High] Unnamed bitfields break nested anonymous struct resolution in btf_find_struct_member(). --

commit 20a606aa6d4e155be5a531c5d52d78c34205c0b4
Author: Masami Hiramatsu (Google) <[email protected]>

tracing/probes: Fix BTF kflag check for anonymous struct member access

This commit fixes how BTF kflags are checked when traversing nested anonymous structures and unions in btf_find_struct_member() by using the actual containing type.

Quoted 3 lines
kernel/trace/trace_btf.c +0 -0

[ ... ]

Quoted 12 lines
kernel/trace/trace_btf.c +4 -0
@@ -91,17 +92,23 @@ const struct btf_member *btf_find_struct_member(struct btf *btf,
 	for_each_member(i, type, member) {
 		if (!member->name_off) {
 			/* Anonymous union/struct: push it for later use */
+			u32 m_off = btf_type_kflag(type) ?
+				BTF_MEMBER_BIT_OFFSET(member->offset) :
+				member->offset;
+
 			if (btf_type_skip_modifiers(btf, member->type, &tid) &&
 			    top < BTF_ANON_STACK_MAX) {
 				anon_stack[top].tid = tid;
 				anon_stack[top++].offset =
Quoted 9 lines

kernel/trace/trace_btf.c const struct btf_member *btf_find_struct_member(struct btf *btf, · line 105

Quoted 4 lines
kernel/trace/trace_btf.c +1 -1
-					cur_offset + member->offset;
+					cur_offset + m_off;
 			}
 		} else {

[Severity: High]
This isn't a bug introduced by this patch, but is it possible for unnamed bitfields to break nested anonymous struct resolution here?

Oops, unnamed bitfield.... I didn't expected that.

Quoted 2 lines

Since unnamed bitfields (like int : 5) also have name_off == 0, they get pushed onto anon_stack alongside actual anonymous structs.

OK, I got it. This function only expects anonymous struct/union, not anonymous bitfield.

Quoted 5 lines

Because anon_stack is processed in LIFO order, an unnamed bitfield defined after an anonymous struct will be popped first. When it is popped at the top of btf_find_struct_member():

Quoted 8 lines

retry:

	if (!btf_type_is_struct(type)) {
		member = ERR_PTR(-EINVAL);
		goto out;
	}

Does this cause the scalar type check to fail and abort the entire search, preventing the earlier anonymous struct from ever being searched?

Yeah, this is wrong. it should check this is the last anon_stack entry and retry. Maybe we should make it a loop.

Thanks!

Quoted 8 lines
Quoted 5 lines
kernel/trace/trace_btf.c +0 -0
 			name = btf_name_by_offset(btf, member->name_off);
 			if (name && !strcmp(member_name, name)) {
 				if (anon_offset)
 					*anon_offset = cur_offset;
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=2
Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
author maintainer
#7 lore raw

PATCH v14 3/14 kprobes: Protect kprobe_blacklist with RCU

In reply to Masami Hiramatsu (Google)

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

__within_kprobe_blacklist() traverses kprobe_blacklist without holding kprobe_mutex. When a module is unloaded, kprobe_remove_area_blacklist() removes blacklist entries and immediately frees them with kfree(). A concurrent call to within_kprobe_blacklist() can therefore dereference freed memory.

Furthermore, within_kprobe_blacklist() can be called in atomic or non-preemptible contexts where the sleeping kprobe_mutex cannot be taken.

Protect kprobe_blacklist with RCU. Use guard(rcu)() and list_for_each_entry_rcu() for traversal, list_add_tail_rcu() for insertions, list_del_rcu() for deletions, and kfree_rcu() to reclaim entries safely after a grace period.

Assisted-by Antigravity:gemini-3.7-flash

---
Changes in v13:
- Newly added.

---
 include/linux/kprobes.h |    1 +
 kernel/kprobes.c        |   14 ++++++++++----
 2 files changed, 11 insertions(+), 4 deletions(-)
include/linux/kprobes.h +1 -0
index 8c4f3bb24429..e6de7ae55bda 100644
@@ -181,6 +181,7 @@ struct kprobe_blacklist_entry {
 	struct list_head list;
 	unsigned long start_addr;
 	unsigned long end_addr;
+	struct rcu_head rcu;
 };
 
 #ifdef CONFIG_KPROBES
kernel/kprobes.c +10 -4
index bfc89083daa9..6337da5cab9e 100644
@@ -1447,8 +1447,14 @@ static bool __within_kprobe_blacklist(unsigned long addr)
 	/*
 	 * If 'kprobe_blacklist' is defined, check the address and
 	 * reject any probe registration in the prohibited area.
+	 * Note: this can return true during transition period where
+	 * (start_addr, end_addr) in the black list is shrinking
+	 * but old entry has not been removed yet. This is acceptable
+	 * because the worst case is that we reject more probes than
+	 * we should.
 	 */
-	list_for_each_entry(ent, &kprobe_blacklist, list) {
+	guard(rcu)();
+	list_for_each_entry_rcu(ent, &kprobe_blacklist, list) {
 		if (addr >= ent->start_addr && addr < ent->end_addr)
 			return true;
 	}
@@ -2509,7 +2515,7 @@ int kprobe_add_ksym_blacklist(unsigned long entry)
 	ent->start_addr = entry;
 	ent->end_addr = entry + size;
 	INIT_LIST_HEAD(&ent->list);
-	list_add_tail(&ent->list, &kprobe_blacklist);
+	list_add_tail_rcu(&ent->list, &kprobe_blacklist);
 
 	return (int)size;
 }
@@ -2603,8 +2609,8 @@ static void kprobe_remove_area_blacklist(unsigned long start, unsigned long end)
 	list_for_each_entry_safe(ent, n, &kprobe_blacklist, list) {
 		if (ent->start_addr < start || ent->start_addr >= end)
 			continue;
-		list_del(&ent->list);
-		kfree(ent);
+		list_del_rcu(&ent->list);
+		kfree_rcu(ent, rcu);
 	}
 }
 
#8 lore raw

Re: [PATCH v14 03/14] kprobes: Protect kprobe_blacklist with RCU

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 11 lines

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

__within_kprobe_blacklist() traverses kprobe_blacklist without holding kprobe_mutex. When a module is unloaded, kprobe_remove_area_blacklist() removes blacklist entries and immediately frees them with kfree(). A concurrent call to within_kprobe_blacklist() can therefore dereference freed memory.

Furthermore, within_kprobe_blacklist() can be called in atomic or non-preemptible contexts where the sleeping kprobe_mutex cannot be taken.

Quoted 7 lines

Protect kprobe_blacklist with RCU. Use guard(rcu)() and list_for_each_entry_rcu() for traversal, list_add_tail_rcu() for insertions, list_del_rcu() for deletions, and kfree_rcu() to reclaim entries safely after a grace period.

Assisted-by Antigravity:gemini-3.7-flash

Sashiko has reviewed this patch and found no issues. It looks great!

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

Re: [PATCH v14 03/14] kprobes: Protect kprobe_blacklist with RCU

In reply to Masami Hiramatsu (Google)
Quoted 8 lines · On Sun, 30 Aug 2026 23:27:23 +0900 "Masami Hiramatsu (Google)"…

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

__within_kprobe_blacklist() traverses kprobe_blacklist without holding kprobe_mutex. When a module is unloaded, kprobe_remove_area_blacklist() removes blacklist entries and immediately frees them with kfree(). A concurrent call to within_kprobe_blacklist() can therefore dereference freed memory.

Quoted 7 lines

Furthermore, within_kprobe_blacklist() can be called in atomic or non-preemptible contexts where the sleeping kprobe_mutex cannot be taken.

Protect kprobe_blacklist with RCU. Use guard(rcu)() and list_for_each_entry_rcu() for traversal, list_add_tail_rcu() for insertions, list_del_rcu() for deletions, and kfree_rcu() to reclaim entries safely after a grace period.

I realized this is required even without wprobe. So let me take this as a stable fix with following tags.

Fixes 376e242429bf ("kprobes: Introduce NOKPROBE_SYMBOL() macro to maintain kprobes blacklist")

Thanks,

Quoted 64 lines
Assisted-by Antigravity:gemini-3.7-flash

---
Changes in v13:
- Newly added.

---
 include/linux/kprobes.h |    1 +
 kernel/kprobes.c        |   14 ++++++++++----
 2 files changed, 11 insertions(+), 4 deletions(-)
include/linux/kprobes.h +1 -0
index 8c4f3bb24429..e6de7ae55bda 100644
@@ -181,6 +181,7 @@ struct kprobe_blacklist_entry {
 	struct list_head list;
 	unsigned long start_addr;
 	unsigned long end_addr;
+	struct rcu_head rcu;
 };
 
 #ifdef CONFIG_KPROBES
kernel/kprobes.c +10 -4
index bfc89083daa9..6337da5cab9e 100644
@@ -1447,8 +1447,14 @@ static bool __within_kprobe_blacklist(unsigned long addr)
 	/*
 	 * If 'kprobe_blacklist' is defined, check the address and
 	 * reject any probe registration in the prohibited area.
+	 * Note: this can return true during transition period where
+	 * (start_addr, end_addr) in the black list is shrinking
+	 * but old entry has not been removed yet. This is acceptable
+	 * because the worst case is that we reject more probes than
+	 * we should.
 	 */
-	list_for_each_entry(ent, &kprobe_blacklist, list) {
+	guard(rcu)();
+	list_for_each_entry_rcu(ent, &kprobe_blacklist, list) {
 		if (addr >= ent->start_addr && addr < ent->end_addr)
 			return true;
 	}
@@ -2509,7 +2515,7 @@ int kprobe_add_ksym_blacklist(unsigned long entry)
 	ent->start_addr = entry;
 	ent->end_addr = entry + size;
 	INIT_LIST_HEAD(&ent->list);
-	list_add_tail(&ent->list, &kprobe_blacklist);
+	list_add_tail_rcu(&ent->list, &kprobe_blacklist);
 
 	return (int)size;
 }
@@ -2603,8 +2609,8 @@ static void kprobe_remove_area_blacklist(unsigned long start, unsigned long end)
 	list_for_each_entry_safe(ent, n, &kprobe_blacklist, list) {
 		if (ent->start_addr < start || ent->start_addr >= end)
 			continue;
-		list_del(&ent->list);
-		kfree(ent);
+		list_del_rcu(&ent->list);
+		kfree_rcu(ent, rcu);
 	}
 }
 
Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
author maintainer
#10 lore raw

PATCH v14 4/14 x86/hw_breakpoints: Make DR7 updates NMI safe

In reply to Masami Hiramatsu (Google)

From: Jinchao Wang <[email protected]>

Hardware breakpoint installation and removal run with IRQs disabled, but an NMI can still enter the same code through KGDB. The interrupted operation and the NMI can consequently claim the same slot or overwrite each other's DR7 state.

Claim and release per-CPU slots with cmpxchg. Update cpu_dr7 with single-instruction per-CPU operations, and preserve hardware-first disable and hardware-last enable ordering. Add a per-CPU sequence number so interrupted DR7 writers and restore paths detect an NMI update and retry from the latest shadow state.

---
Changes in v14:
- In local_db_restore(), return immediately if saved dr7 is 0 to avoid overwriting intentionally cleared hardware DR7 state (e.g. KVM guest entry) with cpu_dr7 when an NMI increments cpu_dr7_seq.
Changes in v13:
- Add compiler barriers (barrier()) before checking cpu_dr7_seq to prevent the compiler from hoisting the sequence check before debug register updates or shadow state modifications.
Changes in v12:
- Use stack variables dr7 and dr7_seq directly in exc_nmi() and remove unnecessary per-CPU nmi_dr7 and nmi_dr7_seq variables.

---
 arch/x86/include/asm/debugreg.h |   37 ++++++++++----
 arch/x86/kernel/cpu/mce/core.c  |   10 ++--
 arch/x86/kernel/hw_breakpoint.c |  101 ++++++++++++++++++++-------------------
 arch/x86/kernel/nmi.c           |    7 ++-
 arch/x86/kernel/traps.c         |   10 +++-
 5 files changed, 95 insertions(+), 70 deletions(-)
arch/x86/include/asm/debugreg.h +27 -10
index a2c1f2d24b64..65e80cd62193 100644
@@ -18,6 +18,7 @@
 #define DR7_FIXED_1	0x00000400
 
 DECLARE_PER_CPU(unsigned long, cpu_dr7);
+DECLARE_PER_CPU(unsigned int, cpu_dr7_seq);
 
 #ifndef CONFIG_PARAVIRT_XXL
 /*
@@ -125,18 +126,20 @@ static __always_inline bool hw_breakpoint_active(void)
 
 extern void hw_breakpoint_restore(void);
 
-static __always_inline unsigned long local_db_save(void)
+static __always_inline void local_db_save(unsigned long *dr7,
+					  unsigned int *dr7_seq)
 {
-	unsigned long dr7;
+	*dr7 = 0;
+	*dr7_seq = this_cpu_read(cpu_dr7_seq);
 
 	if (static_cpu_has(X86_FEATURE_HYPERVISOR) && !hw_breakpoint_active())
-		return 0;
+		return;
 
-	get_debugreg(dr7, 7);
+	get_debugreg(*dr7, 7);
 
 	/* Architecturally set bit */
-	dr7 &= ~DR7_FIXED_1;
-	if (dr7)
+	*dr7 &= ~DR7_FIXED_1;
+	if (*dr7)
 		set_debugreg(DR7_FIXED_1, 7);
 
 	/*
@@ -145,20 +148,34 @@ static __always_inline unsigned long local_db_save(void)
 	 * be good.
 	 */
 	barrier();
-
-	return dr7;
 }
 
-static __always_inline void local_db_restore(unsigned long dr7)
+static __always_inline void local_db_restore(unsigned long dr7,
+					     unsigned int dr7_seq)
 {
+	unsigned int seq;
+
 	/*
 	 * Ensure the compiler doesn't raise this statement into
 	 * the critical section; enabling breakpoints early would
 	 * not be good.
 	 */
 	barrier();
-	if (dr7)
+
+	if (!dr7)
+		return;
+
+	do {
+		seq = this_cpu_read(cpu_dr7_seq);
+		if (seq != dr7_seq) {
+			dr7 = this_cpu_read(cpu_dr7);
+			if (!dr7)
+				dr7 = DR7_FIXED_1;
+		}
+
 		set_debugreg(dr7, 7);
+		barrier();
+	} while (unlikely(seq != this_cpu_read(cpu_dr7_seq)));
 }
 
 #ifdef CONFIG_CPU_SUP_AMD
arch/x86/kernel/cpu/mce/core.c +6 -4
index 61ade8b5c9d7..d45e8b780deb 100644
@@ -2109,6 +2109,7 @@ static __always_inline void exc_machine_check_kernel(struct pt_regs *regs)
 {
 	irqentry_state_t irq_state;
 	unsigned long dr7;
+	unsigned int dr7_seq;
 
 	WARN_ON_ONCE(user_mode(regs));
 
@@ -2119,24 +2120,25 @@ static __always_inline void exc_machine_check_kernel(struct pt_regs *regs)
 	if (mca_cfg.initialized && mce_check_crashing_cpu())
 		return;
 
-	dr7 = local_db_save();
+	local_db_save(&dr7, &dr7_seq);
 	irq_state = irqentry_nmi_enter(regs);
 
 	do_machine_check(regs);
 
 	irqentry_nmi_exit(regs, irq_state);
-	local_db_restore(dr7);
+	local_db_restore(dr7, dr7_seq);
 }
 
 static __always_inline void exc_machine_check_user(struct pt_regs *regs)
 {
 	unsigned long dr7;
+	unsigned int dr7_seq;
 
 	irqentry_enter_from_user_mode(regs);
 
-	dr7 = local_db_save();
+	local_db_save(&dr7, &dr7_seq);
 	do_machine_check(regs);
-	local_db_restore(dr7);
+	local_db_restore(dr7, dr7_seq);
 
 	irqentry_exit_to_user_mode(regs);
 }
arch/x86/kernel/hw_breakpoint.c +51 -50

156 lines not on this page · raw message

arch/x86/kernel/nmi.c +4 -3
index 3c9f60d6ca5a..c3806f7bd3e8 100644
@@ -531,11 +531,12 @@ enum nmi_states {
 };
 static DEFINE_PER_CPU(enum nmi_states, nmi_state);
 static DEFINE_PER_CPU(unsigned long, nmi_cr2);
-static DEFINE_PER_CPU(unsigned long, nmi_dr7);
 
 DEFINE_IDTENTRY_RAW(exc_nmi)
 {
 	irqentry_state_t irq_state;
+	unsigned long dr7;
+	unsigned int dr7_seq;
 	struct nmi_stats *nsp = this_cpu_ptr(&nmi_stats);
 
 	/*
@@ -572,7 +573,7 @@ DEFINE_IDTENTRY_RAW(exc_nmi)
 	 */
 	sev_es_ist_enter(regs);
 
-	this_cpu_write(nmi_dr7, local_db_save());
+	local_db_save(&dr7, &dr7_seq);
 
 	irq_state = irqentry_nmi_enter(regs);
 
@@ -594,7 +595,7 @@ DEFINE_IDTENTRY_RAW(exc_nmi)
 
 	irqentry_nmi_exit(regs, irq_state);
 
-	local_db_restore(this_cpu_read(nmi_dr7));
+	local_db_restore(dr7, dr7_seq);
 
 	sev_es_ist_exit();
 
arch/x86/kernel/traps.c +7 -3
index 30aa8369957e..018abe736285 100644
@@ -1231,8 +1231,12 @@ static noinstr void exc_debug_kernel(struct pt_regs *regs, unsigned long dr6)
 	 * it results in an endless recursion and stack overflow. Thus we stay
 	 * with the IDT approach, i.e., save DR7 and disable #DB.
 	 */
-	unsigned long dr7 = local_db_save();
-	irqentry_state_t irq_state = irqentry_nmi_enter(regs);
+	unsigned long dr7;
+	unsigned int dr7_seq;
+	irqentry_state_t irq_state;
+
+	local_db_save(&dr7, &dr7_seq);
+	irq_state = irqentry_nmi_enter(regs);
 	instrumentation_begin();
 
 	/*
@@ -1289,7 +1293,7 @@ static noinstr void exc_debug_kernel(struct pt_regs *regs, unsigned long dr6)
 	instrumentation_end();
 	irqentry_nmi_exit(regs, irq_state);
 
-	local_db_restore(dr7);
+	local_db_restore(dr7, dr7_seq);
 }
 
 static noinstr void exc_debug_user(struct pt_regs *regs, unsigned long dr6)
#11 lore raw

Re: [PATCH v14 04/14] x86/hw_breakpoints: Make DR7 updates NMI safe

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Pre-existing issues:
- [Critical] Preexisting race condition in local_db_save() leads to recursive exceptions. The function reads DR7, then checks if it was non-zero before disabling hardware breakpoints. If an NMI (e.g., KGDB) installs a breakpoint after DR7 is read but before the check, local_db_save() will falsely believe no breakpoints are active and skip disabling them. The exception handler (e.g., #DB or #MC) will then run with hardware breakpoints enabled, risking a recursive exception.
- [High] The patch adds if (!dr7) return; in local_db_restore() to ostensibly prevent an NMI from overwriting an intentionally cleared hardware DR7 state (such as during KVM guest entry). However, if an NMI (e.g., from KGDB) installs a hardware breakpoint, arch_install_hw_breakpoint() directly overwrites the hardware DR7 register with cpu_dr7. By returning immediately when dr7 == 0, local_db_restore() fails to restore the hardware DR7 back to its cleared state (0x400). Consequently, KVM resumes the guest with the host's newly installed hardware breakpoint active. --

commit e145a86b764e63c7282dc5c5fb7c745db147953a
Author: Jinchao Wang <[email protected]>

x86/hw_breakpoints: Make DR7 updates NMI safe

Hardware breakpoint installation and removal run with IRQs disabled, but an NMI can still enter the same code through KGDB. This patch uses cmpxchg and a per-CPU sequence number to make DR7 updates safe from NMI interruptions.

Quoted 4 lines
arch/x86/include/asm/debugreg.h +0 -0
index a2c1f2d24b645..65e80cd62193f 100644

[ ... ]

Quoted 15 lines
arch/x86/include/asm/debugreg.h +5 -3
@@ -125,18 +126,20 @@ static __always_inline bool hw_breakpoint_active(void)
 
 extern void hw_breakpoint_restore(void);
 
-static __always_inline unsigned long local_db_save(void)
+static __always_inline void local_db_save(unsigned long *dr7,
+					  unsigned int *dr7_seq)
 {
-	unsigned long dr7;
+	*dr7 = 0;
+	*dr7_seq = this_cpu_read(cpu_dr7_seq);
 
 	if (static_cpu_has(X86_FEATURE_HYPERVISOR) && !hw_breakpoint_active())
-		return 0;
+		return;

arch/x86/include/asm/debugreg.h static __always_inline bool hw_breakpoint_active(void) · line 143

Quoted 10 lines
arch/x86/include/asm/debugreg.h +3 -3
 
-	get_debugreg(dr7, 7);
+	get_debugreg(*dr7, 7);
 
 	/* Architecturally set bit */
-	dr7 &= ~DR7_FIXED_1;
-	if (dr7)
+	*dr7 &= ~DR7_FIXED_1;
+	if (*dr7)
 		set_debugreg(DR7_FIXED_1, 7);

[Severity: Critical]
This is a pre-existing issue, but does this conditional check rely on a stale read of DR7?

If an NMI (e.g. KGDB) installs a breakpoint after DR7 is read via get_debugreg() but before this check, local_db_save() will evaluate the old 0 and skip disabling DR7. Can this cause the exception handler (such as #DB or #MC) to run with hardware breakpoints still enabled, risking a recursive exception?

Quoted 16 lines
arch/x86/include/asm/debugreg.h +4 -3
 	/*
@@ -145,20 +148,34 @@ static __always_inline unsigned long local_db_save(void)
 	 * be good.
 	 */
 	barrier();
-
-	return dr7;
 }
 
-static __always_inline void local_db_restore(unsigned long dr7)
+static __always_inline void local_db_restore(unsigned long dr7,
+					     unsigned int dr7_seq)
 {
+	unsigned int seq;
+

arch/x86/include/asm/debugreg.h static __always_inline unsigned long local_db_save(void) · line 166

Quoted 10 lines
arch/x86/include/asm/debugreg.h +3 -1
 	/*
 	 * Ensure the compiler doesn't raise this statement into
 	 * the critical section; enabling breakpoints early would
 	 * not be good.
 	 */
 	barrier();
-	if (dr7)
+
+	if (!dr7)
+		return;

[Severity: High]
This isn't a bug introduced by this patch, but if an NMI interrupts a cleared DR7 state (such as during KVM guest entry), and the NMI installs a hardware breakpoint, doesn't this early return fail to restore the hardware DR7 back to its cleared state (0x400)?

Consequently, it looks like KVM would resume the guest with the host's newly installed hardware breakpoint active.

[ ... ]

Quoted 4 lines
arch/x86/kernel/hw_breakpoint.c +0 -0
index f846c15f21ca1..3204e75dc4546 100644

[ ... ]

Quoted 38 lines
arch/x86/kernel/hw_breakpoint.c +2 -21
@@ -97,38 +100,31 @@ int decode_dr7(unsigned long dr7, int bpnum, unsigned *len, unsigned *type)
 int arch_install_hw_breakpoint(struct perf_event *bp)
 {
 	struct arch_hw_breakpoint *info = counter_arch_bp(bp);
-	unsigned long *dr7;
+	unsigned int seq;
 	int i;
 
 	lockdep_assert_irqs_disabled();
 
 	for (i = 0; i < HBP_NUM; i++) {
-		struct perf_event **slot = this_cpu_ptr(&bp_per_reg[i]);
-
-		if (!*slot) {
-			*slot = bp;
+		if (!this_cpu_cmpxchg(bp_per_reg[i], NULL, bp))
 			break;
-		}
 	}
 
 	if (WARN_ONCE(i == HBP_NUM, "Can't find any breakpoint slot"))
 		return -EBUSY;
 
-	set_debugreg(info->address, i);
-	__this_cpu_write(cpu_debugreg[i], info->address);
-
-	dr7 = this_cpu_ptr(&cpu_dr7);
-	*dr7 |= encode_dr7(i, info->len, info->type);
-
-	/*
-	 * Ensure we first write cpu_dr7 before we set the DR7 register.
-	 * This ensures an NMI never see cpu_dr7 0 when DR7 is not.
-	 */
-	barrier();
-
-	set_debugreg(*dr7, 7);
-	if (info->mask)
-		amd_set_dr_addr_mask(info->mask, i);

arch/x86/kernel/hw_breakpoint.c int decode_dr7(unsigned long dr7, int bpnum, unsigned *len, unsigned *type) · line 125

Quoted 10 lines
arch/x86/kernel/hw_breakpoint.c +10 -0
+	do {
+		seq = this_cpu_inc_return(cpu_dr7_seq);
+		this_cpu_write(cpu_debugreg[i], info->address);
+		barrier();
+		set_debugreg(info->address, i);
+		if (info->mask)
+			amd_set_dr_addr_mask(info->mask, i);
+		this_cpu_or(cpu_dr7, encode_dr7(i, info->len, info->type));
+		barrier();
+		set_debugreg(this_cpu_read(cpu_dr7), 7);

[Severity: High]
This is a pre-existing issue, but since arch_install_hw_breakpoint() unconditionally overwrites the hardware DR7 register inside the NMI, does this mean the KVM cleared state is already lost here before local_db_restore() is even called?

Quoted 5 lines
arch/x86/kernel/hw_breakpoint.c +2 -0
+		barrier();
+	} while (seq != this_cpu_read(cpu_dr7_seq));
 
 	return 0;
 }
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=4
author maintainer
#12 lore raw

Re: [PATCH v14 04/14] x86/hw_breakpoints: Make DR7 updates NMI safe

Quoted 55 lines · On Sun, 30 Aug 2026 14:45:19 +0000 [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Pre-existing issues:
- [Critical] Preexisting race condition in local_db_save() leads to recursive exceptions. The function reads DR7, then checks if it was non-zero before disabling hardware breakpoints. If an NMI (e.g., KGDB) installs a breakpoint after DR7 is read but before the check, local_db_save() will falsely believe no breakpoints are active and skip disabling them. The exception handler (e.g., #DB or #MC) will then run with hardware breakpoints enabled, risking a recursive exception.
- [High] The patch adds if (!dr7) return; in local_db_restore() to ostensibly prevent an NMI from overwriting an intentionally cleared hardware DR7 state (such as during KVM guest entry). However, if an NMI (e.g., from KGDB) installs a hardware breakpoint, arch_install_hw_breakpoint() directly overwrites the hardware DR7 register with cpu_dr7. By returning immediately when dr7 == 0, local_db_restore() fails to restore the hardware DR7 back to its cleared state (0x400). Consequently, KVM resumes the guest with the host's newly installed hardware breakpoint active. --

commit e145a86b764e63c7282dc5c5fb7c745db147953a
Author: Jinchao Wang <[email protected]>

x86/hw_breakpoints: Make DR7 updates NMI safe

Hardware breakpoint installation and removal run with IRQs disabled, but an NMI can still enter the same code through KGDB. This patch uses cmpxchg and a per-CPU sequence number to make DR7 updates safe from NMI interruptions.

Quoted 4 lines
arch/x86/include/asm/debugreg.h +0 -0
index a2c1f2d24b645..65e80cd62193f 100644

[ ... ]

Quoted 15 lines
arch/x86/include/asm/debugreg.h +5 -3
@@ -125,18 +126,20 @@ static __always_inline bool hw_breakpoint_active(void)
 
 extern void hw_breakpoint_restore(void);
 
-static __always_inline unsigned long local_db_save(void)
+static __always_inline void local_db_save(unsigned long *dr7,
+					  unsigned int *dr7_seq)
 {
-	unsigned long dr7;
+	*dr7 = 0;
+	*dr7_seq = this_cpu_read(cpu_dr7_seq);
 
 	if (static_cpu_has(X86_FEATURE_HYPERVISOR) && !hw_breakpoint_active())
-		return 0;
+		return;

arch/x86/include/asm/debugreg.h static __always_inline bool hw_breakpoint_active(void) · line 143

Quoted 10 lines
arch/x86/include/asm/debugreg.h +3 -3
 
-	get_debugreg(dr7, 7);
+	get_debugreg(*dr7, 7);
 
 	/* Architecturally set bit */
-	dr7 &= ~DR7_FIXED_1;
-	if (dr7)
+	*dr7 &= ~DR7_FIXED_1;
+	if (*dr7)
 		set_debugreg(DR7_FIXED_1, 7);

[Severity: Critical]
This is a pre-existing issue, but does this conditional check rely on a stale read of DR7?

Quoted 5 lines

If an NMI (e.g. KGDB) installs a breakpoint after DR7 is read via get_debugreg() but before this check, local_db_save() will evaluate the old 0 and skip disabling DR7. Can this cause the exception handler (such as #DB or #MC) to run with hardware breakpoints still enabled, risking a recursive exception?

Ah, good catch. It also need to check cpu_dr7_seq is not bumped too.

Quoted 25 lines
Quoted 25 lines
arch/x86/include/asm/debugreg.h +5 -4
 	/*
@@ -145,20 +148,34 @@ static __always_inline unsigned long local_db_save(void)
 	 * be good.
 	 */
 	barrier();
-
-	return dr7;
 }
 
-static __always_inline void local_db_restore(unsigned long dr7)
+static __always_inline void local_db_restore(unsigned long dr7,
+					     unsigned int dr7_seq)
 {
+	unsigned int seq;
+
 	/*
 	 * Ensure the compiler doesn't raise this statement into
 	 * the critical section; enabling breakpoints early would
 	 * not be good.
 	 */
 	barrier();
-	if (dr7)
+
Quoted 8 lines

arch/x86/include/asm/debugreg.h static __always_inline unsigned long local_db_save(void) · line 166

Quoted 2 lines
arch/x86/include/asm/debugreg.h +2 -0
+	if (!dr7)
+		return;

[Severity: High]
This isn't a bug introduced by this patch, but if an NMI interrupts a cleared DR7 state (such as during KVM guest entry), and the NMI installs a hardware breakpoint, doesn't this early return fail to restore the hardware DR7 back to its cleared state (0x400)?

Hmm, this was intended to avoid restoring cpu_dr7 over KVM's cleared state. But if the NMI already overwrote hardware DR7 here, returning early leaves the host breakpoint active when entering the guest.

OK, instead of an unconditional early return on "!dr7", local_db_restore() should check if cpu_dr7_seq changed too...

Let me fix it.

Quoted 59 lines

Consequently, it looks like KVM would resume the guest with the host's newly installed hardware breakpoint active.

[ ... ]

Quoted 4 lines
arch/x86/kernel/hw_breakpoint.c +0 -0
index f846c15f21ca1..3204e75dc4546 100644

[ ... ]

Quoted 46 lines
arch/x86/kernel/hw_breakpoint.c +10 -21
@@ -97,38 +100,31 @@ int decode_dr7(unsigned long dr7, int bpnum, unsigned *len, unsigned *type)
 int arch_install_hw_breakpoint(struct perf_event *bp)
 {
 	struct arch_hw_breakpoint *info = counter_arch_bp(bp);
-	unsigned long *dr7;
+	unsigned int seq;
 	int i;
 
 	lockdep_assert_irqs_disabled();
 
 	for (i = 0; i < HBP_NUM; i++) {
-		struct perf_event **slot = this_cpu_ptr(&bp_per_reg[i]);
-
-		if (!*slot) {
-			*slot = bp;
+		if (!this_cpu_cmpxchg(bp_per_reg[i], NULL, bp))
 			break;
-		}
 	}
 
 	if (WARN_ONCE(i == HBP_NUM, "Can't find any breakpoint slot"))
 		return -EBUSY;
 
-	set_debugreg(info->address, i);
-	__this_cpu_write(cpu_debugreg[i], info->address);
-
-	dr7 = this_cpu_ptr(&cpu_dr7);
-	*dr7 |= encode_dr7(i, info->len, info->type);
-
-	/*
-	 * Ensure we first write cpu_dr7 before we set the DR7 register.
-	 * This ensures an NMI never see cpu_dr7 0 when DR7 is not.
-	 */
-	barrier();
-
-	set_debugreg(*dr7, 7);
-	if (info->mask)
-		amd_set_dr_addr_mask(info->mask, i);
+	do {
+		seq = this_cpu_inc_return(cpu_dr7_seq);
+		this_cpu_write(cpu_debugreg[i], info->address);
+		barrier();
+		set_debugreg(info->address, i);
+		if (info->mask)
+			amd_set_dr_addr_mask(info->mask, i);
+		this_cpu_or(cpu_dr7, encode_dr7(i, info->len, info->type));
Quoted 8 lines

arch/x86/kernel/hw_breakpoint.c int decode_dr7(unsigned long dr7, int bpnum, unsigned *len, unsigned *type) · line 125

Quoted 2 lines
arch/x86/kernel/hw_breakpoint.c +2 -0
+		barrier();
+		set_debugreg(this_cpu_read(cpu_dr7), 7);

[Severity: High]
This is a pre-existing issue, but since arch_install_hw_breakpoint() unconditionally overwrites the hardware DR7 register inside the NMI, does this mean the KVM cleared state is already lost here before local_db_restore() is even called?

Yes, so checking the cpu_dr7_seq will avoid this issue too.

Thanks,

Quoted 9 lines
Quoted 6 lines
arch/x86/kernel/hw_breakpoint.c +2 -0
+		barrier();
+	} while (seq != this_cpu_read(cpu_dr7_seq));
 
 	return 0;
 }
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=4
Signature · 1 line
Masami Hiramatsu (Google) <[email protected]>
author maintainer
#13 lore raw

PATCH v14 5/14 x86/hw_breakpoints: Add arch_modify_local_hw_breakpoint_addr() API

In reply to Masami Hiramatsu (Google)

From: Jinchao Wang <[email protected]>

Wprobe needs to move an active per-CPU watchpoint without releasing and reserving its hardware slot.

Add arch_modify_local_hw_breakpoint_addr() as the architecture backend for modify_local_hw_breakpoint_addr(). It validates the new address, finds the installed local slot, and updates the hardware debug register and per-CPU shadow register. The core layer (modify_local_hw_breakpoint_addr()) handles updating bp->attr.bp_addr and counter_arch_bp(bp)->address.

The caller must provide an installed local event and a valid address. Slot ownership, breakpoint type, length, mask and DR7 remain unchanged.

---
Changes in v13:
- Add compiler barrier (barrier()) before checking cpu_dr7_seq to prevent hoisting the sequence check before set_debugreg().
- Clarify in commit log and code comment that the core layer manages updating bp->attr.bp_addr and counter_arch_bp(bp)->address.
Changes in v11:
- Return int error code instead of void.
- Validate the new address using hw_breakpoint_arch_parse() before updating registers.
- Increment cpu_dr7_seq in a sequence loop for NMI protection.
- Return -ENOENT if the breakpoint slot is not found on the local CPU.

---
 arch/x86/include/asm/hw_breakpoint.h |    2 ++
 arch/x86/kernel/hw_breakpoint.c      |   44 ++++++++++++++++++++++++++++++++++
 2 files changed, 46 insertions(+)
arch/x86/include/asm/hw_breakpoint.h +2 -0
index 0bc931cd0698..f35ec491f6dd 100644
@@ -59,6 +59,8 @@ extern int hw_breakpoint_exceptions_notify(struct notifier_block *unused,
 
 
 int arch_install_hw_breakpoint(struct perf_event *bp);
+int arch_modify_local_hw_breakpoint_addr(struct perf_event *bp,
+					 unsigned long addr);
 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 +44 -0

50 lines not on this page · raw message

#14 lore raw

Re: [PATCH v14 05/14] x86/hw_breakpoints: Add arch_modify_local_hw_breakpoint_addr() API

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [High] Modifying a debug address register without first disabling the corresponding breakpoint in DR7…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] Modifying a debug address register without first disabling the corresponding breakpoint in DR7 violates the x86 hardware architecture specification and causes unpredictable behavior. --

commit 35b07811f48c348c13ac1d9de7fa44d25fc7f36a
Author: Jinchao Wang <[email protected]>

x86/hw_breakpoints: Add arch_modify_local_hw_breakpoint_addr() API

This commit adds an architecture backend to modify the address of an active per-CPU watchpoint without releasing its hardware slot. It validates the new address, finds the installed local slot, and updates the hardware debug register and per-CPU shadow state.

Quoted 4 lines
arch/x86/kernel/hw_breakpoint.c +0 -0
index 3204e75dc4546..2227918b0163c 100644

[ ... ]

Quoted 3 lines
arch/x86/kernel/hw_breakpoint.c +3 -0
+int arch_modify_local_hw_breakpoint_addr(struct perf_event *bp,
+					 unsigned long addr)
+{

[ ... ]

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

Quoted 5 lines
arch/x86/kernel/hw_breakpoint.c +5 -0
+	do {
+		seq = this_cpu_inc_return(cpu_dr7_seq);
+		this_cpu_write(cpu_debugreg[i], addr);
+		barrier();
+		set_debugreg(addr, i);

[Severity: High]
Does this code risk violating the x86 hardware architecture specification?

Looking at arch_modify_local_hw_breakpoint_addr() in arch/x86/kernel/hw_breakpoint.c, set_debugreg() directly updates the debug address register (DR0-DR3) while its corresponding enable bit in DR7 might still be set.

According to hardware specs, modifying a debug address register without first disabling the breakpoint in DR7 can lead to spurious debug exceptions or unpredictable processor behavior if an NMI occurs or if execution boundaries trigger a match on the transient state.

Should the DR7 enable bit be cleared before updating the debug register?

Quoted 5 lines
arch/x86/kernel/hw_breakpoint.c +5 -0
+		barrier();
+	} while (seq != this_cpu_read(cpu_dr7_seq));
+
+	return 0;
+}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=5
author maintainer
#15 lore raw

Re: [PATCH v14 05/14] x86/hw_breakpoints: Add arch_modify_local_hw_breakpoint_addr() API

Quoted 20 lines · On Sun, 30 Aug 2026 14:38:48 +0000 [email protected] wrote:
Quoted 3 lines
arch/x86/kernel/hw_breakpoint.c +3 -0
+int arch_modify_local_hw_breakpoint_addr(struct perf_event *bp,
+					 unsigned long addr)
+{

[ ... ]

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

Quoted 5 lines
arch/x86/kernel/hw_breakpoint.c +5 -0
+	do {
+		seq = this_cpu_inc_return(cpu_dr7_seq);
+		this_cpu_write(cpu_debugreg[i], addr);
+		barrier();
+		set_debugreg(addr, i);

[Severity: High]
Does this code risk violating the x86 hardware architecture specification?

Looking at arch_modify_local_hw_breakpoint_addr() in arch/x86/kernel/hw_breakpoint.c, set_debugreg() directly updates the debug address register (DR0-DR3) while its corresponding enable bit in DR7 might still be set.

Quoted 6 lines

According to hardware specs, modifying a debug address register without first disabling the breakpoint in DR7 can lead to spurious debug exceptions or unpredictable processor behavior if an NMI occurs or if execution boundaries trigger a match on the transient state.

Should the DR7 enable bit be cleared before updating the debug register?

For the wprobe, the handler will check the spurious exception, but it is better to make it safer.

I will update arch_modify_local_hw_breakpoint_addr() to temporarily clear the slot's enable bit in DR7 before writing the new address, and re-enable it afterward within the cpu_dr7_seq loop.

Thank you!

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

PATCH v14 6/14 HWBP: Add modify_local_hw_breakpoint_addr() API

In reply to Masami Hiramatsu (Google)

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

Add modify_local_hw_breakpoint_addr() to update only the watched address of an installed hardware breakpoint on the local CPU without releasing and reserving its hardware slot. This is available when the architecture selects CONFIG_HAVE_MODIFY_LOCAL_HW_BREAKPOINT_ADDR.

The caller must provide an installed local event and a valid address, and update other CPUs separately.

---
Changes in v13:
- Add comment to modify_local_hw_breakpoint_addr() clarifying the early update behavior of bp->attr.bp_addr during the NMI race window.
Changes in v12:
- Update bp->attr.bp_addr and counter_arch_bp(bp)->address before calling arch_modify_local_hw_breakpoint_addr() to prevent race conditions during interrupt handling.
- Restore counter_arch_bp(bp)->address and bp->attr.bp_addr if arch_modify_local_hw_breakpoint_addr() fails.
Changes in v11:
- Check return value of arch_modify_local_hw_breakpoint_addr() and return error on failure.
- Update bp->attr.bp_addr and counter_arch_bp(bp)->address only after arch modification succeeds.

---
 arch/Kconfig                  |    8 +++++++
 arch/x86/Kconfig              |    1 +
 include/linux/hw_breakpoint.h |    6 +++++
 kernel/events/hw_breakpoint.c |   48 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 63 insertions(+)
arch/Kconfig +8 -0
index fa7507ac8e13..bea383408e32 100644
@@ -457,6 +457,14 @@ config HAVE_MIXED_BREAKPOINTS_REGS
 	  Select this option if your arch implements breakpoints under the
 	  latter fashion.
 
+config HAVE_MODIFY_LOCAL_HW_BREAKPOINT_ADDR
+	bool
+	depends on HAVE_HW_BREAKPOINT
+	help
+	  Select this if the architecture can modify the address of an
+	  installed hardware breakpoint on the local CPU without releasing
+	  and reserving its hardware slot.
+
 config HAVE_USER_RETURN_NOTIFIER
 	bool
 
arch/x86/Kconfig +1 -0
index fd979e0be1f0..33908103d9a0 100644
@@ -247,6 +247,7 @@ config X86
 	select HAVE_FUNCTION_TRACER
 	select HAVE_GCC_PLUGINS
 	select HAVE_HW_BREAKPOINT
+	select HAVE_MODIFY_LOCAL_HW_BREAKPOINT_ADDR
 	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..bf65c7fffd99 100644
@@ -81,6 +81,9 @@ register_wide_hw_breakpoint(struct perf_event_attr *attr,
 			    perf_overflow_handler_t triggered,
 			    void *context);
 
+int modify_local_hw_breakpoint_addr(struct perf_event *bp,
+				    unsigned long addr);
+
 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_local_hw_breakpoint_addr(struct perf_event *bp,
+				unsigned long addr) { 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 +48 -0

54 lines not on this page · raw message

#17 lore raw

Re: [PATCH v14 06/14] HWBP: Add modify_local_hw_breakpoint_addr() API

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Low] Lockless updates to `bp->attr.bp_addr` and `counter_arch_bp(bp)->address` lack `WRITE_ONCE()`…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Lockless updates to bp->attr.bp_addr and counter_arch_bp(bp)->address lack WRITE_ONCE(), constituting a formal data race under the Linux Kernel Memory Model. --

commit 0c79300d09e43eac52a027f6cb480808e7f3902f
Author: Masami Hiramatsu (Google) <[email protected]>

HWBP: Add modify_local_hw_breakpoint_addr() API

This commit adds the modify_local_hw_breakpoint_addr() API to update the watched address of an installed hardware breakpoint on the local CPU without releasing and reserving its hardware slot.

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

[ ... ]

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

Quoted 11 lines
kernel/events/hw_breakpoint.c +11 -0
+int modify_local_hw_breakpoint_addr(struct perf_event *bp,
+				    unsigned long addr)
+{
+	unsigned long old_addr;
+	int ret;
+
+	lockdep_assert_irqs_disabled();
+
+	old_addr = bp->attr.bp_addr;
+	counter_arch_bp(bp)->address = addr;
+	bp->attr.bp_addr = addr;

[Severity: Low]
The function intentionally updates bp->attr.bp_addr locklessly so NMI handlers can observe the new address during the transition. However, without WRITE_ONCE() annotations, does this constitute a formal data race under the Linux Kernel Memory Model?

While a torn store on 32-bit architectures might only overwrite the upper 32 bits with zero (leaving the address functionally intact), could this still trigger KCSAN warnings when an NMI handler concurrently reads bp->attr.bp_addr via perf_bp_event()?

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

Quoted 5 lines
kernel/events/hw_breakpoint.c +5 -0
+
+	ret = arch_modify_local_hw_breakpoint_addr(bp, addr);
+	if (ret) {
+		counter_arch_bp(bp)->address = old_addr;
+		bp->attr.bp_addr = old_addr;

[Severity: Low]
Similarly, should the rollback path also use WRITE_ONCE() to ensure concurrent accesses by NMI handlers avoid data race warnings from KCSAN?

Quoted 5 lines
kernel/events/hw_breakpoint.c +5 -0
+		return ret;
+	}
+
+	return 0;
+}
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=6
author maintainer
#18 lore raw

Re: [PATCH v14 06/14] HWBP: Add modify_local_hw_breakpoint_addr() API

Quoted 2 lines · On Sun, 30 Aug 2026 14:40:56 +0000 [email protected] wrote:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Lockless updates to bp->attr.bp_addr and counter_arch_bp(bp)->address lack WRITE_ONCE(), constituting a formal data race under the Linux Kernel Memory Model.

Ah, good catch! Let me add WRITE_ONCE() there.

Thank you,

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

PATCH v14 7/14 tracing/wprobe: Add wprobe (watchpoint probe) trace event support

In reply to Masami Hiramatsu (Google)

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

Add hardware-breakpoint-based dynamic trace event support (wprobe). Wprobe creates a dynamic event on data read/write accesses using hardware breakpoints and logs the access context and fetchargs.

---
Changes in v14:
- In parse_address_spec(), reject address 0 and user-space addresses by changing the check to '_addr < TASK_SIZE'.
- In trace_wprobe_show(), format symbol and offset explicitly instead of using '%pS' to ensure dynamic event restoration works properly.
- Add offset field to struct trace_wprobe and pass it to alloc_trace_wprobe().
Changes in v13:
- Fix documentation notation to SYMBOL[+|-OFFS] as same as others.
- Fix documentation indents and add default value.
- Fix README file to add [<args>].
- Add a note about module unloading.
Changes in v12:
- Fix syntax comment typo ('b' to 'w') and remove dead 'ret = 0' initialization in __trace_wprobe_create().
- Fix documentation notation to SYMBOL[[+|-]OFFS].
Changes in v11:
- Add trace_wprobe_is_busy() to prevent releasing busy events.
Changes in v9:
- Use kzalloc_flex for alloc_trace_wprobe.

---
 Documentation/trace/index.rst       |    1 
 Documentation/trace/wprobetrace.rst |   78 ++++
 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          |   21 +
 kernel/trace/trace_probe.h          |   10 
 kernel/trace/trace_wprobe.c         |  767 +++++++++++++++++++++++++++++++++++
 10 files changed, 903 insertions(+), 4 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 +78 -0

78 lines not on this page · raw message

include/linux/trace_events.h +2 -0
index 5cbd09c8be8d..43ffd9a76d88 100644
@@ -337,6 +337,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,
 };
@@ -367,6 +368,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 3e0907aef172..0e7063b2380e 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>] [<args>]\n"
+#endif
 #ifdef CONFIG_HIST_TRIGGERS
 	"\t           s:[synthetic/]<event> <field> [<field>]\n"
 #endif
kernel/trace/trace.h +5 -0
index 74a7a50d1e78..18588c550a98 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 +19 -2
index 908b4b6bc2df..9c7ebe326b76 100644
@@ -1403,6 +1403,23 @@ 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;
+		}
+		goto inval;
+	}
+
 	if (strcmp(arg, "comm") == 0 || strcmp(arg, "COMM") == 0) {
 		code->op = FETCH_OP_COMM;
 		return 0;
@@ -1462,8 +1479,8 @@ static int parse_probe_arg_register(char *arg, struct fetch_insn *code,
 {
 	int ret;
 
-	if (ctx->flags & (TPARG_FL_TEVENT | TPARG_FL_FPROBE)) {
-		/* eprobe and fprobe do not handle registers */
+	if (ctx->flags & (TPARG_FL_TEVENT | TPARG_FL_FPROBE | TPARG_FL_WPROBE)) {
+		/* eprobe, fprobe and wprobe do not handle registers */
 		trace_probe_log_err(ctx->offset, BAD_VAR);
 		return -EINVAL;
 	}
kernel/trace/trace_probe.h +9 -1
index fba1af092a9b..6543d4c2cda5 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 */\
@@ -418,6 +419,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)
@@ -544,6 +546,10 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call,
 	C(ARG_TOO_LONG,		"Argument expression is too long"),		\
 	C(ARRAY_NO_CLOSE,	"Array is not closed"),		\
 	C(ARRAY_TOO_BIG,	"Array number is too big"),		\
+	C(BAD_ACCESS_ADDR,	"Invalid access memory address"),		\
+	C(BAD_ACCESS_FMT,	"Access memory address requires @"),		\
+	C(BAD_ACCESS_LEN,	"This memory access length is not supported"),	\
+	C(BAD_ACCESS_TYPE,	"Bad memory access type"),			\
 	C(BAD_ADDR_SUFFIX,	"Invalid probed address suffix"),		\
 	C(BAD_ARG_NAME,		"Argument name must follow the same rules as C identifiers"),	\
 	C(BAD_ARG_NUM,		"Invalid argument number"),		\
@@ -626,7 +632,9 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call,
 	C(TYPECAST_NOT_EVENT,	"Typecasts are only for eprobe fields"),	\
 	C(TYPECAST_REQ_FIELD,	"Typecast requires a field access"),	\
 	C(TYPECAST_SYM_OFFSET,	"@SYM+/-OFFSET with typecast needs parentheses"),	\
-	C(USED_ARG_NAME,	"This argument name is already used"),
+	C(USED_ARG_NAME,	"This argument name is already used"),		\
+	C(WPROBE_NO_MAXACT,	"Watchpoint probe does not support maxactive"),	\
+	C(WPROBE_NO_SIBLING,	"Watchpoint probe does not support sibling probes"),
 
 #undef C
 #define C(a, b)		TP_ERR_##a
kernel/trace/trace_wprobe.c new file +767 -0

767 lines not on this page · raw message

#20 lore raw

Re: [PATCH v14 07/14] tracing/wprobe: Add wprobe (watchpoint probe) trace event support

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 9 lines

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

Add hardware-breakpoint-based dynamic trace event support (wprobe). Wprobe creates a dynamic event on data read/write accesses using hardware breakpoints and logs the access context and fetchargs.

Sashiko has reviewed this patch and found no issues. It looks great!

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

PATCH v14 8/14 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.

---
Changes in v11:
- Move select to alphabetically sorted place.

---
 arch/Kconfig         |   10 ++++++++++
 arch/x86/Kconfig     |    1 +
 kernel/trace/Kconfig |    1 +
 3 files changed, 12 insertions(+)
arch/Kconfig +10 -0
index bea383408e32..a096952987a8 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_MODIFY_LOCAL_HW_BREAKPOINT_ADDR
 	bool
 	depends on HAVE_HW_BREAKPOINT
arch/x86/Kconfig +1 -0
index 33908103d9a0..533010ab29a9 100644
@@ -287,6 +287,7 @@ config X86
 	select MMU_GATHER_RCU_TABLE_FREE
 	select MMU_GATHER_MERGE_VMAS
 	select HAVE_POSIX_CPU_TIMERS_TASK_WORK
+	select HAVE_POST_BREAKPOINT_HOOK
 	select HAVE_REGS_AND_STACK_ACCESS_API
 	select HAVE_RELIABLE_STACKTRACE		if UNWINDER_ORC || STACK_VALIDATION
 	select HAVE_FUNCTION_ARG_ACCESS_API
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
#22 lore raw

Re: [PATCH v14 08/14] x86: hw_breakpoint: Add a kconfig to clarify when a breakpoint fires

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 7 lines

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.

Sashiko has reviewed this patch and found no issues. It looks great!

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

PATCH v14 9/14 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.

---
Changes in v9:
- Fix command check logic to prevent early exit under 'set -e' (errexit) when grep or test fails.
- Simplify enable/disable status checks by removing cat pipes.
Changes in v8:
- Fixed silently test failure path.

---
 tools/testing/selftests/ftrace/config              |    1 
 .../ftrace/test.d/dynevent/add_remove_wprobe.tc    |   63 ++++++++++++++++++++
 2 files changed, 64 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 +63 -0

63 lines not on this page · raw message

#24 lore raw

Re: [PATCH v14 09/14] selftests: tracing: Add a basic testcase for wprobe

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 6 lines

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.

Sashiko has reviewed this patch and found no issues. It looks great!

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

PATCH v14 10/14 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.

---
Changes in v14:
- Update comment for invalid offset syntax check to BAD_PROBE_ADDR.
- Add testcase for invalid address 0 error check (BAD_ACCESS_ADDR).
Changes in v12:
- Add NO_ARG_BODY error case.
Changes in v11:
- Add WPROBE_NO_MAXACT syntax error case.

---
 .../test.d/dynevent/wprobes_syntax_errors.tc       |   23 ++++++++++++++++++++
 1 file changed, 23 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 +23 -0
new file mode 100644
index 000000000000..e0df58fe7aa3
@@ -0,0 +1,23 @@
+#!/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'			# NO_ARG_BODY
+check_error 'w^10 w@jiffies'		# WPROBE_NO_MAXACT
+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@^0:4'			# BAD_ACCESS_ADDR
+check_error 'w w@jiffies^+offset'	# BAD_PROBE_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
#26 lore raw

Re: [PATCH v14 10/14] selftests: tracing: Add syntax testcase for wprobe

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 6 lines

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

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

Sashiko has reviewed this patch and found no issues. It looks great!

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

PATCH v14 11/14 tracing/wprobe: Add set_wprobe and clear_wprobe event triggers

In reply to Masami Hiramatsu (Google)

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

Add set_wprobe and clear_wprobe event triggers to dynamically attach and detach hardware breakpoint address monitoring based on event field contents.

---
Changes in v14:
- Allow '-1' as a dummy address in parse_address_spec() to set initial address to WPROBE_DEFAULT_CLEAR_ADDRESS for trigger-only wprobes.
- In trace_wprobe_show(), format WPROBE_DEFAULT_CLEAR_ADDRESS as '-1'.
- Update documentation and examples to use '-1' instead of '0'.
- Skip field parsing in clear_wprobe if parameter is a count keyword.
- Set tw->addr to WPROBE_DEFAULT_CLEAR_ADDRESS before soft-enabling event and roll back on failure.
- Fix memory and refcount leak of trigger_data/wprobe_data on error path.
Changes in v13:
- Align WPROBE_DEFAULT_CLEAR_ADDRESS to 8 bytes using dedicated u64.
- Remove manual count decrement to rely on core event_trigger_count().
- Parse optional FIELD argument in clear_wprobe command.
- Fix trigger reference count leak and error cleanup in command parsing.
- Update document to add [:COUNT] to trigger syntax and fix adjust forms.
- Add notes about the exclusive trigger setting behavior.
- Remove unused dyn_event_ops_mutex.
Changes in v12:
- Decrement trigger data->count only when watchpoint state is actually changed.
- Remove dyn_event_ops_mutex to fix lockdep circular dependency deadlock.
- Add event_trigger_init() call to prevent premature freeing of trigger_data.

---
 Documentation/trace/wprobetrace.rst |  107 ++++++
 include/linux/trace_events.h        |    1 
 kernel/trace/Kconfig                |   10 +
 kernel/trace/trace.h                |    1 
 kernel/trace/trace_events_trigger.c |    2 
 kernel/trace/trace_probe.c          |    2 
 kernel/trace/trace_probe.h          |    9 -
 kernel/trace/trace_wprobe.c         |  594 +++++++++++++++++++++++++++++++++++
 8 files changed, 723 insertions(+), 3 deletions(-)
Documentation/trace/wprobetrace.rst +107 -0
index 31cc68b464e9..df8985283312 100644
@@ -76,3 +76,110 @@ Wprobe event does not disable itself even if the module is unloaded.
 For example, if you add a wprobe event on a module variable, and then
 unload the module, the wprobe event will still be enabled. This is for
 watching the address is used unexpectedly after the module is unloaded.
+
+Combination with trigger action
+-------------------------------
+The event trigger action can extend the utilization of this wprobe.
+
+- set_wprobe:WPEVENT:FIELD[+|-ADJUST][:COUNT]
+- clear_wprobe:WPEVENT[:FIELD[+|-ADJUST][:COUNT]]
+
+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.
+If COUNT is specified, it will set/clear WPEVENT only if it hits COUNT
+times.
+
+Notes:
+- set_wprobe only works on the wprobe which is NOT set a valid address yet,
+  and it must be enabled after the trigger is set.
+- clear_wprobe only works on the wprobe which is set a valid address, and it
+  will be soft-disabled after the trigger is cleared.
+- Therefore, if a trigger sets/clears a wprobe, other/same trigger events
+  will not work (on the same event) while the wprobe is set.
+
+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 invalid dummy address (-1) and keep disabled.
+
+Wprobe triggers only support target addresses in kernel memory. If a
+set_wprobe trigger evaluates to a user-space memory address or NULL
+pointer, the trigger action ignores the update and skips setting the
+watchpoint.
+
+Wprobe triggers are not supported on kprobe_events, because kprobes
+themselves can use software breakpoints which conflicts with wprobe
+operation.
+
+
+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@-1: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 43ffd9a76d88..f81a5308c116 100644
@@ -738,6 +738,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..5fd8ed63c516 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_MODIFY_LOCAL_HW_BREAKPOINT_ADDR
+	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.h +1 -0
index 18588c550a98..a61f840b18bd 100644
@@ -1983,6 +1983,7 @@ trigger_data_alloc(struct event_command *cmd_ops, char *cmd, char *param,
 		   void *private_data);
 extern void trigger_data_free(struct event_trigger_data *data);
 extern int event_trigger_init(struct event_trigger_data *data);
+extern void event_trigger_free(struct event_trigger_data *data);
 extern int trace_event_trigger_enable_disable(struct trace_event_file *file,
 					      int trigger_enable);
 extern void update_cond_flag(struct trace_event_file *file);
kernel/trace/trace_events_trigger.c +1 -1
index 149300cc5e8a..120e6ec634b7 100644
@@ -589,7 +589,7 @@ int event_trigger_init(struct event_trigger_data *data)
  * Usually used directly as the @free method in event trigger
  * implementations.
  */
-static void
+void
 event_trigger_free(struct event_trigger_data *data)
 {
 	if (WARN_ON_ONCE(data->ref <= 0))
kernel/trace/trace_probe.c +1 -1
index 9c7ebe326b76..5033fffd71c5 100644
@@ -20,7 +20,7 @@
 #undef C
 #define C(a, b)		b
 
-static const char *trace_probe_err_text[] = { ERRORS };
+const char *trace_probe_err_text[] = { ERRORS };
 
 static const char *reserved_field_names[] = {
 	"common_type",
kernel/trace/trace_probe.h +8 -1
index 6543d4c2cda5..e08f17c99138 100644
@@ -634,7 +634,12 @@ extern int traceprobe_define_arg_fields(struct trace_event_call *event_call,
 	C(TYPECAST_SYM_OFFSET,	"@SYM+/-OFFSET with typecast needs parentheses"),	\
 	C(USED_ARG_NAME,	"This argument name is already used"),		\
 	C(WPROBE_NO_MAXACT,	"Watchpoint probe does not support maxactive"),	\
-	C(WPROBE_NO_SIBLING,	"Watchpoint probe does not support sibling probes"),
+	C(WPROBE_NO_SIBLING,	"Watchpoint probe does not support sibling probes"), \
+	C(WPROBE_ON_KPROBE,	"Wprobe trigger is not supported on kprobe event"), \
+	C(WPROBE_NOT_FOUND,	"Target wprobe event is not found"),		\
+	C(WPROBE_BUSY,		"Target wprobe event is already enabled"),	\
+	C(WPROBE_NEED_FIELD,	"Wprobe trigger requires a target field"),	\
+	C(WPROBE_BAD_FIELD,	"Target field must be pointer size"),
 
 #undef C
 #define C(a, b)		TP_ERR_##a
@@ -658,6 +663,8 @@ void __trace_probe_log_err(int offset, int err);
 
 DEFINE_FREE(trace_probe_log_clear, const char *, if (_T) trace_probe_log_clear())
 
+extern const char *trace_probe_err_text[];
+
 #define trace_probe_log_err(offs, err)	\
 	__trace_probe_log_err(offs, TP_ERR_##err)
 
kernel/trace/trace_wprobe.c +594 -0

660 lines not on this page · raw message

#28 lore raw

Re: [PATCH v14 11/14] tracing/wprobe: Add set_wprobe and clear_wprobe event triggers

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider: - [Medium] Calling `cancel_work_sync()` on an uninitialized `work_struct` in the error path triggers a…

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] Calling cancel_work_sync() on an uninitialized work_struct in the error path triggers a kernel warning (WARN_ON). --

commit 5e74cd56c202daf7ad5f60c9e251b2bc86cf21d8
Author: Masami Hiramatsu (Google) <[email protected]>
Subject: tracing/wprobe: Add set_wprobe and clear_wprobe event triggers

This commit adds set_wprobe and clear_wprobe event triggers to dynamically attach and detach hardware breakpoint address monitoring based on event field contents.

Quoted 4 lines
kernel/trace/trace_wprobe.c +0 -0
index 6a11c55e75373..29eae708ae7bc 100644

[ ... ]

Quoted 1 line
kernel/trace/trace_wprobe.c +0 -0
@@ -199,14 +217,62 @@ static int __register_trace_wprobe(struct trace_wprobe *tw)

[ ... ]

kernel/trace/trace_wprobe.c static int __register_trace_wprobe(struct trace_wprobe *tw) · line 275

Quoted 5 lines

static void free_trace_wprobe(struct trace_wprobe *tw) {

kernel/trace/trace_wprobe.c +2 -0
 	if (tw) {
+		irq_work_sync(&tw->irq_work);
+		cancel_work_sync(&tw->work);

[Severity: Medium]
Will this trigger a kernel warning in __flush_work() if the work struct has not been initialized yet?

In alloc_trace_wprobe(), if the kstrdup() allocation for the symbol fails, the function returns early. Since the tw pointer is managed by the __free(free_trace_wprobe) guard, it will automatically invoke this cleanup function:

alloc_trace_wprobe() {
    ...
    tw = kzalloc_flex(*tw, tp.args, nargs);
    ...
    if (symbol) {
        tw->symbol = kstrdup(symbol, GFP_KERNEL);
        if (!tw->symbol)
            return ERR_PTR(-ENOMEM); /* triggers free_trace_wprobe(tw) */
    }
    ...
    INIT_WORK(&tw->work, wprobe_work_func);
}

Because the early return happens before INIT_WORK() is called, cancel_work_sync() will operate on a zeroed work_struct, which can cause a WARN_ON(!work->func).

Quoted 3 lines
kernel/trace/trace_wprobe.c +0 -0
 		trace_probe_cleanup(&tw->tp);
 		kfree(tw->symbol);
 		kfree(tw);
Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=11
author maintainer
#29 lore raw

Re: [PATCH v14 11/14] tracing/wprobe: Add set_wprobe and clear_wprobe event triggers

Quoted 22 lines · On Sun, 30 Aug 2026 14:52:52 +0000 [email protected] wrote:

[Severity: Medium]
Will this trigger a kernel warning in __flush_work() if the work struct has not been initialized yet?

In alloc_trace_wprobe(), if the kstrdup() allocation for the symbol fails, the function returns early. Since the tw pointer is managed by the __free(free_trace_wprobe) guard, it will automatically invoke this cleanup function:

alloc_trace_wprobe() {
    ...
    tw = kzalloc_flex(*tw, tp.args, nargs);
    ...
    if (symbol) {
        tw->symbol = kstrdup(symbol, GFP_KERNEL);
        if (!tw->symbol)
            return ERR_PTR(-ENOMEM); /* triggers free_trace_wprobe(tw) */
    }
    ...
    INIT_WORK(&tw->work, wprobe_work_func);
}
Quoted 3 lines

Because the early return happens before INIT_WORK() is called, cancel_work_sync() will operate on a zeroed work_struct, which can cause a WARN_ON(!work->func).

Oops, good catch!

OK, I'll move the initialize those first and also check tw->work.func in free_trace_wprobe() for safety.

Thank you!

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

PATCH v14 12/14 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 event and static trace event to monitor memory accesses within the trace-events-sample kernel module. Also add a testcase for verifying wprobe trigger syntax error logging.

---
Changes in v14:
- Update dummy wprobe event definition to use '-1' instead of '0'.
- Add BTF argument dependency requirement in trigger-wprobe.tc.
- Add delay before clear_trace in trigger-wprobe.tc to avoid race with clear_wprobe trigger execution.
Changes in v13:
- Look up target function dynamically in trigger-wprobe-syntax-errors.tc.
- Disable set_wprobe trigger before verifying clear_wprobe in trigger-wprobe.tc to avoid race condition with sample_timer_cb.
Changes in v12:
- Add trigger-wprobe-syntax-errors.tc for verifying wprobe trigger syntax error logging.
- Fix requires line in trigger-wprobe-syntax-errors.tc so test is not evaluated as unsupported prior to execution.
- Define dfd=$arg1 fetcharg on testevent for trigger syntax checks.
Changes in v11:
- Update testcase to use trace-events-sample kernel module instead of VFS file operations.

---
 tools/testing/selftests/ftrace/config              |    1 
 .../test.d/trigger/trigger-wprobe-syntax-errors.tc |   37 ++++++++
 .../ftrace/test.d/trigger/trigger-wprobe.tc        |   90 ++++++++++++++++++++
 3 files changed, 128 insertions(+)
 create mode 100644 tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-syntax-errors.tc
 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-syntax-errors.tc new file +37 -0
new file mode 100644
index 000000000000..3ce154a17f5b
@@ -0,0 +1,37 @@
+#!/bin/sh
+# SPDX-License-Identifier: GPL-2.0
+# description: event trigger - test wprobe trigger syntax errors
+# requires: dynamic_events error_log "w[:[<group>/][<event>]] [r|w|rw]@<addr>[:<len>]":README "f[:[<group>/][<event>]] <func-name>[%return] [<args>]":README
+
+check_error() { # command-with-error-pos-by-^
+    ftrace_errlog_check "wprobe_trigger" "$1" "events/fprobes/testevent/trigger"
+}
+
+TARGET_FUNC=$(grep -m 1 -E -w "[tT] (vfs_read|do_sys_openat2|do_sys_open)" /proc/kallsyms | awk '{print $3}')
+if [ -z "$TARGET_FUNC" ]; then
+    echo "UNRESOLVED: target function not found"
+    exit_unresolved
+fi
+
+# Add a dummy fprobe event to attach triggers to
+echo "f:fprobes/testevent $TARGET_FUNC dfd=\$arg1" > dynamic_events
+
+# Add a target wprobe event
+echo 'w:watch rw@-1:8' >> dynamic_events
+
+# Test errors on trigger syntax
+check_error 'set_wprobe:^non_exist_wprobe:dfd'	# WPROBE_NOT_FOUND
+check_error 'set_wprobe:^'				# WPROBE_NOT_FOUND
+check_error 'set_wprobe:watch^'				# WPROBE_NEED_FIELD
+check_error 'set_wprobe:watch:^non_exist_field'	# NO_EVENT_FIELD
+
+# Enable target wprobe event and test WPROBE_BUSY error
+echo 1 > events/wprobes/watch/enable
+check_error 'set_wprobe:^watch:dfd'			# WPROBE_BUSY
+echo 0 > events/wprobes/watch/enable
+
+# Cleanup
+echo '-:watch' >> dynamic_events
+echo '-:testevent' >> dynamic_events
+
+exit 0
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe.tc new file +90 -0

90 lines not on this page · raw message

#31 lore raw

Re: [PATCH v14 12/14] selftests: ftrace: Add wprobe trigger testcase

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 10 lines

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

Add a testcase for checking wprobe trigger. This sets set_wprobe and clear_wprobe triggers on fprobe event and static trace event to monitor memory accesses within the trace-events-sample kernel module. Also add a testcase for verifying wprobe trigger syntax error logging.

Sashiko has reviewed this patch and found no issues. It looks great!

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

PATCH v14 13/14 tracing/wprobe: Support BTF typecast in fetchargs

In reply to Masami Hiramatsu (Google)

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

Allow BTF typecast syntax (STRUCT)FETCHARG->MEMBER in wprobe event fetchargs. Previously, handle_typecast() rejected any probe context that was not a function entry/return or tracepoint event probe.

Wprobe events use $addr (the accessed address) and $value (the value at that address). By enabling BTF typecast, users can now cast these to a concrete struct type and access its fields directly. For example:

echo 'w:watch rw@-1:8 dflag=(dentry)$addr->d_flags' >> dynamic_events

With a set_wprobe trigger pointing the watchpoint at a dentry address, the resulting trace shows d_flags being accessed at that location.

Note that $addr and $value are restricted to kernel-space memory, which is consistent with the existing TPARG_FL_KERNEL flag used when parsing wprobe fetchargs.

Assisted-by Antigravity:gemini-3.5-flash

---
Changes in v14:
- Update dummy wprobe event definition to use '-1' instead of '0'.
Changes in v11:
- Update trigger-wprobe-btf-typecast.tc to use trace-events-sample kernel module.
- Fix commit comment.
Changes in v9:
- Newly added.

---
 kernel/trace/trace_probe.c                         |   13 +++-
 kernel/trace/trace_probe.h                         |    5 +
 tools/testing/selftests/ftrace/config              |    1 
 .../test.d/trigger/trigger-wprobe-btf-typecast.tc  |   72 ++++++++++++++++++++
 4 files changed, 90 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-typecast.tc
kernel/trace/trace_probe.c +12 -1
index 5033fffd71c5..97d8ca747fd1 100644
@@ -890,6 +890,16 @@ static int query_btf_struct(const char *sname, struct traceprobe_parse_context *
 		ctx->struct_btf = NULL;
 	}
 
+	if (ctx->btf) {
+		id = btf_find_by_name_kind(ctx->btf, sname, BTF_KIND_STRUCT);
+		if (id > 0) {
+			btf_get(ctx->btf);
+			ctx->struct_btf = ctx->btf;
+			ctx->last_struct = btf_type_by_id(ctx->struct_btf, id);
+			return 0;
+		}
+	}
+
 	id = bpf_find_btf_id(sname, BTF_KIND_STRUCT, &btf);
 	if (id < 0)
 		return id;
@@ -965,7 +975,8 @@ static int handle_typecast(char *arg, struct traceprobe_parse_context *ctx)
 
 	if (!(tparg_is_event_probe(ctx->flags) ||
 	      tparg_is_function_entry(ctx->flags) ||
-	      tparg_is_function_return(ctx->flags))) {
+	      tparg_is_function_return(ctx->flags) ||
+	      tparg_is_wprobe(ctx->flags))) {
 		trace_probe_log_err(ctx->offset, NOSUP_BTFARG);
 		return -EOPNOTSUPP;
 	}
kernel/trace/trace_probe.h +5 -0
index e08f17c99138..8c495097acf2 100644
@@ -437,6 +437,11 @@ static inline bool tparg_is_event_probe(unsigned int flags)
 	return !!(flags & TPARG_FL_TEVENT);
 }
 
+static inline bool tparg_is_wprobe(unsigned int flags)
+{
+	return !!(flags & TPARG_FL_WPROBE);
+}
+
 /* Each typecast consumes nested level. So the max number of typecast is 8. */
 #define TRACEPROBE_MAX_NESTED_LEVEL 8
 
tools/testing/selftests/ftrace/config +1 -0
index ecdee77f360f..f067874902ed 100644
@@ -1,5 +1,6 @@
 CONFIG_BPF_SYSCALL=y
 CONFIG_DEBUG_INFO_BTF=y
+CONFIG_DEBUG_INFO_BTF_MODULES=y
 CONFIG_DEBUG_INFO_DWARF4=y
 CONFIG_EPROBE_EVENTS=y
 CONFIG_FPROBE=y
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-typecast.tc new file +72 -0

72 lines not on this page · raw message

#33 lore raw

Re: [PATCH v14 13/14] tracing/wprobe: Support BTF typecast in fetchargs

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 15 lines

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

Allow BTF typecast syntax (STRUCT)FETCHARG->MEMBER in wprobe event fetchargs. Previously, handle_typecast() rejected any probe context that was not a function entry/return or tracepoint event probe.

Wprobe events use $addr (the accessed address) and $value (the value at that address). By enabling BTF typecast, users can now cast these to a concrete struct type and access its fields directly. For example:

echo 'w:watch rw@-1:8 dflag=(dentry)$addr->d_flags' >> dynamic_events

With a set_wprobe trigger pointing the watchpoint at a dentry address, the resulting trace shows d_flags being accessed at that location.

Quoted 6 lines

Note that $addr and $value are restricted to kernel-space memory, which is consistent with the existing TPARG_FL_KERNEL flag used when parsing wprobe fetchargs.

Assisted-by Antigravity:gemini-3.5-flash

Sashiko has reviewed this patch and found no issues. It looks great!

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

PATCH v14 14/14 tracing/wprobe: Support BTF struct offset resolution in set_wprobe trigger

In reply to Masami Hiramatsu (Google)

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

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

---
Changes in v14:
- Update dummy wprobe event definition to use '-1' instead of '0'.
- Pass member_type to btf_find_struct_member() and check its kflag in get_offset_of_field().
Changes in v13:
- Check for field token before calling wprobe_trigger_field_parse() in clear_wprobe to avoid spurious error log entries on numeric counts.
Changes in v12:
- Refactor field parsing logic into wprobe_trigger_field_parse().
- Remove unused variable count_str in wprobe_trigger_cmd_parse().

---
 kernel/trace/trace_wprobe.c                        |  264 ++++++++++++++------
 .../test.d/trigger/trigger-wprobe-btf-offset.tc    |   74 ++++++
 2 files changed, 263 insertions(+), 75 deletions(-)
 create mode 100644 tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-offset.tc
kernel/trace/trace_wprobe.c +189 -75
index 29eae708ae7b..a6ea7344dc41 100644
@@ -27,6 +27,7 @@
 #include <asm/ptrace.h>
 
 #include "trace.h"
+#include "trace_btf.h"
 #include "trace_dynevent.h"
 #include "trace_probe.h"
 #include "trace_probe_kernel.h"
@@ -971,9 +972,10 @@ static int wprobe_trigger_print(struct seq_file *m,
 		seq_printf(m, ":count=%ld", data->count);
 
 	if (data->filter_str)
-		seq_printf(m, " if %s\n", data->filter_str);
-	else
-		seq_putc(m, '\n');
+		seq_printf(m, " if %s", data->filter_str);
+
+	seq_printf(m, " # offset:%d adjust:%ld\n",
+		   wprobe_data->offset, wprobe_data->adjust);
 
 	return 0;
 }
@@ -1011,6 +1013,182 @@ static void wprobe_trigger_free(struct event_trigger_data *data)
 	}
 }
 
+#ifdef CONFIG_PROBE_EVENTS_BTF_ARGS
+
+static int get_offset_of_field(struct btf *btf, const struct btf_type *type, char *field_name)
+{
+	const struct btf_member *field;
+	const struct btf_type *mtype;
+	int bitoffs = 0;
+	u32 anon_offs;
+	char *next;
+
+	do {
+		next = strchr(field_name, '.');
+		if (next)
+			*next++ = '\0';
+
+		field = btf_find_struct_member(btf, type, field_name, &anon_offs, &mtype);
+		if (IS_ERR_OR_NULL(field))
+			return -ENOENT;
+
+		if (btf_type_kflag(mtype)) {
+			/* Reject bitfield member access */
+			if (BTF_MEMBER_BITFIELD_SIZE(field->offset))
+				return -EINVAL;
+			bitoffs += anon_offs + BTF_MEMBER_BIT_OFFSET(field->offset);
+		} else {
+			bitoffs += anon_offs + field->offset;
+		}
+
+		field_name = next;
+		if (next) {
+			type = btf_type_skip_modifiers(btf, field->type, NULL);
+			if (!type)
+				return -ENOENT;
+		}
+	} while (next);
+	return bitoffs / BITS_PER_BYTE;
+}
+
+/* btf_put(NULL) is acceptable. */
+DEFINE_FREE(btf_put, struct btf *, btf_put(_T))
+
+/* parse typecast: (TYPE[,ASGN])EVENT_FIELD->FIELD[.SUBFIELD...] and set adjust. */
+static int wprobe_trigger_typecast_parse(char **field_str_ptr,
+					 struct trace_event_file *file,
+					 struct wprobe_trigger_data *wprobe_data,
+					 const char *glob)
+{
+	struct btf *btf __free(btf_put) = NULL;
+	const struct btf_type *type;
+	char *assign_field;
+	char *event_field;
+	char *type_field;
+	char *type_name;
+	char *offs;
+	long val = 0;
+	int id;
+	int adjust;
+
+	type_name = *field_str_ptr + 1;
+	event_field = strchr(type_name, ')');
+	if (!event_field) {
+		wprobe_trigger_log_err(file, glob, type_name - glob, DEREF_OPEN_BRACE);
+		return -EINVAL;
+	}
+	*event_field++ = '\0';
+
+	/* Check the optional assign field. */
+	assign_field = strchr(type_name, ',');
+	if (assign_field)
+		*assign_field++ = '\0';
+
+	/* Get the type field name. */
+	type_field = strstr(event_field, "->");
+	if (!type_field) {
+		wprobe_trigger_log_err(file, glob, event_field - glob, TYPECAST_REQ_FIELD);
+		return -EINVAL;
+	}
+	*type_field = '\0';
+	type_field += 2;
+
+	offs = strpbrk(type_field, "+-");
+	if (offs) {
+		if (kstrtol(offs, 0, &val) < 0) {
+			wprobe_trigger_log_err(file, glob, offs - glob, BAD_DEREF_OFFS);
+			return -EINVAL;
+		}
+		*offs = '\0';
+	}
+
+	/* find type from BTF */
+	id = bpf_find_btf_id(type_name, BTF_KIND_STRUCT, &btf);
+	if (id < 0) {
+		wprobe_trigger_log_err(file, glob, type_name - glob, BAD_BTF_TID);
+		return id;
+	}
+
+	type = btf_type_by_id(btf, id);
+	if (!type) {
+		wprobe_trigger_log_err(file, glob, type_name - glob, BAD_BTF_TID);
+		return -EINVAL;
+	}
+
+	adjust = get_offset_of_field(btf, type, type_field);
+	if (adjust < 0) {
+		wprobe_trigger_log_err(file, glob, type_field - glob, NO_BTF_FIELD);
+		return adjust;
+	}
+	wprobe_data->adjust = adjust + val;
+
+	if (assign_field) {
+		/* assign_field should be a struct field */
+		adjust = get_offset_of_field(btf, type, assign_field);
+		if (adjust < 0) {
+			wprobe_trigger_log_err(file, glob, assign_field - glob, NO_BTF_FIELD);
+			return adjust;
+		}
+		wprobe_data->adjust -= adjust;
+	}
+
+	*field_str_ptr = event_field;
+	return 0;
+}
+#else
+static int wprobe_trigger_typecast_parse(char **field_str_ptr,
+					 struct trace_event_file *file,
+					 struct wprobe_trigger_data *wprobe_data,
+					 const char *glob)
+{
+	wprobe_trigger_log_err(file, glob, *field_str_ptr - glob, NOSUP_BTFARG);
+	return -EOPNOTSUPP;
+}
+#endif /* CONFIG_PROBE_EVENTS_BTF_ARGS */
+
+static int wprobe_trigger_field_parse(char *field_str, struct trace_event_file *file,
+					struct wprobe_trigger_data *wprobe_data,
+					const char *glob)
+{
+	struct ftrace_event_field *field;
+	char *offs;
+
+	if (field_str[0] == '(') {
+		int ret = wprobe_trigger_typecast_parse(&field_str, file, wprobe_data, glob);
+
+		if (ret < 0)
+			return ret;
+	} else {
+		offs = strpbrk(field_str, "+-");
+		if (offs) {
+			long val;
+
+			if (kstrtol(offs, 0, &val) < 0) {
+				wprobe_trigger_log_err(file, glob, offs - glob, BAD_DEREF_OFFS);
+				return -EINVAL;
+			}
+			wprobe_data->adjust = val;
+			*offs = '\0';
+		}
+	}
+
+	field = trace_find_event_field(file->event_call, field_str);
+	if (!field) {
+		wprobe_trigger_log_err(file, glob, field_str - glob, NO_EVENT_FIELD);
+		return -ENOENT;
+	}
+	if (field->size != sizeof(void *)) {
+		wprobe_trigger_log_err(file, glob, field_str - glob, WPROBE_BAD_FIELD);
+		return -ENOEXEC;
+	}
+	wprobe_data->offset = field->offset;
+	wprobe_data->field = kstrdup(field_str, GFP_KERNEL);
+	if (!wprobe_data->field)
+		return -ENOMEM;
+
+	return 0;
+}
+
 static int wprobe_trigger_cmd_parse(struct event_command *cmd_ops,
 				    struct trace_event_file *file,
 				    char *glob, char *cmd,
@@ -1023,10 +1201,8 @@ static int wprobe_trigger_cmd_parse(struct event_command *cmd_ops,
 	struct wprobe_trigger_data *wprobe_data = NULL;
 	struct event_trigger_data *trigger_data = NULL;
 	struct trace_event_file *wprobe_file;
+	char *event_str, *comment;
 	struct trace_array *tr = file->tr;
-	char *event_str, *field_str, *comment;
-	struct ftrace_event_field *field;
-	struct trace_event_call *event;
 	bool remove, clear = false;
 	unsigned long orig_addr = 0;
 	struct trace_wprobe *tw;
@@ -1087,86 +1263,24 @@ static int wprobe_trigger_cmd_parse(struct event_command *cmd_ops,
 
 	/* clear_wprobe does not need field, but can have optional field. */
 	if (!clear) {
-		char *offs;
+		char *field_str = strsep(&param, ":");
 
-		/* Find target field, which must be equivalent to "void *" */
-		field_str = strsep(&param, ":");
 		if (!field_str) {
 			wprobe_trigger_log_err(file, glob, strlen(glob), WPROBE_NEED_FIELD);
 			ret = -EINVAL;
 			goto out_free;
 		}
-
-		offs = strpbrk(field_str, "+-");
-		if (offs) {
-			long val;
-
-			if (kstrtol(offs, 0, &val) < 0) {
-				wprobe_trigger_log_err(file, glob, offs - glob, BAD_DEREF_OFFS);
-				ret = -EINVAL;
-				goto out_free;
-			}
-			wprobe_data->adjust = val;
-			*offs = '\0';
-		}
-
-		event = file->event_call;
-		field = trace_find_event_field(event, field_str);
-		if (!field) {
-			wprobe_trigger_log_err(file, glob, field_str - glob, NO_EVENT_FIELD);
-			ret = -ENOENT;
-			goto out_free;
-		}
-
-		if (field->size != sizeof(void *)) {
-			wprobe_trigger_log_err(file, glob, field_str - glob, WPROBE_BAD_FIELD);
-			ret = -ENOEXEC;
-			goto out_free;
-		}
-		wprobe_data->offset = field->offset;
-		wprobe_data->field = kstrdup(field_str, GFP_KERNEL);
-		if (!wprobe_data->field) {
-			ret = -ENOMEM;
+		ret = wprobe_trigger_field_parse(field_str, file, wprobe_data, glob);
+		if (ret < 0)
 			goto out_free;
-		}
-	} else if (param && (isalpha(param[0]) || param[0] == '_')) {
+	} else if (param && (isalpha(param[0]) || param[0] == '_' || param[0] == '(')) {
 		if (strncmp(param, "count=", 6) != 0 &&
 		    strncmp(param, "unlimited", 9) != 0) {
-			char *offs;
-
-			field_str = strsep(&param, ":");
-			offs = strpbrk(field_str, "+-");
-			if (offs) {
-				long val;
-
-				if (kstrtol(offs, 0, &val) < 0) {
-					wprobe_trigger_log_err(file, glob, offs - glob, BAD_DEREF_OFFS);
-					ret = -EINVAL;
-					goto out_free;
-				}
-				wprobe_data->adjust = val;
-				*offs = '\0';
-			}
+			char *field_str = strsep(&param, ":");
 
-			event = file->event_call;
-			field = trace_find_event_field(event, field_str);
-			if (!field) {
-				wprobe_trigger_log_err(file, glob, field_str - glob, NO_EVENT_FIELD);
-				ret = -ENOENT;
+			ret = wprobe_trigger_field_parse(field_str, file, wprobe_data, glob);
+			if (ret < 0)
 				goto out_free;
-			}
-
-			if (field->size != sizeof(void *)) {
-				wprobe_trigger_log_err(file, glob, field_str - glob, WPROBE_BAD_FIELD);
-				ret = -ENOEXEC;
-				goto out_free;
-			}
-			wprobe_data->offset = field->offset;
-			wprobe_data->field = kstrdup(field_str, GFP_KERNEL);
-			if (!wprobe_data->field) {
-				ret = -ENOMEM;
-				goto out_free;
-			}
 		}
 	}
 
tools/testing/selftests/ftrace/test.d/trigger/trigger-wprobe-btf-offset.tc new file +74 -0

74 lines not on this page · raw message

#35 lore raw

Re: [PATCH v14 14/14] tracing/wprobe: Support BTF struct offset resolution in set_wprobe trigger

Sashiko has reviewed this patch and found no issues. It looks great!
Quoted 8 lines

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

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

Sashiko has reviewed this patch and found no issues. It looks great!

Signature · 1 line
Sashiko AI review · https://sashiko.dev/#/patchset/178810001186.64882.2161016469449127450.stgit@devnote2?part=14